Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1 @@
|
||||
You are Hermes Agent, built by Nous Research. Be direct: match the length of your reply to the weight of the ask — a one-line question gets a one-line answer, and finished work gets a short report of what changed, what's verified, and what's left, never a replay of the process. No filler ("Great question," "I'd be happy to"), no restating the request back, no re-summarizing what you already said, no narrating tool calls the user can see. Plain claims over adjectives; when unsure, say so plainly. Agree because it's right, not because the user said it. Depth is earned — give it when the user asks for detail, teaches, or the stakes demand it, not by default.
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/command/with-contenv sh
|
||||
# shellcheck shell=sh
|
||||
# Make supervise/ trees for ALL declared s6 services queryable and
|
||||
# controllable by the unprivileged hermes user (UID 10000).
|
||||
#
|
||||
# Background (PR #30136 review item I4): the entire s6 lifecycle
|
||||
# (s6-svc, s6-svstat, s6-svwait) is dispatched as the hermes user
|
||||
# inside the container (every Hermes runtime path runs under
|
||||
# ``s6-setuidgid hermes``). But s6-supervise creates each service's
|
||||
# ``supervise/`` and top-level ``event/`` directory with mode 0700
|
||||
# owned by its effective UID — which is root, because s6-supervise
|
||||
# is spawned by s6-svscan running as PID 1. So unprivileged clients
|
||||
# get EACCES on every probe / control call against the slot.
|
||||
#
|
||||
# Two fixes, one in each registration path:
|
||||
#
|
||||
# 1. For RUNTIME-registered profile gateways (created via the s6
|
||||
# runtime register hooks in profiles.py): the Python helper
|
||||
# ``_seed_supervise_skeleton`` pre-creates supervise/ + event/ +
|
||||
# supervise/control owned by hermes BEFORE s6-svscanctl -a fires.
|
||||
# s6-supervise's mkdir/mkfifo are EEXIST-safe, so it inherits our
|
||||
# ownership and never tries to chown back to root.
|
||||
#
|
||||
# 2. For STATIC s6-rc services (dashboard, main-hermes) declared at
|
||||
# image-build time under /etc/s6-overlay/s6-rc.d/*: these are
|
||||
# compiled by s6-rc at boot, and s6-supervise spawns BEFORE
|
||||
# cont-init.d gets to run — so by the time we're here, the
|
||||
# supervise/ tree is already there as root:root 0700. We chown
|
||||
# it here. s6-supervise will keep using the same files; it never
|
||||
# re-asserts ownership on a running service.
|
||||
#
|
||||
# This script runs as root after 01-hermes-setup but before
|
||||
# 02-reconcile-profiles, so the chowns are settled before the
|
||||
# Python reconciler walks the scandir. Lexicographic ordering
|
||||
# guarantees this — the suffix is unusual because we want to slot
|
||||
# in between 01 and the existing 02-reconcile-profiles without
|
||||
# renumbering both (which would be a churn-noise patch on its own).
|
||||
|
||||
set -eu
|
||||
|
||||
# /run/s6-rc/servicedirs holds the live, compiled service directories
|
||||
# for every static (s6-rc) service. Symlinks under /run/service/*
|
||||
# point here. Per-service supervise/ + event/ both need hermes
|
||||
# ownership for s6-svstat etc. to work as hermes.
|
||||
SVC_ROOT=/run/s6-rc/servicedirs
|
||||
|
||||
if [ ! -d "$SVC_ROOT" ]; then
|
||||
echo "[supervise-perms] $SVC_ROOT not present; skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for svc in "$SVC_ROOT"/*; do
|
||||
[ -d "$svc" ] || continue
|
||||
name=$(basename "$svc")
|
||||
|
||||
# Skip s6-overlay-internal services (they need to stay root-only;
|
||||
# the s6rc-* helpers manage the supervision tree itself).
|
||||
case "$name" in
|
||||
s6rc-*|s6-linux-*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
# supervise/ tree — needed by s6-svstat / s6-svc.
|
||||
if [ -d "$svc/supervise" ]; then
|
||||
chown -R hermes:hermes "$svc/supervise" 2>/dev/null || \
|
||||
echo "[supervise-perms] could not chown $svc/supervise"
|
||||
# 0710 = group searchable. ``s6-svstat`` only needs to openat
|
||||
# status, not list the dir, but giving the hermes group +x is
|
||||
# the minimum that lets group members access the contents.
|
||||
chmod 0710 "$svc/supervise" 2>/dev/null || true
|
||||
# supervise/control is a FIFO that s6-svc writes commands
|
||||
# into; the hermes user needs +w. Owner is already hermes
|
||||
# after the recursive chown above; widen perms to 0660 so
|
||||
# ``s6-svc`` works for any member of the hermes group too.
|
||||
if [ -p "$svc/supervise/control" ]; then
|
||||
chmod 0660 "$svc/supervise/control" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Top-level event/ dir — s6-svlisten1 / s6-svwait subscribe here.
|
||||
if [ -d "$svc/event" ]; then
|
||||
chown hermes:hermes "$svc/event" 2>/dev/null || \
|
||||
echo "[supervise-perms] could not chown $svc/event"
|
||||
# Preserve s6's 03730 mode (setgid + g+rwx + sticky).
|
||||
chmod 03730 "$svc/event" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo "[supervise-perms] chowned supervise/ trees for static s6-rc services"
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/command/with-contenv sh
|
||||
# shellcheck shell=sh
|
||||
# Container-boot reconciliation of per-profile gateway s6 services.
|
||||
#
|
||||
# Runs as root after 01-hermes-setup (the stage2 hook) has chowned
|
||||
# the volume and seeded $HERMES_HOME, but before s6-rc starts user
|
||||
# services. /etc/cont-init.d/* scripts run in lexicographic order,
|
||||
# so the `02-` prefix guarantees ordering.
|
||||
#
|
||||
# Service directories under /run/service/ live on tmpfs and are
|
||||
# wiped on every container restart. Profile directories under
|
||||
# $HERMES_HOME/profiles/ live on the persistent VOLUME. This script
|
||||
# walks the persistent profiles, recreates the s6 service slots,
|
||||
# and auto-starts only those whose last recorded state was
|
||||
# `running` — see hermes_cli/container_boot.py.
|
||||
#
|
||||
# Phase 4 also needs hermes-user writes to /run/service/ (so the
|
||||
# profile create/delete hooks can register/unregister at runtime),
|
||||
# so we chown the scandir before invoking the reconciler. We
|
||||
# additionally chown the s6-svscan control FIFO so the hermes user
|
||||
# can send rescan signals via ``s6-svscanctl -a``; without this the
|
||||
# entire runtime-registration path is inert under UID 10000 (the
|
||||
# Python wrapper catches the resulting EACCES, prints a warning,
|
||||
# and swallows the failure).
|
||||
set -e
|
||||
|
||||
# Make the dynamic scandir hermes-writable. The directory itself
|
||||
# starts root-owned by s6-overlay.
|
||||
chown hermes:hermes /run/service 2>/dev/null || true
|
||||
|
||||
# Make the svscan control FIFO hermes-writable so s6-svscanctl -a
|
||||
# / -an work for the hermes user. The FIFO is created by s6-svscan
|
||||
# at PID-1 startup, so by the time this cont-init.d script runs it
|
||||
# already exists. Both ``control`` and ``lock`` need to be writable
|
||||
# for the various svscanctl operations; the directory itself stays
|
||||
# root-owned (we only need to touch the two FIFOs/locks inside).
|
||||
if [ -d /run/service/.s6-svscan ]; then
|
||||
for entry in control lock; do
|
||||
if [ -e "/run/service/.s6-svscan/$entry" ]; then
|
||||
chown hermes:hermes "/run/service/.s6-svscan/$entry" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Skip the drop when already non-root.
|
||||
[ "$(id -u)" = 0 ] || exec /opt/hermes/.venv/bin/python -m hermes_cli.container_boot
|
||||
exec s6-setuidgid hermes /opt/hermes/.venv/bin/python -m hermes_cli.container_boot
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=sh
|
||||
# Entry-point dispatcher for runtimes that may or may not give the image
|
||||
# ownership of PID 1.
|
||||
#
|
||||
# Normal Docker / Podman path: this script is PID 1, so we delegate to
|
||||
# s6-overlay's /init exactly as before and keep the full supervision tree.
|
||||
#
|
||||
# Wrapped-runtime path (Fly Machines, `docker run --init`, some Nomad/K8s
|
||||
# setups): the platform's own init is already PID 1 and execs the image
|
||||
# entrypoint as a child. s6-overlay aborts there with "can only run as pid 1",
|
||||
# so we run the stage2 bootstrap directly and then exec the main wrapper
|
||||
# without /init.
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$$" -eq 1 ]; then
|
||||
exec /init /opt/hermes/docker/main-wrapper.sh "$@"
|
||||
fi
|
||||
|
||||
echo "[hermes] WARNING: container entrypoint is not PID 1; skipping s6-overlay /init and falling back to direct bootstrap. Supervised services are unavailable in this runtime, but the requested command will still run." >&2
|
||||
# /init normally seeds PATH with s6's helpers; the non-PID-1 fallback skips it.
|
||||
export PATH="/command:/package/admin/s6/command:${PATH}"
|
||||
/opt/hermes/docker/stage2-hook.sh
|
||||
exec /opt/hermes/docker/main-wrapper.sh "$@"
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# s6-overlay shim. The real logic lives in docker/stage2-hook.sh, invoked
|
||||
# by /etc/cont-init.d/01-hermes-setup (installed by the Dockerfile). This
|
||||
# file exists so external references to docker/entrypoint.sh still work,
|
||||
# but it's no longer the ENTRYPOINT — /init is.
|
||||
#
|
||||
# When called directly (e.g. by an old wrapper script that hard-coded
|
||||
# docker/entrypoint.sh as the container ENTRYPOINT, or by an external
|
||||
# orchestration script that invokes it inside the container), forward to
|
||||
# the stage2 hook for parity with the pre-s6 entrypoint behavior. The
|
||||
# stage2 hook only handles cont-init bootstrap (UID remap, chown, config
|
||||
# seed, skills sync); it does NOT exec the CMD. Callers that depended
|
||||
# on the pre-s6 contract "entrypoint.sh sets up state then execs hermes"
|
||||
# will see the bootstrap happen but the CMD will not run from this shim.
|
||||
#
|
||||
# Deprecation: this shim is preserved for one release cycle to give
|
||||
# downstream users time to migrate their wrappers to the image's real
|
||||
# ENTRYPOINT (`/init`). It will be removed in a future major release.
|
||||
# Surface a warning to stderr so anyone still invoking this path
|
||||
# sees the migration notice in their logs.
|
||||
echo "[hermes] WARNING: docker/entrypoint.sh is a deprecated shim under " \
|
||||
"s6-overlay. The container's real ENTRYPOINT is " \
|
||||
"entrypoint-dispatch.sh (which delegates to /init + main-wrapper.sh " \
|
||||
"when PID 1); this script only runs the stage2 cont-init hook " \
|
||||
"and does NOT exec the CMD. If you hard-coded docker/entrypoint.sh " \
|
||||
"as your ENTRYPOINT, drop the override — docker will use the image's " \
|
||||
"default ENTRYPOINT dispatcher, which handles bootstrap AND CMD." >&2
|
||||
exec /opt/hermes/docker/stage2-hook.sh "$@"
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=sh
|
||||
# /opt/hermes/bin/hermes — `docker exec` privilege-drop shim.
|
||||
#
|
||||
# Background
|
||||
# ----------
|
||||
# The s6 image runs the supervised gateway/main process as the unprivileged
|
||||
# `hermes` user (UID 10000). When an operator runs `docker exec <c> hermes ...`
|
||||
# the default UID is root (0), and any file the command writes under
|
||||
# $HERMES_HOME — auth.json, .env, config.yaml — ends up root-owned and
|
||||
# unreadable to the supervised gateway. The most common manifestation: the
|
||||
# user runs `docker exec <c> hermes login`, this writes
|
||||
# /opt/data/auth.json as root:root mode 0600, and from then on the gateway
|
||||
# returns "Provider authentication failed: Hermes is not logged into Nous
|
||||
# Portal" on every incoming message — even though `docker exec <c> hermes
|
||||
# chat -q ping` (also running as root) succeeds because root happens to be
|
||||
# able to read its own root-owned file. See systematic-debugging skill
|
||||
# notes attached to this fix.
|
||||
#
|
||||
# Fix
|
||||
# ---
|
||||
# This shim sits at /opt/hermes/bin/hermes and is placed earliest on PATH.
|
||||
# When invoked as root, it drops to the hermes user (via s6-setuidgid)
|
||||
# before exec'ing the real venv binary, so anything that writes under
|
||||
# $HERMES_HOME is uid-aligned with the supervised processes. When invoked
|
||||
# as any non-root UID — including the supervised processes themselves,
|
||||
# `docker exec --user hermes`, kanban subagents, etc. — it short-circuits
|
||||
# straight to the venv binary with no privilege change. Net: one extra
|
||||
# fork on the docker-exec-as-root path, zero behavioral change on every
|
||||
# other path.
|
||||
#
|
||||
# Recursion safety: the shim exec's the venv binary by *absolute path*
|
||||
# (/opt/hermes/.venv/bin/hermes), so the second hop cannot re-enter this
|
||||
# shim regardless of PATH state. No sentinel env var needed.
|
||||
#
|
||||
# Opt-out: set HERMES_DOCKER_EXEC_AS_ROOT=1 (1/true/yes, case-insensitive)
|
||||
# to keep running as root. Reserved for diagnostic sessions where the
|
||||
# operator deliberately wants root semantics — e.g. inspecting root-only
|
||||
# state via the hermes CLI. Default is to drop.
|
||||
|
||||
set -e
|
||||
|
||||
REAL=/opt/hermes/.venv/bin/hermes
|
||||
|
||||
# Defensive: if the venv binary is missing (corrupted image, partial
|
||||
# install), fail loudly rather than silently masking it.
|
||||
if [ ! -x "$REAL" ]; then
|
||||
echo "hermes-shim: $REAL not found or not executable" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
# Already non-root? Just exec the real binary. This is the hot path for
|
||||
# supervised processes (uid 10000) and for `docker exec --user hermes`.
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
exec "$REAL" "$@"
|
||||
fi
|
||||
|
||||
# Root, with opt-out set? Honor it.
|
||||
case "${HERMES_DOCKER_EXEC_AS_ROOT:-}" in
|
||||
1|true|TRUE|True|yes|YES|Yes)
|
||||
exec "$REAL" "$@"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Root, no opt-out. Drop to the hermes user.
|
||||
#
|
||||
# s6-setuidgid lives under /command/ which is NOT on `docker exec`'s PATH
|
||||
# (s6-overlay only puts /command/ on PATH for supervision-tree children).
|
||||
# Reference it by absolute path so the drop is robust against PATH
|
||||
# manipulation.
|
||||
S6_SUID=/command/s6-setuidgid
|
||||
if [ ! -x "$S6_SUID" ]; then
|
||||
# Non-s6 image (someone stripped s6-overlay, or a hand-built variant).
|
||||
# Fail loud rather than silently re-execing as root and leaking the
|
||||
# bug this shim exists to prevent.
|
||||
echo "hermes-shim: $S6_SUID not found; refusing to silently run as root." >&2
|
||||
echo "hermes-shim: re-run with --user hermes or set HERMES_DOCKER_EXEC_AS_ROOT=1." >&2
|
||||
exit 126
|
||||
fi
|
||||
|
||||
# Reset HOME to the hermes user's home before dropping privileges. Without
|
||||
# this, $HOME stays /root and any library that resolves paths off $HOME
|
||||
# (XDG caches, lockfiles, .config writes) will try to write to /root and
|
||||
# fail with EACCES. Mirrors main-wrapper.sh.
|
||||
export HOME=/opt/data
|
||||
|
||||
exec "$S6_SUID" hermes "$REAL" "$@"
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=sh
|
||||
# /opt/hermes/docker/main-wrapper.sh — wraps the container's CMD with
|
||||
# the same argument-routing logic the pre-s6 entrypoint.sh used. Runs
|
||||
# as /init's "main program" (Docker CMD) so it inherits stdin/stdout/
|
||||
# stderr from the container. The non-PID-1 entrypoint fallback also
|
||||
# execs this script directly after running the stage2 bootstrap.
|
||||
#
|
||||
# Env note: /init scrubs env before invoking CMD, so when this wrapper
|
||||
# is launched through the supervised path it must rehydrate via
|
||||
# with-contenv before touching HERMES_HOME / PATH. On the non-PID-1
|
||||
# fallback path the Dockerfile env is still intact, so we skip the
|
||||
# re-exec and continue directly.
|
||||
#
|
||||
# Routing:
|
||||
# no args → exec `hermes` (the default)
|
||||
# first arg is an executable → exec it directly (sleep, bash, sh, …)
|
||||
# first arg is anything else → exec `hermes <args>` (subcommand passthrough)
|
||||
#
|
||||
# Drop to hermes via s6-setuidgid, but skip it when already non-root.
|
||||
set -e
|
||||
|
||||
if [ -z "${HERMES_MAIN_WRAPPER_ENV_READY:-}" ] && \
|
||||
[ -z "${HERMES_HOME:-}" ] && \
|
||||
[ -x /command/with-contenv ]; then
|
||||
export HERMES_MAIN_WRAPPER_ENV_READY=1
|
||||
exec /command/with-contenv sh "$0" "$@"
|
||||
fi
|
||||
unset HERMES_MAIN_WRAPPER_ENV_READY
|
||||
|
||||
drop() { [ "$(id -u)" = 0 ] && set -- s6-setuidgid hermes "$@"; exec "$@"; }
|
||||
|
||||
# --- Reject the unsupported `docker run --user <uid>:<gid>` start ---
|
||||
# Mirror the guard in stage2-hook.sh (cont-init). This is the surface the
|
||||
# user actually sees in `docker run` output: when the container is pinned to
|
||||
# an arbitrary non-root, non-hermes UID, the bootstrap was skipped and the
|
||||
# baked image dirs (owned by the hermes build UID) are unwritable, so fail
|
||||
# fast here with actionable guidance rather than crashing on `cd`/EACCES
|
||||
# further down. See stage2-hook.sh for the full rationale.
|
||||
cur_uid="$(id -u)"
|
||||
if [ "$cur_uid" != 0 ] && [ "$cur_uid" != "$(id -u hermes)" ]; then
|
||||
cat >&2 <<EOF
|
||||
[hermes] ERROR: container started with --user $cur_uid (an arbitrary, non-hermes UID) — not supported.
|
||||
|
||||
To make container-written files match your HOST user, don't use --user.
|
||||
Start as root (the default) and pass your host UID/GID instead:
|
||||
|
||||
docker run -e HERMES_UID=\$(id -u) -e HERMES_GID=\$(id -g) ...
|
||||
|
||||
NAS users (Synology / unRAID / UGOS) can use the PUID/PGID aliases:
|
||||
|
||||
docker run -e PUID=\$(id -u) -e PGID=\$(id -g) ...
|
||||
|
||||
The image remaps the hermes user to that UID/GID at boot and chowns the data
|
||||
volume, so files land owned by your host user — the same outcome --user gave,
|
||||
without breaking the s6 supervision tree.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# HOME comes through with-contenv as /root (the /init context). Override
|
||||
# to the hermes user's home before dropping privileges so libraries that
|
||||
# resolve paths via $HOME (e.g. discord lockfile under XDG_STATE_HOME)
|
||||
# don't try to write to /root.
|
||||
export HOME=/opt/data
|
||||
|
||||
# Save the Docker -w (or default) working directory before init
|
||||
# scripts cd to /opt/data, so the container starts in the
|
||||
# directory the user requested.
|
||||
_hermes_orig_cwd="${HERMES_ORIG_CWD:-$PWD}"
|
||||
|
||||
cd /opt/data
|
||||
# shellcheck disable=SC1091
|
||||
. /opt/hermes/.venv/bin/activate
|
||||
|
||||
# Restore the original working directory before handing off to
|
||||
# the user's command so `hermes chat` starts in the Docker -w
|
||||
# directory, not /opt/data.
|
||||
cd "$_hermes_orig_cwd"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
drop hermes
|
||||
fi
|
||||
|
||||
if command -v "$1" >/dev/null 2>&1; then
|
||||
# Bare executable — pass through directly.
|
||||
drop "$@"
|
||||
fi
|
||||
|
||||
# Hermes subcommand pass-through.
|
||||
drop hermes "$@"
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/command/with-contenv sh
|
||||
# shellcheck shell=sh
|
||||
# Dashboard finish script. Companion to ./run.
|
||||
#
|
||||
# When HERMES_DASHBOARD is unset (or falsy), ./run exits 0 immediately.
|
||||
# Without this finish script, s6-supervise would just restart the run
|
||||
# script in a tight loop. By exiting 125 here, we tell s6-supervise
|
||||
# "this service has permanently failed; do not restart" — equivalent
|
||||
# to `s6-svc -O`. The supervise slot reports as down, matching reality
|
||||
# (no dashboard process is running).
|
||||
#
|
||||
# When HERMES_DASHBOARD IS enabled and the run script later exits or
|
||||
# is killed, we want s6-supervise to restart it (the whole point of
|
||||
# supervised lifecycle). So we exit non-125 in that case.
|
||||
|
||||
# Arguments passed to a finish script: $1=run-exit-code, $2=signal-num,
|
||||
# $3=service-dir-name, $4=run-pgid. See servicedir(7).
|
||||
|
||||
case "${HERMES_DASHBOARD:-}" in
|
||||
1|true|TRUE|True|yes|YES|Yes)
|
||||
# Dashboard was enabled — let s6-supervise restart on crash by
|
||||
# exiting non-125. (Pass-through any sensible default.)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
# Dashboard disabled — permanent-failure marker so s6-supervise
|
||||
# leaves the slot in 'down' state and s6-svstat reflects that.
|
||||
exit 125
|
||||
;;
|
||||
esac
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/command/with-contenv sh
|
||||
# shellcheck shell=sh
|
||||
# Dashboard service. Always declared so s6 has a supervised slot; if
|
||||
# HERMES_DASHBOARD isn't truthy the run script exits cleanly and the
|
||||
# companion finish script returns 125 (s6's "permanent failure, do
|
||||
# not restart" marker), so s6-svstat reports the slot as down. See
|
||||
# also docker/s6-rc.d/dashboard/finish.
|
||||
|
||||
case "${HERMES_DASHBOARD:-}" in
|
||||
1|true|TRUE|True|yes|YES|Yes) ;;
|
||||
*)
|
||||
# Exit 0; the finish script will exit 125 → s6-supervise won't
|
||||
# restart us and the slot reports down. Using a clean exit
|
||||
# (rather than `exec sleep infinity`) means s6-svstat reflects
|
||||
# reality: when HERMES_DASHBOARD is unset, the service is NOT
|
||||
# running, just supervised-with-permanent-failure. See PR
|
||||
# #30136 review item I3.
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# with-contenv repopulates HOME from /init as /root. Reset it before
|
||||
# dropping privileges so HOME-anchored state lands under /opt/data.
|
||||
export HOME=/opt/data
|
||||
|
||||
cd /opt/data
|
||||
# shellcheck disable=SC1091
|
||||
. /opt/hermes/.venv/bin/activate
|
||||
|
||||
dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"
|
||||
dash_port="${HERMES_DASHBOARD_PORT:-9119}"
|
||||
|
||||
# The dashboard's auth gate engages automatically on non-loopback binds and
|
||||
# REQUIRES a DashboardAuthProvider to be registered, else start_server fails
|
||||
# closed. Two zero-infra ways to satisfy it in a container:
|
||||
# • Password: set HERMES_DASHBOARD_BASIC_AUTH_USERNAME + _PASSWORD (bundled
|
||||
# dashboard_auth/basic provider — no external IDP).
|
||||
# • OAuth: set HERMES_DASHBOARD_OAUTH_CLIENT_ID (bundled nous provider).
|
||||
#
|
||||
# HERMES_DASHBOARD_INSECURE no longer disables the gate (June 2026 hardening:
|
||||
# unauthenticated public dashboards were the entry point for the MCP-config
|
||||
# persistence campaign). It is accepted but ignored; warn if set so operators
|
||||
# migrate to a real provider.
|
||||
case "${HERMES_DASHBOARD_INSECURE:-}" in
|
||||
1|true|TRUE|True|yes|YES|Yes)
|
||||
echo "[dashboard] HERMES_DASHBOARD_INSECURE no longer disables the auth gate." >&2
|
||||
echo "[dashboard] A non-loopback dashboard requires an auth provider:" >&2
|
||||
echo "[dashboard] set HERMES_DASHBOARD_BASIC_AUTH_USERNAME + _PASSWORD (password)" >&2
|
||||
echo "[dashboard] or HERMES_DASHBOARD_OAUTH_CLIENT_ID (OAuth)." >&2
|
||||
;;
|
||||
esac
|
||||
|
||||
# Skip the drop when already non-root.
|
||||
[ "$(id -u)" = 0 ] || exec hermes dashboard --host "$dash_host" --port "$dash_port" --no-open
|
||||
exec s6-setuidgid hermes hermes dashboard \
|
||||
--host "$dash_host" --port "$dash_port" --no-open
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/command/with-contenv sh
|
||||
# shellcheck shell=sh
|
||||
# Main hermes service.
|
||||
#
|
||||
# IMPORTANT — this is NOT how the user's CMD runs.
|
||||
#
|
||||
# We chose Architecture B from the plan: the container's CMD (the bare
|
||||
# command the user passes to `docker run <image> …`) runs as /init's
|
||||
# "main program" via Docker's CMD mechanism, NOT as an s6-supervised
|
||||
# service. This is the canonical s6-overlay pattern for "container
|
||||
# exits when the program exits" semantics, and it lets us preserve
|
||||
# every pre-s6 invocation contract (chat passthrough, sleep infinity,
|
||||
# bash, --tui) without re-implementing argument routing through
|
||||
# /run/s6/container_environment.
|
||||
#
|
||||
# So why does this service exist at all? Two reasons:
|
||||
# 1. s6-rc requires at least one user service for the "user" bundle
|
||||
# to be valid. We can't ship an empty bundle.
|
||||
# 2. Future work may want to supervise a long-lived hermes process
|
||||
# (e.g. for gateway-server containers); having the slot already
|
||||
# wired in keeps that change small.
|
||||
#
|
||||
# For now this service is a no-op: it sleeps forever, doing nothing.
|
||||
# The dashboard runs as a real s6 service alongside it (see
|
||||
# ../dashboard/run) and per-profile gateways register dynamically via
|
||||
# /run/service/ at runtime (Phase 4).
|
||||
exec sleep infinity
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
Executable
+697
@@ -0,0 +1,697 @@
|
||||
#!/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"
|
||||
|
||||
# Drop to hermes via s6-setuidgid, but skip it when already non-root.
|
||||
as_hermes() { [ "$(id -u)" = 0 ] || { "$@"; return; }; s6-setuidgid hermes "$@"; }
|
||||
|
||||
# --- Reject the unsupported `docker run --user <uid>:<gid>` start ---
|
||||
# Detect the case where the container was launched with `--user` pinned to an
|
||||
# arbitrary host UID (the classic `--user $(id -u):$(id -g)` invocation people
|
||||
# used in the tini era to make container-written files match their host user).
|
||||
#
|
||||
# Under s6-overlay this no longer works: the bootstrap (UID remap, data-volume
|
||||
# ownership, config seeding) requires root, and it is skipped when the container
|
||||
# starts non-root. The baked install tree under /opt/hermes is intentionally
|
||||
# root-owned and non-writable; mutable runtime state must live under
|
||||
# $HERMES_HOME. An arbitrary `--user` UID therefore cannot repair or populate
|
||||
# the data volume, and startup fails with EACCES. See #34837 for the
|
||||
# supervision-tree side of this.
|
||||
#
|
||||
# The supported way to match host-side ownership is to start as root (the image
|
||||
# default) and pass HERMES_UID/HERMES_GID — or the PUID/PGID aliases — which the
|
||||
# remap block below consumes via usermod/groupmod + targeted chown. That gives
|
||||
# the exact same outcome (files owned by your host UID) without breaking s6.
|
||||
#
|
||||
# preinit runs setuid-root (euid=0) but cont-init.d hooks run with the real UID
|
||||
# the container was started as, so `id -u` here is the host UID (e.g. 1000), and
|
||||
# `id -u hermes` is the unremapped build UID (10000) because no root-only remap
|
||||
# could run. root starts (id -u = 0) and the normal supervised drop to the
|
||||
# hermes UID are both unaffected.
|
||||
cur_uid="$(id -u)"
|
||||
if [ "$cur_uid" != 0 ] && [ "$cur_uid" != "$(id -u hermes)" ]; then
|
||||
cat >&2 <<EOF
|
||||
[stage2] ERROR: container started with --user $cur_uid (an arbitrary, non-hermes UID).
|
||||
|
||||
This is not supported under the s6-overlay image. The container bootstrap
|
||||
(UID remap, data-volume ownership, config seeding) needs to start as root,
|
||||
and the baked /opt/hermes install tree is intentionally root-owned and
|
||||
non-writable, so a pinned --user UID cannot repair startup state — startup
|
||||
will fail.
|
||||
|
||||
To make container-written files match your HOST user, DON'T use --user.
|
||||
Start the container as root (the default) and pass your host UID/GID instead:
|
||||
|
||||
docker run -e HERMES_UID=\$(id -u) -e HERMES_GID=\$(id -g) ...
|
||||
|
||||
NAS users (Synology / unRAID / UGOS) can use the PUID/PGID aliases:
|
||||
|
||||
docker run -e PUID=\$(id -u) -e PGID=\$(id -g) ...
|
||||
|
||||
The image remaps the hermes user to that UID/GID at boot and chowns the data
|
||||
volume accordingly, so files land owned by your host user — the same outcome
|
||||
--user was being used for, without breaking the supervision tree.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 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"
|
||||
|
||||
# Numeric UID/GID validation: must be digits only, non-root, 1-65534.
|
||||
# NAS hosts such as Unraid commonly use low non-root IDs (99:100).
|
||||
validate_uid_gid() {
|
||||
case "$1" in
|
||||
''|*[!0-9]*) return 1 ;;
|
||||
*) [ "$1" -ge 1 ] && [ "$1" -le 65534 ] ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- 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:-}" ] && validate_uid_gid "$HERMES_UID" && [ "$HERMES_UID" != "$(id -u hermes)" ]; then
|
||||
echo "[stage2] Changing hermes UID to $HERMES_UID"
|
||||
usermod -u "$HERMES_UID" hermes
|
||||
fi
|
||||
if [ -n "${HERMES_GID:-}" ] && validate_uid_gid "$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)
|
||||
|
||||
path_has_symlink_component() {
|
||||
path="$1"
|
||||
root="${2:-$HERMES_HOME}"
|
||||
while [ -n "$path" ] && [ "$path" != "/" ]; do
|
||||
if [ -L "$path" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$path" = "$root" ]; then
|
||||
break
|
||||
fi
|
||||
parent="$(dirname "$path")"
|
||||
if [ "$parent" = "$path" ]; then
|
||||
break
|
||||
fi
|
||||
path="$parent"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
refuse_symlinked_path() {
|
||||
action="$1"
|
||||
target="$2"
|
||||
if path_has_symlink_component "$target"; then
|
||||
echo "[stage2] Warning: refusing $action through symlinked path $target — continuing"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
chown_hermes_tree() {
|
||||
target="$1"
|
||||
if refuse_symlinked_path "recursive chown" "$target"; then
|
||||
return 0
|
||||
fi
|
||||
chown -R hermes:hermes "$target" 2>/dev/null || \
|
||||
echo "[stage2] Warning: chown $target failed (rootless container?) — continuing"
|
||||
}
|
||||
|
||||
tree_has_non_hermes_owner() {
|
||||
target="$1"
|
||||
find "$target" \( ! -user hermes -o ! -group hermes \) -print -quit 2>/dev/null | grep -q .
|
||||
}
|
||||
|
||||
needs_chown=false
|
||||
if [ "$(stat -c %u "$HERMES_HOME" 2>/dev/null)" != "$actual_hermes_uid" ]; then
|
||||
needs_chown=true
|
||||
fi
|
||||
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.
|
||||
if refuse_symlinked_path "chown" "$HERMES_HOME"; then
|
||||
:
|
||||
else
|
||||
chown hermes:hermes "$HERMES_HOME" 2>/dev/null || \
|
||||
echo "[stage2] Warning: chown $HERMES_HOME failed (rootless container?) — continuing"
|
||||
fi
|
||||
# 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 pairing platforms/pairing lazy-packages; do
|
||||
if [ -e "$HERMES_HOME/$sub" ] && tree_has_non_hermes_owner "$HERMES_HOME/$sub"; then
|
||||
chown_hermes_tree "$HERMES_HOME/$sub"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# --- Immutable install tree ---
|
||||
# Do not chown runtime code or dependency trees under $INSTALL_DIR back to the
|
||||
# hermes user. Hosted/container instances keep mutable state under
|
||||
# $HERMES_HOME (/opt/data) and run with PYTHONDONTWRITEBYTECODE plus
|
||||
# HERMES_DISABLE_LAZY_INSTALLS=1. Keeping /opt/hermes root-owned and
|
||||
# non-writable prevents an agent session from self-modifying the installed
|
||||
# source, venv, TUI bundle, or node_modules and bricking the gateway.
|
||||
#
|
||||
# Lazy-installable optional backends (Firecrawl, Exa, Feishu, etc.) cannot
|
||||
# install into the sealed venv, so they are redirected to the writable
|
||||
# $HERMES_HOME/lazy-packages dir on the data volume (Dockerfile sets
|
||||
# HERMES_LAZY_INSTALL_TARGET). That dir is appended to the END of sys.path,
|
||||
# so a package installed there can only ADD modules — it can never shadow or
|
||||
# break a core module, which is what keeps the sealed-venv guarantee intact
|
||||
# even though installs are re-enabled. The dir is seeded + chowned to hermes
|
||||
# in the mkdir/chown blocks above so first-use installs succeed as the
|
||||
# unprivileged runtime user, and it persists across container recreates /
|
||||
# image updates (an ABI stamp wipes it if a rebuild bumps the interpreter).
|
||||
|
||||
# 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. Skip the recursive walk when the tree is already
|
||||
# owned correctly so warm boots do not rescan huge profile caches.
|
||||
# Idempotent; skipped on rootless containers where chown would fail.
|
||||
if [ -d "$HERMES_HOME/profiles" ] && tree_has_non_hermes_owner "$HERMES_HOME/profiles"; then
|
||||
chown_hermes_tree "$HERMES_HOME/profiles"
|
||||
fi
|
||||
|
||||
# Always reset ownership of $HERMES_HOME/cron on every boot for the same
|
||||
# docker-exec/root-write reason as profiles/. The cron scheduler state
|
||||
# (jobs.json) must stay readable by the unprivileged hermes runtime even
|
||||
# after root-context maintenance commands or scheduler writes. Skip the
|
||||
# recursive walk when the tree is already owned correctly (same warm-boot
|
||||
# gate as profiles/).
|
||||
if [ -d "$HERMES_HOME/cron" ] && tree_has_non_hermes_owner "$HERMES_HOME/cron"; then
|
||||
chown_hermes_tree "$HERMES_HOME/cron"
|
||||
fi
|
||||
|
||||
# Always ensure logs/gateways is hermes-owned (#45258). Formerly healed by
|
||||
# restartable gateway log/run chown — removed due to symlink TOCTOU
|
||||
# (CWE-59/367). The targeted data-volume chown above only runs when the
|
||||
# top-level $HERMES_HOME is mis-owned, so a warm volume with hermes-owned
|
||||
# HERMES_HOME but root-owned logs/gateways would otherwise leave
|
||||
# s6-setuidgid hermes mkdir failing with Permission denied. Non-recursive:
|
||||
# profile leaf dirs are each created/owned by their own log/run as hermes.
|
||||
if [ -d "$HERMES_HOME/logs/gateways" ]; then
|
||||
if refuse_symlinked_path "chown" "$HERMES_HOME/logs/gateways"; then
|
||||
:
|
||||
else
|
||||
chown hermes:hermes "$HERMES_HOME/logs/gateways" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Always reset ownership of pairing data on every boot, same docker-exec/
|
||||
# root-write reason as profiles/ and cron/. `docker exec <container>
|
||||
# hermes pairing approve …` defaults to uid=0 and writes 0600 root-owned
|
||||
# approval files that the unprivileged hermes gateway cannot read,
|
||||
# silently leaving the approved user unauthorized (#10270). The targeted
|
||||
# data-volume chown above only runs when the top-level $HERMES_HOME is
|
||||
# mis-owned, so warm boots skip it — this block makes a container restart
|
||||
# self-heal. Tiny directory (a handful of small JSON files), so even the
|
||||
# ownership pre-scan is negligible; gated for consistency with profiles/
|
||||
# and cron/.
|
||||
if [ -d "$HERMES_HOME/platforms/pairing" ] && tree_has_non_hermes_owner "$HERMES_HOME/platforms/pairing"; then
|
||||
chown_hermes_tree "$HERMES_HOME/platforms/pairing"
|
||||
fi
|
||||
# Legacy location (pre-consolidated layout).
|
||||
if [ -d "$HERMES_HOME/pairing" ] && tree_has_non_hermes_owner "$HERMES_HOME/pairing"; then
|
||||
chown_hermes_tree "$HERMES_HOME/pairing"
|
||||
fi
|
||||
|
||||
# Reset ownership of hermes-owned top-level state files on every boot.
|
||||
# The targeted data-volume chown above only covers hermes-owned
|
||||
# *subdirectories*; loose state files living directly under $HERMES_HOME
|
||||
# are missed. When those files are created or rewritten by
|
||||
# `docker exec <container> hermes …` (root unless `-u` is passed) they
|
||||
# land root-owned, and the unprivileged hermes runtime then hits
|
||||
# PermissionError on next startup (e.g. gateway.lock / state.db /
|
||||
# auth.json), producing a gateway restart loop.
|
||||
#
|
||||
# We use an explicit allowlist rather than a blanket `find -user root`
|
||||
# sweep so host-owned files in a bind-mounted $HERMES_HOME are never
|
||||
# touched — same targeted-ownership contract as the subdir chown above
|
||||
# (issue #19788, PR #19795). The list mirrors the top-level *file*
|
||||
# entries of hermes_cli.profile_distribution.USER_OWNED_EXCLUDE plus the
|
||||
# runtime lock files; keep them in sync if that set changes.
|
||||
for f in \
|
||||
auth.json auth.lock .env \
|
||||
state.db state.db-shm state.db-wal \
|
||||
hermes_state.db \
|
||||
response_store.db response_store.db-shm response_store.db-wal \
|
||||
gateway.pid gateway.lock gateway_state.json processes.json \
|
||||
active_profile; do
|
||||
if [ -e "$HERMES_HOME/$f" ]; then
|
||||
if refuse_symlinked_path "chown" "$HERMES_HOME/$f"; then
|
||||
:
|
||||
else
|
||||
chown hermes:hermes "$HERMES_HOME/$f" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# --- 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
|
||||
if refuse_symlinked_path "chown/chmod" "$HERMES_HOME/config.yaml"; then
|
||||
:
|
||||
else
|
||||
chown hermes:hermes "$HERMES_HOME/config.yaml" 2>/dev/null || true
|
||||
chmod 640 "$HERMES_HOME/config.yaml" 2>/dev/null || true
|
||||
fi
|
||||
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.
|
||||
as_hermes mkdir -p \
|
||||
"$HERMES_HOME/backups" \
|
||||
"$HERMES_HOME/cron" \
|
||||
"$HERMES_HOME/sessions" \
|
||||
"$HERMES_HOME/logs" \
|
||||
"$HERMES_HOME/logs/gateways" \
|
||||
"$HERMES_HOME/hooks" \
|
||||
"$HERMES_HOME/memories" \
|
||||
"$HERMES_HOME/skills" \
|
||||
"$HERMES_HOME/skins" \
|
||||
"$HERMES_HOME/plans" \
|
||||
"$HERMES_HOME/workspace" \
|
||||
"$HERMES_HOME/home" \
|
||||
"$HERMES_HOME/pairing" \
|
||||
"$HERMES_HOME/platforms/pairing" \
|
||||
"$HERMES_HOME/lazy-packages"
|
||||
|
||||
# --- Install-method stamp ---
|
||||
# The 'docker' stamp is baked into the immutable install tree at
|
||||
# /opt/hermes/.install_method (see Dockerfile), NOT written here into
|
||||
# $HERMES_HOME. detect_install_method() reads the code-scoped stamp first.
|
||||
#
|
||||
# Why we no longer stamp $HERMES_HOME: it is a shared DATA volume, commonly
|
||||
# bind-mounted from the host (~/.hermes:/opt/data) and sometimes shared with a
|
||||
# host-side Desktop/CLI install. Stamping 'docker' here clobbered that host
|
||||
# install's marker, so its in-app updater read 'docker' and refused to run
|
||||
# 'hermes update'. To heal homes already poisoned by older images, remove a
|
||||
# stale 'docker' stamp from $HERMES_HOME if one is present (the host install's
|
||||
# own installer re-creates its code-scoped stamp; a genuine container relies on
|
||||
# the baked /opt/hermes stamp, so deleting the data-dir copy is safe).
|
||||
if [ -f "$HERMES_HOME/.install_method" ]; then
|
||||
stamped="$(tr -d '[:space:]' < "$HERMES_HOME/.install_method" 2>/dev/null || true)"
|
||||
if [ "$stamped" = "docker" ]; then
|
||||
rm -f "$HERMES_HOME/.install_method" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Seed config files (only on first boot) ---
|
||||
seed_one() {
|
||||
dest=$1
|
||||
src=$2
|
||||
if [ ! -f "$HERMES_HOME/$dest" ] && [ -f "$INSTALL_DIR/$src" ]; then
|
||||
if refuse_symlinked_path "seed" "$HERMES_HOME/$dest"; then
|
||||
:
|
||||
else
|
||||
as_hermes cp "$INSTALL_DIR/$src" "$HERMES_HOME/$dest"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
seed_one ".env" ".env.example"
|
||||
seed_one "config.yaml" "cli-config.yaml.example"
|
||||
seed_one "SOUL.md" "docker/SOUL.md"
|
||||
|
||||
# --- Ensure a gateway api_server key exists (loopback control plane) ---
|
||||
# The gateway's aiohttp api_server refuses to start without a strong
|
||||
# API_SERVER_KEY (>=16 chars; startup guard in gateway/platforms/api_server.py).
|
||||
# Hosted deployments need that listener on loopback so the dashboard — the
|
||||
# container's only public HTTP door — can forward Chronos cron fires into the
|
||||
# GATEWAY process, where the live platform adapters (relay, E2EE) live. The
|
||||
# cron-fire route itself is NAS-JWT-authed, not key-authed; the key gates the
|
||||
# rest of the api_server surface. Generate once, persist in .env (mounted
|
||||
# volume), never overwrite an operator-provided value. Loopback-only: the
|
||||
# default bind host is 127.0.0.1 and the Fly service only exposes the
|
||||
# dashboard's port, so this listener is never publicly reachable.
|
||||
#
|
||||
# CREATE .env when it is missing rather than requiring it to exist (OOF-285):
|
||||
# the first-boot seed above depends on /opt/hermes/.env.example being present
|
||||
# in the image, and when it isn't (the .dockerignore excluded it for a long
|
||||
# stretch of releases) seed_one is a silent no-op, no .env ever exists, this
|
||||
# keygen never ran, the api_server never started, and every scheduled cron
|
||||
# fire on the instance was silently lost. The key must not depend on the
|
||||
# example-file seed having worked.
|
||||
#
|
||||
# OPERATOR-PROVIDED KEYS WIN: if the container environment already carries
|
||||
# API_SERVER_KEY (documented `docker run -e API_SERVER_KEY=...` flow), do
|
||||
# not generate one. Hermes loads $HERMES_HOME/.env with override=True, so
|
||||
# a generated key written here would silently SHADOW the operator's env
|
||||
# key and 401 every client still using the supplied credential.
|
||||
if [ -n "${API_SERVER_KEY:-}" ]; then
|
||||
if [ -f "$HERMES_HOME/.env" ] && grep -q '^API_SERVER_KEY=..*' "$HERMES_HOME/.env" 2>/dev/null; then
|
||||
echo "[stage2] Warning: API_SERVER_KEY is set in both the container environment and $HERMES_HOME/.env — the .env value wins at runtime (loaded with override=True)"
|
||||
else
|
||||
# The env key is the effective key on this boot (no .env key wins
|
||||
# over it). The api_server startup guard refuses keys shorter than
|
||||
# 16 chars; since this branch skips generation, a weak operator key
|
||||
# means the server stays DOWN (cron fires lost), not just 401s.
|
||||
# Warn where the operator will look — the boot log. Checked only in
|
||||
# this branch: when a strong .env key wins at runtime, the warning
|
||||
# would be false (the server does start).
|
||||
if [ "${#API_SERVER_KEY}" -lt 16 ]; then
|
||||
echo "[stage2] Warning: container-provided API_SERVER_KEY is shorter than 16 characters — the gateway api_server will refuse to start (cron fires unavailable). Generate a strong secret, e.g. \`openssl rand -hex 32\`."
|
||||
fi
|
||||
# A stale empty `API_SERVER_KEY=` line (left by an old seed) would
|
||||
# clobber the container-provided key at runtime: .env is loaded with
|
||||
# override=True and python-dotenv sets the empty string, which fails
|
||||
# the api_server's startup guard — the exact silent-cron-loss symptom
|
||||
# this hook exists to prevent. Drop it so the operator key wins.
|
||||
if [ -f "$HERMES_HOME/.env" ] && ! refuse_symlinked_path "clean" "$HERMES_HOME/.env"; then
|
||||
sed -i '/^API_SERVER_KEY=$/d' "$HERMES_HOME/.env" 2>/dev/null || true
|
||||
fi
|
||||
echo "[stage2] API_SERVER_KEY provided via container environment — skipping generation"
|
||||
fi
|
||||
elif ! grep -q '^API_SERVER_KEY=..*' "$HERMES_HOME/.env" 2>/dev/null; then
|
||||
if refuse_symlinked_path "append" "$HERMES_HOME/.env"; then
|
||||
:
|
||||
else
|
||||
if [ ! -f "$HERMES_HOME/.env" ]; then
|
||||
# Create an empty, owner-only .env so the append below (and any
|
||||
# later runtime save_env_value writes) have a durable target.
|
||||
# Created under a restrictive umask so the file is 0600 from the
|
||||
# first instant — no touch→chmod window, and no dependence on a
|
||||
# silenced chmod succeeding. The chown/chmod block below still
|
||||
# re-tightens perms every boot.
|
||||
(umask 077 && as_hermes touch "$HERMES_HOME/.env") 2>/dev/null || true
|
||||
fi
|
||||
if [ -f "$HERMES_HOME/.env" ]; then
|
||||
_gen_key=$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
||||
if [ -n "$_gen_key" ]; then
|
||||
# Drop an empty assignment line if the seed left one behind,
|
||||
# then append the generated key. The append is guarded: on a
|
||||
# read-only volume / full disk it must degrade to the warning
|
||||
# below, not abort the whole stage2 hook under `set -e`.
|
||||
sed -i '/^API_SERVER_KEY=$/d' "$HERMES_HOME/.env" 2>/dev/null || true
|
||||
if printf 'API_SERVER_KEY=%s\n' "$_gen_key" >> "$HERMES_HOME/.env" 2>/dev/null; then
|
||||
echo "[stage2] Generated API_SERVER_KEY for the loopback gateway api_server"
|
||||
else
|
||||
echo "[stage2] Warning: could not write API_SERVER_KEY to $HERMES_HOME/.env (read-only volume?) — gateway api_server (cron fires) will be unavailable"
|
||||
fi
|
||||
fi
|
||||
unset _gen_key
|
||||
else
|
||||
echo "[stage2] Warning: could not create $HERMES_HOME/.env — gateway api_server (cron fires) will be unavailable"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# .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
|
||||
if refuse_symlinked_path "chown/chmod" "$HERMES_HOME/.env"; then
|
||||
:
|
||||
else
|
||||
chown hermes:hermes "$HERMES_HOME/.env" 2>/dev/null || true
|
||||
chmod 600 "$HERMES_HOME/.env" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Grant the gateway access to the Fly Machines API socket (scale-to-zero) ---
|
||||
# On Fly, flyd mounts the local Machines API ("flaps") unix socket at /.fly/api
|
||||
# owned root:root 0755. The gateway's scale-to-zero self-suspend
|
||||
# (gateway/scale_to_zero.py suspend_self) must POST to it, but the gateway runs
|
||||
# as the unprivileged `hermes` user — without this it gets EACCES on every
|
||||
# suspend attempt and the machine can never sleep (fail-awake; verified live on
|
||||
# staging 2026-08-20: "flaps suspend request failed: [Errno 13]"). This hook
|
||||
# runs as root before user services (the gateway) start, so grant group access
|
||||
# here. Scope note: group-write exposes the WHOLE local Machines API to the
|
||||
# hermes group (any group member could e.g. stop/suspend this machine), not
|
||||
# just the suspend endpoint — accepted because the agent already executes
|
||||
# arbitrary user code as that same principal and the socket only controls THIS
|
||||
# machine. No-op off Fly (socket absent).
|
||||
if [ -S /.fly/api ]; then
|
||||
if refuse_symlinked_path "chgrp/chmod" /.fly/api; then
|
||||
:
|
||||
elif chgrp hermes /.fly/api 2>/dev/null && chmod g+w /.fly/api 2>/dev/null; then
|
||||
echo "[stage2] Granted hermes group access to the Fly Machines API socket"
|
||||
else
|
||||
echo "[stage2] Warning: could not grant group access to /.fly/api — scale-to-zero self-suspend will fail EACCES (fail-awake)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Migrate persisted config schema ---
|
||||
# Docker image upgrades replace the code under $INSTALL_DIR but preserve
|
||||
# $HERMES_HOME on the mounted volume. Run the same safe, non-interactive
|
||||
# config-schema migrations that `hermes update` runs for non-Docker installs,
|
||||
# after first-boot seeding and before supervised gateway services start.
|
||||
# Set HERMES_SKIP_CONFIG_MIGRATION=1 for controlled/manual migrations.
|
||||
if [ -f "$HERMES_HOME/config.yaml" ]; then
|
||||
s6-setuidgid hermes "$INSTALL_DIR/.venv/bin/python" "$INSTALL_DIR/scripts/docker_config_migrate.py" \
|
||||
|| echo "[stage2] Warning: docker_config_migrate.py failed; continuing"
|
||||
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
|
||||
if refuse_symlinked_path "seed" "$HERMES_HOME/auth.json"; then
|
||||
:
|
||||
else
|
||||
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
|
||||
fi
|
||||
|
||||
# auth.json: re-seed a TERMINALLY-DEAD Nous bootstrap session (self-heal).
|
||||
#
|
||||
# The [ ! -f ] guard above deliberately refuses to clobber an existing
|
||||
# auth.json, so a container whose Nous bootstrap session took a terminal
|
||||
# invalid_grant (tokens cleared, providers.nous.last_auth_error.relogin_required
|
||||
# stamped) can NOT recover from a plain restart — it stays unauthenticated until
|
||||
# the credential is replaced. An orchestrator that manages the container can
|
||||
# supply a freshly-issued session via HERMES_AUTH_JSON_REBOOTSTRAP (distinct
|
||||
# from the create-only *_BOOTSTRAP var); this helper swaps ONLY the
|
||||
# providers.nous entry when the on-disk entry is provably terminal OR the
|
||||
# orchestrator seed has a later obtained_at timestamp. The latter covers the
|
||||
# stop/update/start sequence where NAS already revoked the still-healthy-looking
|
||||
# local session. Older/incomparable seeds remain no-ops, so leaving the env set
|
||||
# cannot roll a healthy rotated token backward. Runs as its own stdlib-only
|
||||
# subprocess (no app imports) and always exits 0.
|
||||
if [ -f "$HERMES_HOME/auth.json" ] && [ -n "${HERMES_AUTH_JSON_REBOOTSTRAP:-}" ]; then
|
||||
if refuse_symlinked_path "reseed" "$HERMES_HOME/auth.json"; then
|
||||
:
|
||||
else
|
||||
s6-setuidgid hermes "$INSTALL_DIR/.venv/bin/python" \
|
||||
"$INSTALL_DIR/scripts/docker_rebootstrap_nous_session.py" \
|
||||
"$HERMES_HOME/auth.json" \
|
||||
|| echo "[stage2] Warning: docker_rebootstrap_nous_session.py failed; continuing"
|
||||
fi
|
||||
fi
|
||||
|
||||
# gateway_state.json: declare the gateway's INITIAL supervised state on a
|
||||
# fresh volume. Same first-boot-only env-seed pattern as auth.json above.
|
||||
#
|
||||
# On a blank volume there is no gateway_state.json, so the boot reconciler
|
||||
# (cont-init.d/02-reconcile-profiles → container_boot.reconcile_profile_gateways)
|
||||
# registers the gateway-default s6 slot but leaves it DOWN — it only
|
||||
# auto-starts when the last recorded state was "running". That means a
|
||||
# freshly-provisioned container comes up with the gateway down until
|
||||
# someone starts it (e.g. from the dashboard). An orchestrator that
|
||||
# provisions a fresh volume and wants the gateway running from first boot
|
||||
# can set HERMES_GATEWAY_BOOTSTRAP_STATE=running; we seed the state file
|
||||
# here, BEFORE 02-reconcile-profiles runs (cont-init.d scripts run in
|
||||
# lexicographic order), so the reconciler sees prior_state=running and
|
||||
# brings the supervised slot up on the very first boot.
|
||||
#
|
||||
# This is a generic container contract, not specific to any host: it seeds
|
||||
# the SAME gateway_state.json the reconciler already consults, exactly as
|
||||
# HERMES_AUTH_JSON_BOOTSTRAP seeds auth.json. The [ ! -f ] guard is the
|
||||
# load-bearing part — on every subsequent boot the persisted state wins,
|
||||
# so a gateway the operator deliberately stopped stays stopped across
|
||||
# restarts and we never clobber real runtime state.
|
||||
#
|
||||
# Only a literal "running" is honoured (the sole value in the reconciler's
|
||||
# _AUTOSTART_STATES); any other value is ignored so a typo can't write a
|
||||
# bogus state the reconciler would treat as "no prior state" anyway.
|
||||
if [ ! -f "$HERMES_HOME/gateway_state.json" ] && \
|
||||
[ "${HERMES_GATEWAY_BOOTSTRAP_STATE:-}" = "running" ]; then
|
||||
if refuse_symlinked_path "seed" "$HERMES_HOME/gateway_state.json"; then
|
||||
:
|
||||
else
|
||||
printf '{"gateway_state":"running"}\n' > "$HERMES_HOME/gateway_state.json"
|
||||
chown hermes:hermes "$HERMES_HOME/gateway_state.json" 2>/dev/null || true
|
||||
chmod 644 "$HERMES_HOME/gateway_state.json"
|
||||
fi
|
||||
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
|
||||
as_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 / 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 '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.
|
||||
# Some container runtimes / s6-overlay versions do not create the
|
||||
# envdir before cont-init hooks run, so create it defensively.
|
||||
mkdir -p /run/s6/container_environment
|
||||
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"
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=sh
|
||||
# /usr/bin/tini — compatibility shim for legacy orchestration templates.
|
||||
#
|
||||
# Background
|
||||
# ----------
|
||||
# The published image used to ship real `tini` as PID 1. After the
|
||||
# s6-overlay migration, PID 1 is `/init`. Downstream catalogs (Hostinger
|
||||
# Hermes WebUI, NAS "update" flows that preserve an old entrypoint, etc.)
|
||||
# still pin entrypoints like:
|
||||
#
|
||||
# ["/usr/bin/tini", "-g", "--"]
|
||||
# ["/usr/bin/tini", "-g", "--", "gateway", "run"]
|
||||
#
|
||||
# A plain `ln -sf /init /usr/bin/tini` (#34192) made the binary exist, but
|
||||
# forwarded tini's own flags into s6-overlay. `/init` then handed `-g` to
|
||||
# `rc.init` as the container CMD, which fails with:
|
||||
#
|
||||
# /run/s6/basedir/scripts/rc.init: 91: -g: not found
|
||||
#
|
||||
# …and with `restart: unless-stopped` the container boot-loops forever
|
||||
# (#66679).
|
||||
#
|
||||
# This shim strips the tini CLI surface, then exec's `/init` with the
|
||||
# image's main-wrapper so privilege drop + arg routing still apply.
|
||||
#
|
||||
# Tini flags we discard (see krallin/tini --help):
|
||||
# -g / --kill-process-group, -s / --subreaper, -w, -v (repeatable),
|
||||
# -h / --help, -l, --version, -p SIGNAL, -e EXIT_CODE, and `--`.
|
||||
# s6-overlay already reaps zombies and forwards signals as PID 1; the
|
||||
# flag semantics are intentionally no-ops here.
|
||||
#
|
||||
# Test hook: HERMES_TINI_SHIM_TARGET overrides the `/init` path (and
|
||||
# HERMES_TINI_SHIM_WRAPPER the main-wrapper path) so unit tests can
|
||||
# record argv without a real s6 tree.
|
||||
|
||||
set -e
|
||||
|
||||
INIT_TARGET="${HERMES_TINI_SHIM_TARGET:-/init}"
|
||||
WRAPPER="${HERMES_TINI_SHIM_WRAPPER:-/opt/hermes/docker/main-wrapper.sh}"
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-g|--kill-process-group|-s|--subreaper|-w|-l|-h|--help|--version)
|
||||
shift
|
||||
;;
|
||||
-v)
|
||||
# tini allows repeated -v; drop them all.
|
||||
shift
|
||||
;;
|
||||
-p|-e)
|
||||
# These take a mandatory argument.
|
||||
shift
|
||||
if [ "$#" -gt 0 ]; then
|
||||
shift
|
||||
fi
|
||||
;;
|
||||
-*)
|
||||
# Unknown short/long option from an older/newer tini — drop
|
||||
# rather than forwarding into /init (would recreate #66679).
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# No program left (entrypoint was just `tini -g --`) → same as the
|
||||
# image's default ENTRYPOINT + empty CMD.
|
||||
if [ "$#" -eq 0 ]; then
|
||||
exec "$INIT_TARGET" "$WRAPPER"
|
||||
fi
|
||||
|
||||
# Caller already supplied the wrapper or /init — don't double-wrap.
|
||||
case "$1" in
|
||||
/init|"$INIT_TARGET")
|
||||
exec "$@"
|
||||
;;
|
||||
"$WRAPPER"|*/main-wrapper.sh)
|
||||
exec "$INIT_TARGET" "$@"
|
||||
;;
|
||||
esac
|
||||
|
||||
exec "$INIT_TARGET" "$WRAPPER" "$@"
|
||||
Reference in New Issue
Block a user