mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-19 10:02:16 +00:00
The install method (docker/git/pip/...) describes the *running binary*, but
detect_install_method() read it from $HERMES_HOME/.install_method — a shared
DATA directory. The Docker docs deliberately bind-mount $HERMES_HOME
(~/.hermes:/opt/data) so config/sessions/memory persist and can be shared with
a host-side Desktop/CLI install.
When a containerized gateway and a host install share one $HERMES_HOME, the
home-scoped stamp is a single slot describing two installs: the published image
stamps 'docker' on every boot, the host install then reads 'docker' and the
in-app updater refuses to run 'hermes update' ("doesn't apply inside the Docker
container"). Reinstalling the Desktop app from the DMG doesn't help because the
contaminated stamp is re-read every time.
Fix (option 1 — code-scoped stamp):
- detect_install_method() reads <install tree>/.install_method first (next to
the running code, immune to the shared data dir). It falls back to the legacy
$HERMES_HOME stamp for back-compat, but IGNORES a 'docker' home stamp when
not actually containerized — so already-poisoned shared homes self-heal.
- stamp_install_method() writes the code-scoped stamp.
- install.sh stamps $INSTALL_DIR instead of $HERMES_HOME.
- Dockerfile bakes 'docker' into /opt/hermes/.install_method at build time
(inside the immutable block); stage2-hook.sh no longer writes the home stamp
and proactively removes a stale 'docker' one to heal existing shared homes.
Genuine containers still resolve to 'docker' (baked stamp, or legacy home stamp
honored when containerized). Unstamped installs in generic containers still fall
through to git/pip (preserves the #34397 fix).
86 lines
3 KiB
Python
86 lines
3 KiB
Python
"""Contract tests for the Docker image's immutable /opt/hermes install tree."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
DOCKERFILE = REPO_ROOT / "Dockerfile"
|
|
|
|
|
|
def _dockerfile_text() -> str:
|
|
return DOCKERFILE.read_text()
|
|
|
|
|
|
def test_dockerfile_makes_opt_hermes_root_owned_and_non_writable() -> None:
|
|
text = _dockerfile_text()
|
|
|
|
assert "COPY --chown=hermes:hermes . ." not in text
|
|
assert "COPY . ." in text
|
|
assert "chown -R root:root /opt/hermes" in text
|
|
assert "chmod -R a+rX /opt/hermes" in text
|
|
assert "chmod -R a-w /opt/hermes" in text
|
|
|
|
immutable_block = re.search(
|
|
r"RUN mkdir -p /opt/hermes/bin && \\\n"
|
|
r"(?:.*\\\n)+?"
|
|
r"\s+chmod -R a-w /opt/hermes",
|
|
text,
|
|
)
|
|
assert immutable_block, "Dockerfile must lock /opt/hermes after installing code/deps"
|
|
|
|
|
|
def test_dockerfile_keeps_mutable_state_under_opt_data() -> None:
|
|
text = _dockerfile_text()
|
|
|
|
assert "ENV HERMES_HOME=/opt/data" in text
|
|
assert "ENV HERMES_WRITE_SAFE_ROOT=/opt/data" in text
|
|
assert 'VOLUME [ "/opt/data" ]' in text
|
|
|
|
|
|
def test_dockerfile_disables_runtime_install_mutations() -> None:
|
|
text = _dockerfile_text()
|
|
|
|
assert "ENV PYTHONDONTWRITEBYTECODE=1" in text
|
|
assert "ENV HERMES_DISABLE_LAZY_INSTALLS=1" in text
|
|
assert "HERMES_TUI_DIR=/opt/hermes/ui-tui" in text
|
|
|
|
|
|
def test_dockerfile_does_not_chown_install_trees_to_hermes() -> None:
|
|
text = _dockerfile_text()
|
|
forbidden_patterns = (
|
|
r"chown\s+-R\s+hermes:hermes\s+/opt/hermes/\.venv",
|
|
r"chown\s+-R\s+hermes:hermes\s+/opt/hermes/ui-tui",
|
|
r"chown\s+-R\s+hermes:hermes\s+/opt/hermes/gateway",
|
|
r"chown\s+-R\s+hermes:hermes\s+/opt/hermes/node_modules",
|
|
)
|
|
for pattern in forbidden_patterns:
|
|
assert not re.search(pattern, text), (
|
|
"runtime install trees under /opt/hermes must stay immutable; "
|
|
f"found forbidden pattern {pattern!r}"
|
|
)
|
|
|
|
|
|
def test_dockerfile_bakes_code_scoped_install_method_stamp() -> None:
|
|
"""The 'docker' install-method stamp is baked next to the code.
|
|
|
|
detect_install_method() reads the code-scoped stamp
|
|
(/opt/hermes/.install_method) first; baking it at build time keeps the
|
|
published image self-identifying as 'docker' WITHOUT writing into the
|
|
shared $HERMES_HOME data volume (which a host install may also use).
|
|
It must live inside the immutable block so the runtime user can't alter it.
|
|
"""
|
|
text = _dockerfile_text()
|
|
assert "printf 'docker\\n' > /opt/hermes/.install_method" in text
|
|
|
|
immutable_block = re.search(
|
|
r"RUN mkdir -p /opt/hermes/bin && \\\n"
|
|
r"(?:.*\\\n)+?"
|
|
r"\s+chmod -R a-w /opt/hermes",
|
|
text,
|
|
)
|
|
assert immutable_block, "immutable block must exist"
|
|
assert ".install_method" in immutable_block.group(0), (
|
|
"the code-scoped install-method stamp must be baked inside the "
|
|
"immutable /opt/hermes block"
|
|
)
|