fix(docker): stage agent source to writable build dir before pip install
The Docker smoke gate added in this same PR caught a real production regression on its very first CI run. v0.51.84 (PR #2470) mounted hermes-agent-src read-only on the WebUI side and widened the chown prune to keep the read-only walk happy, but missed that the WebUI's startup also runs: uv pip install "$_agent_src[all]" against the same now-read-only mount. setuptools' egg_info step writes hermes_agent.egg-info/ inside the source tree even under PEP 517 build isolation (this is by design -- PEP 517 isolates the BUILD environment, not the source tree's metadata directory). On a :ro mount this returns EROFS, the install fails, error_exit fires, and every multi-container deploy dies at startup. The smoke gate flagged it on both the two-container and three-container variants. The fix ------- Stage the agent source into a writable build dir under /tmp BEFORE invoking pip install, then point pip at the staged copy. _stage_src="/tmp/hermes-agent-build" rm -rf "$_stage_src" && mkdir -p "$_stage_src" rsync -a --exclude='*.egg-info' --exclude='build' --exclude='dist' \ --exclude='__pycache__' --exclude='.git' \ "$_agent_src"/ "$_stage_src"/ uv pip install "$_stage_src[all]" ... rm -rf "$_stage_src" The exclusion list matters: when setuptools sees a pre-baked *.egg-info, build, or dist directory, it takes a timestamp-update code path that also reads/writes inside that directory -- which itself fails on a :ro source. Excluding them keeps the build on the fresh-build path unconditionally. rsync is in the production image (Dockerfile line 41-44). For users running custom WebUI images without rsync, the script falls back to cp -a + post-copy rm -rf of the same artifacts. Tests ----- Two new source-level invariants in tests/test_docker_docs_and_readonly.py: test_docker_init_stages_agent_source_for_writable_install -- asserts _stage_src=... is declared -- asserts every `uv pip install ...[all]` line uses _stage_src, NOT raw $_agent_src test_docker_init_excludes_egg_info_during_staging -- asserts the staging path excludes *.egg-info (rsync exclude form or cp-fallback's explicit rm -rf both pass) These would have caught the v0.51.84 regression at the source level (once written; they're new). The Docker runtime smoke gate is the durable defence for the broader class of :ro x init-script interactions, since source-level invariants only catch what they're written to catch. Verification ------------ - pytest tests/test_docker_docs_and_readonly.py: 11 passed (9 existing + 2 new) - pytest tests/ -q --timeout=60: 5891 passed, 6 skipped (was 5889; delta is exactly the 2 new tests) - bash -n docker_init.bash: clean Once this lands, the Docker smoke gate's two/three-container variants should go green, completing the self-validating loop.
This commit is contained in:
1 parent
5b6f69c884
commit
70f371c8b9
3 files changed
+108
-2
No files matched your search
+34
-1
@@ -391,7 +391,40 @@ else
|
||||
fi
|
||||
done
|
||||
if [ -n "$_agent_src" ]; then
|
||||
uv pip install "$_agent_src[all]" --trusted-host pypi.org --trusted-host files.pythonhosted.org || error_exit "Failed to install hermes-agent's requirements"
|
||||
# The agent source can be mounted read-only (see docker-compose.two-container.yml
|
||||
# / docker-compose.three-container.yml — the WebUI only reads this volume to
|
||||
# install the agent's Python dependencies and never writes to it). setuptools'
|
||||
# `egg_info` build step, however, touches `hermes_agent.egg-info/` inside the
|
||||
# source tree even under PEP 517 build isolation, which `EROFS`-fails on a
|
||||
# `:ro` mount and (under `set -e`) kills startup of every multi-container
|
||||
# deploy. Stage the source into a writable tmpfs copy so the build can write
|
||||
# its metadata side-by-side without touching the underlying mount.
|
||||
#
|
||||
# The copy excludes any pre-baked `*.egg-info` / `build` / `dist` artifacts
|
||||
# to avoid the timestamp-update path setuptools takes when one is present,
|
||||
# and `--reflink=auto` makes the copy near-free on overlay2/btrfs where
|
||||
# supported. We rebuild on every container start (the agent source can
|
||||
# change across volume re-init); cost is one rsync of ~10MB of Python source.
|
||||
_stage_src="/tmp/hermes-agent-build"
|
||||
rm -rf "$_stage_src"
|
||||
mkdir -p "$_stage_src"
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
rsync -a \
|
||||
--exclude='*.egg-info' --exclude='build' --exclude='dist' \
|
||||
--exclude='__pycache__' --exclude='.git' \
|
||||
"$_agent_src"/ "$_stage_src"/ \
|
||||
|| error_exit "Failed to stage hermes-agent source to writable build dir"
|
||||
else
|
||||
# Fallback when rsync isn't in the image — straight cp -a, then drop
|
||||
# the build artifacts that would trip setuptools.
|
||||
cp -a "$_agent_src"/. "$_stage_src"/ \
|
||||
|| error_exit "Failed to copy hermes-agent source to writable build dir"
|
||||
rm -rf "$_stage_src"/*.egg-info "$_stage_src"/build "$_stage_src"/dist 2>/dev/null || true
|
||||
find "$_stage_src" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||
fi
|
||||
uv pip install "$_stage_src[all]" --trusted-host pypi.org --trusted-host files.pythonhosted.org \
|
||||
|| error_exit "Failed to install hermes-agent's requirements"
|
||||
rm -rf "$_stage_src"
|
||||
else
|
||||
echo ""
|
||||
echo "!! WARNING: hermes-agent source not found."
|
||||
|
||||
Reference in new issue
Block a user