@@ -9,8 +9,11 @@ FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df228
99FROM node:22-bookworm-slim@sha256:7af03b14a13c8cdd38e45058fd957bf00a72bbe17feac43b1c15a689c029c732 AS node_source
1010FROM debian:13.4
1111
12- # Disable Python stdout buffering to ensure logs are printed immediately
12+ # Disable Python stdout buffering to ensure logs are printed immediately.
13+ # Do not write .pyc files at runtime: /opt/hermes is immutable in the
14+ # published container and writable state belongs under /opt/data.
1315ENV PYTHONUNBUFFERED=1
16+ ENV PYTHONDONTWRITEBYTECODE=1
1417
1518# Store Playwright browsers outside the volume mount so the build-time
1619# install survives the /opt/data volume overlay at runtime.
@@ -186,36 +189,38 @@ RUN cd web && npm run build && \
186189
187190# ---------- Source code ----------
188191# .dockerignore excludes node_modules, so the installs above survive.
189- COPY --chown=hermes:hermes . .
192+ COPY . .
190193
191194# ---------- Permissions ----------
192- # Make install dir world-readable so any HERMES_UID can read it at runtime.
193- # The venv needs to be traversable too.
194- # node_modules trees additionally need to be writable by the hermes user
195- # so the runtime `npm install` triggered by _tui_need_npm_install() in
196- # hermes_cli/main.py succeeds (see #18800). /opt/hermes/web is build-time
197- # only (HERMES_WEB_DIST points at hermes_cli/web_dist) and is intentionally
198- # not chowned here.
199- # /opt/hermes/gateway is runtime-writable: Python may create __pycache__ and
200- # gateway state artifacts beneath the package after services drop privileges,
201- # especially when the hermes UID is remapped at boot (#27221).
202- # The .venv MUST remain hermes-writable so lazy_deps.py can install
203- # remaining optional platform packages and future pin bumps at first use.
204- # Without this, `uv pip install` fails with EACCES and adapters silently
205- # fail to load. See tools/lazy_deps.py.
195+ # Link hermes-agent itself (editable). Deps are already installed in the
196+ # cached layer above; `--no-deps` makes this a fast egg-link creation with no
197+ # resolution or downloads.
198+ RUN uv pip install --no-cache-dir --no-deps -e "."
199+
200+ # Keep /opt/hermes immutable for the runtime hermes user. Hosted/container
201+ # instances must not be able to self-edit the installed source or venv; user
202+ # data, skills, plugins, config, logs, and dashboard uploads live under
203+ # /opt/data instead. Root can still repair the image during build/boot, but
204+ # supervised Hermes processes drop to the non-root hermes user.
206205USER root
207- RUN chmod -R a+rX /opt/hermes && \
208- chown -R hermes:hermes /opt/hermes/.venv /opt/hermes/ui-tui /opt/hermes/gateway /opt/hermes/node_modules
206+ RUN mkdir -p /opt/hermes/bin && \
207+ cp /opt/hermes/docker/hermes-exec-shim.sh /opt/hermes/bin/hermes && \
208+ chmod 0755 /opt/hermes/bin/hermes && \
209+ printf 'docker\n ' > /opt/hermes/.install_method && \
210+ chown -R root:root /opt/hermes && \
211+ chmod -R a+rX /opt/hermes && \
212+ chmod -R a-w /opt/hermes
213+ # The ``.install_method`` stamp is baked next to the running code (the install
214+ # tree), NOT into $HERMES_HOME. $HERMES_HOME (/opt/data) is a shared data
215+ # volume that is commonly bind-mounted from the host and even shared with a
216+ # host-side Desktop/CLI install; stamping it at boot used to clobber that
217+ # host install's marker and wrongly block its ``hermes update``. A code-scoped
218+ # stamp is read first by detect_install_method() and is immune to the share.
209219# Start as root so the s6-overlay stage2 hook can usermod/groupmod and chown
210220# the data volume. Each supervised service then drops to the hermes user via
211221# `s6-setuidgid hermes` in its run script. If HERMES_UID is unset, services
212222# run as the default hermes user (UID 10000).
213223
214- # ---------- Link hermes-agent itself (editable) ----------
215- # Deps are already installed in the cached layer above; `--no-deps` makes
216- # this a fast (~1s) egg-link creation with no resolution or downloads.
217- RUN uv pip install --no-cache-dir --no-deps -e "."
218-
219224# ---------- Bake build-time git revision ----------
220225# .dockerignore excludes .git, so `git rev-parse HEAD` from inside the
221226# container always returns nothing — meaning `hermes dump` reports
@@ -235,8 +240,9 @@ RUN uv pip install --no-cache-dir --no-deps -e "."
235240# every published image has it.
236241ARG HERMES_GIT_SHA=
237242RUN if [ -n "${HERMES_GIT_SHA}" ]; then \
243+ chmod u+w /opt/hermes && \
238244 printf '%s\n ' "${HERMES_GIT_SHA}" > /opt/hermes/.hermes_build_sha && \
239- chown hermes: hermes /opt/hermes/.hermes_build_sha; \
245+ chmod a-w /opt/ hermes /opt/hermes/.hermes_build_sha; \
240246 fi
241247
242248# ---------- s6-overlay service wiring ----------
@@ -282,6 +288,8 @@ ENV HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist
282288# check. (A separate launcher hardening is tracked independently.)
283289ENV HERMES_TUI_DIR=/opt/hermes/ui-tui
284290ENV HERMES_HOME=/opt/data
291+ ENV HERMES_WRITE_SAFE_ROOT=/opt/data
292+ ENV HERMES_DISABLE_LAZY_INSTALLS=1
285293
286294# `docker exec` privilege-drop shim. When operators run
287295# `docker exec <c> hermes ...` they default to root, and any file the
@@ -294,7 +302,6 @@ ENV HERMES_HOME=/opt/data
294302# Recursion is impossible because the shim exec's the venv binary by
295303# absolute path (/opt/hermes/.venv/bin/hermes). See the shim source for
296304# the opt-out env var (HERMES_DOCKER_EXEC_AS_ROOT=1).
297- COPY --chmod=0755 docker/hermes-exec-shim.sh /opt/hermes/bin/hermes
298305
299306# Pre-s6 entrypoint.sh did `source .venv/bin/activate` which exported
300307# the venv bin onto PATH; Architecture B's main-wrapper.sh does the
0 commit comments