fix(docker): redirect lazy installs to a durable target so opt-in backends work in the immutable image (#51136)
The published Docker image seals the agent venv (root-owned, read-only /opt/hermes) and sets HERMES_DISABLE_LAZY_INSTALLS=1 so a runtime install can't mutate and brick the core. But opt-in backends (Firecrawl web search, Exa, Feishu, ...) deliberately keep their SDKs in tools/lazy_deps.py and out of [all] (pyproject policy 2026-05-12: one quarantined release must not break every install). The two policies collided: the SDK isn't baked in AND can't lazy-install, so the default Firecrawl web_search/web_extract fail out of the box in Docker (#51136), as do Exa (#49445) and Feishu (#50205). Fix the whole class instead of baking in one backend: when HERMES_LAZY_INSTALL_TARGET is set, lazy installs are redirected to a writable dir on the durable /opt/data volume via `pip/uv install --target`, and that dir is APPENDED to the end of sys.path. Because the core venv always wins name collisions, a package installed this way can only ADD new modules — it can never shadow, downgrade, or break a module the core ships. The worst a bad/incompatible backend package can do is fail to import and report itself unavailable; the agent core stays healthy. That structural guarantee is what made it safe to seal the venv, and it is preserved here even with installs re-enabled. - tools/lazy_deps.py: durable-target mode — `--target` install + core-pinned `--constraint` file (shared deps resolve to core's versions, conflicts fail loudly at install time), append-only sys.path activation, ABI/Python-version stamp that wipes the store if an image rebuild bumps the interpreter, and a reworked gate so HERMES_DISABLE_LAZY_INSTALLS=1 redirects (rather than hard- blocks) when a target is set. security.allow_lazy_installs=false still disables installs in every mode. - hermes_bootstrap.py: activate the durable target on sys.path at first import (before any backend imports its SDK) so packages installed on a previous run are importable on this run. - Dockerfile: set HERMES_LAZY_INSTALL_TARGET=/opt/data/lazy-packages. - docker/stage2-hook.sh: seed + chown the dir on the data volume. - tests: real-install E2E proving installs land in the target, import cleanly, don't leak into the sealed venv, and that a core package is never shadowed; ABI-stamp wipe/preserve; gate matrix; Dockerfile/stage2 contract test. Fixes #51136
This commit is contained in:
@@ -199,7 +199,7 @@ if [ "$needs_chown" = true ]; then
|
||||
# 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; do
|
||||
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" ]; then
|
||||
chown -R hermes:hermes "$HERMES_HOME/$sub" 2>/dev/null || \
|
||||
echo "[stage2] Warning: chown $HERMES_HOME/$sub failed (rootless container?) — continuing"
|
||||
@@ -214,6 +214,17 @@ fi
|
||||
# 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
|
||||
@@ -289,7 +300,8 @@ as_hermes mkdir -p \
|
||||
"$HERMES_HOME/workspace" \
|
||||
"$HERMES_HOME/home" \
|
||||
"$HERMES_HOME/pairing" \
|
||||
"$HERMES_HOME/platforms/pairing"
|
||||
"$HERMES_HOME/platforms/pairing" \
|
||||
"$HERMES_HOME/lazy-packages"
|
||||
|
||||
# --- Install-method stamp ---
|
||||
# The 'docker' stamp is baked into the immutable install tree at
|
||||
|
||||
Reference in New Issue
Block a user