Docker environment detection, update handling, config loading
NousResearch/hermes-agent · cluster 2305 · agent
Data as of
5 open items. Fix merged in #2328. Check it covers your case.
The short answer
- Known?
- Yes. 32 related items grouped here, 8 issues already closed.
- Fix event
- A fix was merged to main in PR #2328. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PRs #2328, #35139, #37085, #109173. That is separate from the fix event. - Still open
- 3 issues and 2 PRs.
Next step: read #2328, then check whether your report is one of the 3 still open.
Read #2328Pick up a Lab taskThese items address overlapping concerns: correctly detecting Docker vs. non-Docker environments for update behavior, surfacing appropriate guidance instead of errors when updates are attempted inside containers, and loading platform/webhook configuration from config.yaml into the gateway and extra fields.
Open issues3
Open PRs2
Fixes on record
-
#2328 fix(gateway): load platforms section from config.yaml for webhook routes
files:
gateway/config.pyShow the diff
-
gateway/config.py@@ -455,11 +455,27 @@ def load_gateway_config() -> GatewayConfig: "pair", ) - # Bridge per-platform settings from config.yaml into gw_data + # Merge platforms section from config.yaml into gw_data so that + # nested keys like platforms.webhook.extra.routes are loaded. + yaml_platforms = yaml_cfg.get("platforms") platforms_data = gw_data.setdefault("platforms", {}) if not isinstance(platforms_data, dict): platforms_data = {} gw_data["platforms"] = platforms_data + if isinstance(yaml_platforms, dict): + for plat_name, plat_block in yaml_platforms.items(): + if not isinstance(plat_block, dict): + continue + existing = platforms_data.get(plat_name, {}) + if not isinstance(existing, dict): + existing = {} + # Deep-merge extra dicts so gateway.json defaults survive + merged_extra = {**existing.get("extra", {}), **plat_block.get("extra", {})} + merged = {**existing, **plat_block} + if merged_extra: + merged["extra"] = merged_extra + platforms_data[plat_name] = merged + gw_data["platforms"] = platforms_data for plat in Platform: if plat == Platform.LOCAL: continue
-
-
#35139 fix(cli): don't treat any container as the Docker image for updates
files:
hermes_cli/config.py,tests/hermes_cli/test_pip_install_detection.pyShow the diff
-
hermes_cli/config.py@@ -285,9 +285,22 @@ def detect_install_method(project_root: Optional[Path] = None) -> str: Resolution order: 1. Stamped ``~/.hermes/.install_method`` file (written by installers) 2. HERMES_MANAGED env / .managed marker (NixOS, Homebrew) - 3. Container detection (/.dockerenv, /run/.containerenv, cgroup) - 4. .git directory presence -> 'git' - 5. Fallback -> 'pip' + 3. .git directory presence -> 'git' + 4. Fallback -> 'pip' + + Note: running inside a container is NOT treated as "docker" on its own. + The two supported install paths both self-identify via the + ``.install_method`` stamp (caught by step 1), so neither relies on + container detection here: + - the curl installer (scripts/install.sh, the README/website install + command) git-clones the repo and stamps ``git``; + - the published ``nousresearch/hermes-agent`` image stamps ``docker`` + at boot via ``docker/stage2-hook.sh``. + An unsupported manual install dropped into a container (no stamp) was + wrongly classified as the published image by bare container detection, + so ``hermes update`` bailed with "doesn't apply inside the Docker + container". Without that fallback such installs fall through to the + ``.git``/pip checks and behave like any off-path install. See issue #34397. """ stamp = get_hermes_home() / ".install_method" try: @@ -299,9 +312,6 @@ def detect_install_method(project_root: Optional[Path] = None) -> str: managed = get_managed_system() if managed: return managed.lower().replace(" ", "-") - from hermes_constants import is_container - if is_container(): - return "docker" if project_root is None: project_root = Path(__file__).parent.parent.resolve() if (project_root / ".git").is_dir(): -
tests/hermes_cli/test_pip_install_detection.py@@ -48,12 +48,32 @@ def test_stamp_file_takes_precedence(tmp_path): assert detect_install_method(project_root=tmp_path) == "docker" -def test_docker_detected_via_dockerenv(tmp_path): +def test_container_without_stamp_is_not_docker(tmp_path): + """An unstamped install in a generic container must NOT be flagged as docker. + + Regression for issue #34397. The two supported installs both stamp + ``.install_method`` (the curl installer -> ``git``, covered by + ``test_stamp_file_takes_precedence``; the published image -> ``docker``), + so neither hits this path. An unsupported manual install dropped into a + container has no stamp and was wrongly classified as the published Docker + image, so ``hermes update`` refused to run. With a ``.git`` checkout it + must resolve to ``git``. + """ + (tmp_path / ".git").mkdir() with patch("hermes_cli.config.get_managed_system", return_value=None), \ patch("hermes_cli.config.get_hermes_home", return_value=tmp_path), \ patch("hermes_constants.is_container", return_value=True): from hermes_cli.config import detect_install_method - assert detect_install_method(project_root=tmp_path) == "docker" + assert detect_install_method(project_root=tmp_path) == "git" + + +def test_container_pip_install_without_stamp_is_pip(tmp_path): + """Container + no .git + no stamp -> pip, not docker (issue #34397).""" + with patch("hermes_cli.config.get_managed_system", return_value=None), \ + patch("hermes_cli.config.get_hermes_home", return_value=tmp_path), \ + patch("hermes_constants.is_container", return_value=True): + from hermes_cli.config import detect_install_method + assert detect_install_method(project_root=tmp_path) == "pip" def test_recommended_update_command_docker():
-
-
#36263 fix(dashboard): guard update endpoint in Docker with structured guidance (salvage #34831)
files:
hermes_cli/web_server.py,tests/hermes_cli/test_web_server.py,web/src/lib/api.ts -
#37085 fix(dashboard): surface Docker update guidance instead of generic failure (#34347)
files:
web/src/contexts/SystemActions.tsxShow the diff
-
web/src/contexts/SystemActions.tsx@@ -71,10 +71,28 @@ export function SystemActionsProvider({ try { if (action === "restart") { await api.restartGateway(); + setActiveAction(action); } else { - await api.updateHermes(); + const resp = await api.updateHermes(); + // In a Docker install the image is immutable, so `hermes update` + // can't apply — the endpoint returns 200 with a structured + // {ok:false, error:"docker_update_unsupported", message, update_command} + // envelope instead of spawning the action (see #34347 / #36263). + // Surface that guidance to the user rather than starting the poll, + // which would otherwise report a generic "failed (exit 1)". + if (!resp.ok && resp.error === "docker_update_unsupported") { + const cmd = resp.update_command ? ` ${resp.update_command}` : ""; + setToast({ + type: "success", + message: + (resp.message ?? + "Updates don't apply inside Docker — re-pull the image instead.") + + cmd, + }); + return; + } + setActiveAction(action); } - setActiveAction(action); } catch (err) { const detail = err instanceof Error ? err.message : String(err); setToast({
-
-
#48188 fix(update): scope install-method stamp to the code tree, not $HERMES_HOME
files:
Dockerfile,docker/stage2-hook.sh,hermes_cli/config.py,scripts/install.sh,tests/hermes_cli/test_pip_install_detection.py,tests/test_install_sh_install_method_stamp.py,tests/tools/test_dockerfile_immutable_install.py,tests/tools/test_stage2_hook_install_method_stamp.pyShow the diff
-
Dockerfile@@ -206,9 +206,16 @@ USER root RUN mkdir -p /opt/hermes/bin && \ cp /opt/hermes/docker/hermes-exec-shim.sh /opt/hermes/bin/hermes && \ chmod 0755 /opt/hermes/bin/hermes && \ + printf 'docker\n' > /opt/hermes/.install_method && \ chown -R root:root /opt/hermes && \ chmod -R a+rX /opt/hermes && \ chmod -R a-w /opt/hermes +# The ``.install_method`` stamp is baked next to the running code (the install +# tree), NOT into $HERMES_HOME. $HERMES_HOME (/opt/data) is a shared data +# volume that is commonly bind-mounted from the host and even shared with a +# host-side Desktop/CLI install; stamping it at boot used to clobber that +# host install's marker and wrongly block its ``hermes update``. A code-scoped +# stamp is read first by detect_install_method() and is immune to the share. # Start as root so the s6-overlay stage2 hook can usermod/groupmod and chown # the data volume. Each supervised service then drops to the hermes user via # `s6-setuidgid hermes` in its run script. If HERMES_UID is unset, services -
docker/stage2-hook.sh@@ -291,13 +291,25 @@ as_hermes mkdir -p \ "$HERMES_HOME/pairing" \ "$HERMES_HOME/platforms/pairing" -# --- Install-method stamp (read by detect_install_method() in hermes status) --- -# Preserved from the tini-era entrypoint (PR #27843). Must be written as -# the hermes user so ownership matches the file's documented owner. -# tee is invoked directly via s6-setuidgid (no `sh -c` wrapper) for the -# same shell-metacharacter safety described above. -printf 'docker\n' | as_hermes tee "$HERMES_HOME/.install_method" >/dev/null \ - || true +# --- 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() { -
hermes_cli/config.py@@ -350,52 +350,124 @@ def get_managed_update_command() -> Optional[str]: return None +def _install_method_project_root(project_root: Optional[Path] = None) -> Path: + """Resolve the directory that holds the *running code* (the install tree). + + This is the parent of ``hermes_cli/`` — i.e. the git checkout for source + installs, ``/opt/hermes`` inside the published image, the venv's + site-packages root for pip installs. It is a property of the running + interpreter, NOT of ``$HERMES_HOME``, which is why a code-scoped stamp + here is immune to two installs sharing one data directory. + """ + if project_root is not None: + return project_root + return Path(__file__).parent.parent.resolve() + + def detect_install_method(project_root: Optional[Path] = None) -> str: """Detect how Hermes was installed: 'docker', 'nixos', 'homebrew', 'git', or 'pip'. Resolution order: - 1. Stamped ``~/.hermes/.install_method`` file (written by installers) - 2. HERMES_MANAGED env / .managed marker (NixOS, Homebrew) - 3. .git directory presence -> 'git' - 4. Fallback -> 'pip' + 1. Code-scoped stamp ``<install tree>/.install_method`` (next to the + running code) — the authoritative marker. + 2. Legacy home-scoped stamp ``$HERMES_HOME/.install_method`` — read for + backward compatibility, but a ``docker`` value is IGNORED when we are + not actually running inside a container (see below). + 3. HERMES_MANAGED env / .managed marker (NixOS, Homebrew) + 4. .git directory presence -> 'git' + 5. Fallback -> 'pip' + + Why the stamp is code-scoped, not home-scoped (issue: shared ``~/.hermes``) + -------------------------------------------------------------------------- + The install method describes *the binary that is running*, but + ``$HERMES_HOME`` is a shared DATA directory — the Docker docs deliberately + bind-mount it (``~/.hermes:/opt/data``) so config/sessions/memory persist + and can be shared with a host-side Desktop/CLI install. When a + containerised gateway and a host install share one ``$HERMES_HOME``, a + home-scoped stamp is a single slot describing two different installs: + the container stamps ``docker`` on every boot, the host install then reads + ``docker`` and ``hermes update`` refuses to run ("doesn't apply inside the + Docker container") even though the host binary is a perfectly updatable + git/pip install. Scoping the stamp to the install tree gives each install + its own truthful marker. + + Self-healing for already-poisoned homes: a legacy ``docker`` value in the + home-scoped stamp is only honoured when we are genuinely in a container. + On a host install that read a contaminating ``docker`` stamp, we fall + through to managed/.git/pip detection instead — so existing shared-home + setups recover without the user touching anything. Note: running inside a container is NOT treated as "docker" on its own. - The two supported install paths both self-identify via the - ``.install_method`` stamp (caught by step 1), so neither relies on - container detection here: + The supported installs self-identify via the code-scoped stamp: - the curl installer (scripts/install.sh, the README/website install - command) git-clones the repo and stamps ``git``; - - the published ``nousresearch/hermes-agent`` image stamps ``docker`` - at boot via ``docker/stage2-hook.sh``. - An unsupported manual install dropped into a container (no stamp) was - wrongly classified as the published image by bare container detection, - so ``hermes update`` bailed with "doesn't apply inside the Docker - container". Without that fallback such installs fall through to the - ``.git``/pip checks and behave like any off-path install. See issue #34397. + command) git-clones the repo and stamps ``git`` next to the code; + - the published ``nousresearch/hermes-agent`` image bakes a ``docker`` + stamp into ``/opt/hermes`` at build time. + An unsupported manual install dropped into a container (no stamp) falls + through to the ``.git``/pip checks and behaves like any off-path install. + See issue #34397. """ - stamp = get_hermes_home() / ".install_method" + root = _install_method_project_root(project_root) + + # 1. Code-scoped stamp — authoritative, immune to shared $HERMES_HOME. try: - method = stamp.read_text(encoding="utf-8").strip().lower() + method = (root / ".install_method").read_text(encoding="utf-8").strip().lower() if method: return method except OSError: pass + + # 2. Legacy home-scoped stamp — back-compat. Ignore a ``docker`` value + # when we are not actually containerised: that is the signature of a + # host install whose shared $HERMES_HOME was stamped by a co-located + # container, and honouring it wrongly blocks ``hermes update``. + try: + method = ( + (get_hermes_home() / ".install_method") + .read_text(encoding="utf-8") + .strip() + .lower() + ) + if method and not (method == "docker" and not _running_in_container()): + return method + except OSError: + pass + managed = get_managed_system() if managed: return managed.lower().replace(" ", "-") - if project_root is None: - project_root = Path(__file__).parent.parent.resolve() - if (project_root / ".git").is_dir(): + if (root / ".git").is_dir(): return "git" return "pip" -def stamp_install_method(method: str) -> None: - """Write the install method to ~/.hermes/.install_method.""" - stamp = get_hermes_home() / ".install_method" +def _running_in_container() -> bool: + """Thin wrapper around ``hermes_constants.is_container`` (import-safe).""" + try: + from hermes_constants import is_container + + return is_container() + except Exception: + return False + + +def stamp_install_method(method: str, project_root: Optional[Path] = None) -> None: + """Write the install method next to the running code (code-scoped stamp). + + The stamp lives in the install tree (``<install tree>/.install_method``), + not in ``$HERMES_HOME``, so that two installs sharing one data directory + do not overwrite each other's marker. See ``detect_install_method`` for + the full rationale. + + Best-effort: if the install tree is read-only (e.g. the immutable + ``/opt/hermes`` in the published image, which instead bakes the stamp at + build time) the write silently no-ops and detection falls back to its + other signals. + """ + root = _install_method_project_root(project_root) try: - stamp.parent.mkdir(parents=True, exist_ok=True) - stamp.write_text(method + "\n", encoding="utf-8") + root.mkdir(parents=True, exist_ok=True) + (root / ".install_method").write_text(method + "\n", encoding="utf-8") except OSError: pass -
scripts/install.sh@@ -2732,7 +2732,12 @@ run_stage_body() { detect_os resolve_install_layout print_success - echo "git" > "$HERMES_HOME/.install_method" + # Code-scoped stamp: write next to the install tree, not into + # $HERMES_HOME. $HERMES_HOME is a shared data dir (it can be + # bind-mounted into a Docker gateway too), so a stamp there gets + # clobbered by the container's 'docker' stamp and wrongly blocks + # 'hermes update' on this host install. See detect_install_method(). + echo "git" > "$INSTALL_DIR/.install_method" ;; *) log_error "Unknown stage: $stage" @@ -2811,7 +2816,12 @@ main() { print_success - echo "git" > "$HERMES_HOME/.install_method" + # Code-scoped stamp: write next to the install tree, not into $HERMES_HOME. + # $HERMES_HOME is a shared data dir (it can be bind-mounted into a Docker + # gateway too), so a stamp there gets clobbered by the container's 'docker' + # stamp and wrongly blocks 'hermes update' on this host install. + # See detect_install_method(). + echo "git" > "$INSTALL_DIR/.install_method" } if [ "$MANIFEST_MODE" = true ]; then -
tests/hermes_cli/test_pip_install_detection.py@@ -48,6 +48,97 @@ def test_stamp_file_takes_precedence(tmp_path): assert detect_install_method(project_root=tmp_path) == "docker" +def test_code_scoped_stamp_wins_over_home_stamp(tmp_path): + """The stamp next to the running code is authoritative over $HERMES_HOME. + + Models a host git install whose $HERMES_HOME is shared with (and stamped + 'docker' by) a co-located container. The code-scoped stamp must win so the + host install is correctly identified as 'git' and 'hermes update' works. + """ + code = tmp_path / "code" + home = tmp_path / "home" + code.mkdir() + home.mkdir() + (code / ".install_method").write_text("git\n") + (home / ".install_method").write_text("docker\n") # container contamination + with patch("hermes_cli.config.get_managed_system", return_value=None), \ + patch("hermes_cli.config.get_hermes_home", return_value=home): + from hermes_cli.config import detect_install_method + assert detect_install_method(project_root=code) == "git" + + +def test_home_docker_stamp_ignored_when_not_containerized(tmp_path): + """A 'docker' home stamp is ignored on a host (non-container) install. + + Self-heal path for homes already poisoned by an older image that wrote + 'docker' into the shared $HERMES_HOME. With no code-scoped stamp, a host + git checkout must fall through to '.git' detection rather than honour the + contaminating 'docker' value and refuse to update. + """ + code = tmp_path / "code" + home = tmp_path / "home" + code.mkdir() + home.mkdir() + (code / ".git").mkdir() + (home / ".install_method").write_text("docker\n") + with patch("hermes_cli.config.get_managed_system", return_value=None), \ + patch("hermes_cli.config.get_hermes_home", return_value=home), \ + patch("hermes_cli.config._running_in_container", return_value=False): + from hermes_cli.config import detect_install_method + assert detect_install_method(project_root=code) == "git" + + +def test_home_docker_stamp_honored_inside_container(tmp_path): + """A 'docker' home stamp is still honoured when genuinely containerized. + + Back-compat: an older published image that only ever wrote the home-scoped + stamp (no baked code stamp) must still resolve to 'docker' so the update + path keeps directing the user to ``docker pull``. + """ + code = tmp_path / "code" + home = tmp_path / "home" + code.mkdir() + home.mkdir() + (home / ".install_method").write_text("docker\n") + with patch("hermes_cli.config.get_managed_system", return_value=None), \ + patch("hermes_cli.config.get_hermes_home", return_value=home), \ + patch("hermes_cli.config._running_in_container", return_value=True): + from hermes_cli.config import detect_install_method + assert detect_install_method(project_root=code) == "docker" + + +def test_home_non_docker_stamp_still_honored_for_backcompat(tmp_path): + """Legacy non-'docker' home stamps (e.g. 'git') are still respected. + + Only the 'docker' value carries the cross-contamination risk, so a host + install that historically stamped 'git'/'pip' into $HERMES_HOME keeps + resolving from there when no code-scoped stamp exists yet. + """ + code = tmp_path / "code" + home = tmp_path / "home" + code.mkdir() + home.mkdir() + (home / ".install_method").write_text("git\n") + with patch("hermes_cli.config.get_managed_system", return_value=None), \ + patch("hermes_cli.config.get_hermes_home", return_value=home), \ + patch("hermes_cli.config._running_in_container", return_value=False): + from hermes_cli.config import detect_install_method + assert detect_install_method(project_root=code) == "git" + + +def test_stamp_install_method_writes_code_scoped(tmp_path): + """stamp_install_method writes next to the code, not into $HERMES_HOME.""" + code = tmp_path / "code" + home = tmp_path / "home" + code.mkdir() + home.mkdir() + with patch("hermes_cli.config.get_hermes_home", return_value=home): + from hermes_cli.config import stamp_install_method + stamp_install_method("pip", project_root=code) + assert (code / ".install_method").read_text().strip() == "pip" + assert not (home / ".install_method").exists() + + def test_container_without_stamp_is_not_docker(tmp_path): """An unstamped install in a generic container must NOT be flagged as docker. -
tests/test_install_sh_install_method_stamp.py@@ -0,0 +1,40 @@ +"""Contract test: install.sh stamps the install method next to the code tree +($INSTALL_DIR), not into the shared $HERMES_HOME. + +Background (shared-$HERMES_HOME bug) +------------------------------------ +$HERMES_HOME is a data directory users frequently bind-mount into a Docker +gateway as well (``~/.hermes:/opt/data``). The published image stamps 'docker' +there on boot, so if install.sh had written its 'git' marker into the same +$HERMES_HOME the two installs would fight over one slot — and the container, +booting last, would win and wrongly make the host install look like 'docker' +(blocking ``hermes update``). + +The fix: detect_install_method() reads a CODE-scoped stamp first, and the +installer writes ``git`` into $INSTALL_DIR (the git checkout, e.g. +``~/.hermes/hermes-agent``), which is unique to this install and immune to the +shared data dir. +""" +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +INSTALL_SH = REPO_ROOT / "scripts" / "install.sh" + + +def test_install_sh_stamps_code_tree_not_home() -> None: + text = INSTALL_SH.read_text() + + # Stamps the code tree. + assert text.count('echo "git" > "$INSTALL_DIR/.install_method"') >= 1, ( + "install.sh must stamp $INSTALL_DIR/.install_method (code-scoped)" + ) + + # Never stamps the shared data dir. + assert not re.search(r'>\s*"\$HERMES_HOME/\.install_method"', text), ( + "install.sh must not stamp $HERMES_HOME/.install_method — that data " + "dir may be shared with a Docker gateway whose 'docker' stamp would " + "clobber it and block host-side `hermes update`" + ) -
tests/tools/test_dockerfile_immutable_install.py@@ -59,3 +59,28 @@ def test_dockerfile_does_not_chown_install_trees_to_hermes() -> None: "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" + ) -
tests/tools/test_stage2_hook_install_method_stamp.py@@ -0,0 +1,61 @@ +"""Contract test: the s6-overlay stage2 hook must NOT stamp the install method +into the shared $HERMES_HOME, and must heal a stale 'docker' stamp left there +by older images. + +Background (shared-$HERMES_HOME bug) +------------------------------------ +$HERMES_HOME (/opt/data) is a DATA volume that users commonly bind-mount from +the host (``~/.hermes:/opt/data``) and sometimes share with a host-side +Desktop/CLI install. Older images wrote ``printf 'docker' > $HERMES_HOME/.install_method`` +at boot, which clobbered the host install's own marker — so the host's in-app +updater read 'docker' and refused to run ``hermes update`` ("doesn't apply +inside the Docker container"). + +The fix scopes the stamp to the install tree (baked at +``/opt/hermes/.install_method`` in the Dockerfile, read first by +``detect_install_method``). stage2 must therefore: + + * NOT write the 'docker' stamp into $HERMES_HOME any more, and + * proactively remove a stale 'docker' stamp from $HERMES_HOME so homes + already poisoned by an older image self-heal on the next boot. +""" +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[2] +STAGE2_HOOK = REPO_ROOT / "docker" / "stage2-hook.sh" + + [redacted:email](scope="module") +def stage2_text() -> str: + if not STAGE2_HOOK.exists(): + pytest.skip("docker/stage2-hook.sh not present in this checkout") + return STAGE2_HOOK.read_text() + + +def test_stage2_does_not_write_install_method_into_home(stage2_text: str) -> None: + # No write/tee of the home-scoped install-method stamp anywhere. + assert not re.search( + r"(tee|>)\s*\"?\$HERMES_HOME/\.install_method", stage2_text + ), ( + "stage2 must not stamp $HERMES_HOME/.install_method — that data dir " + "may be shared with a host install whose marker would be clobbered" + ) + + +def test_stage2_heals_stale_docker_home_stamp(stage2_text: str) -> None: + # It must remove a stale 'docker' stamp from $HERMES_HOME so already + # poisoned shared homes recover. + assert 'rm -f "$HERMES_HOME/.install_method"' in stage2_text, ( + "stage2 must remove a stale 'docker' stamp from $HERMES_HOME to heal " + "homes poisoned by older images" + ) + # The removal must be guarded on the value being 'docker' so we never + # delete a legitimately-different stamp a user/host install put there. + assert re.search(r'\[\s*"\$stamped"\s*=\s*"docker"\s*\]', stage2_text), ( + "the stale-stamp removal must be guarded on the value == 'docker'" + )
-
-
#109173 Top-level platform keys like platforms.webhook.port reach the adapter (#10206, #20501)
files:
gateway/config.py,gateway/config_loader.py,tests/gateway/test_config.py,website/docs/user-guide/messaging/webhooks.md
13 PRs closed without merging
From the close comments: done elsewhere 5 · duplicate 3 · merged via another change 3 · PR volume cleanup 1 · closed without comment 1
-
#2307 fix(gateway): bridge webhook extra.routes from config.yaml to gateway config · done elsewhere →
#2328 -
#2308 fix: Webhook route docs/config mismatch · merged via another change →
#2328 - #2310 feat(config): add support for nested platform mapping in config.yaml · closed without comment
-
#10211 fix(config): forward unknown platform keys into config.extra · duplicate →
#10453 -
#10219 fix(gateway): pass unknown platform config keys to extra · duplicate →
#10453
Show 8 more
- #10453 fix(gateway): preserve top-level platform config keys · PR volume cleanup
- #20528 fix: read top-level api_server config fields into extra · done elsewhere
-
#21114 fix(gateway): honor top-level platform config extras · duplicate →
#20506,#10453 -
#34426 fix(cli): scope docker install detection to the official image · done elsewhere →
c1b2d09,#35139,#34397 -
#34831 fix: guard dashboard update in Docker · merged via another change →
#36263,#34347,#35835 -
#35034 fix(cli): check .git before stamp file in detect_install_method · done elsewhere →
4440d77,#48188 -
#35339 fix(dashboard): guard update endpoint in Docker with structured guidance · merged via another change →
#36263,#34831 -
#36254 fix(config): ignore stale docker install-method stamp when .git exists (#35835) · done elsewhere →
4440d77,#48188
Closed issues · 8
- Closed#2305[Bug]: Webhook route docs/config mismatch
- Closed#10206[Bug]: platforms.webhook.port in config.yaml does not work
- Closed#20501platforms.api_server config values (port, host, key) silently ignored when not nested under 'extra'
- Closed#34347WebUI "Update" button surfaces raw SystemExit when run in Docker context
- Closed#34397[Bug]: Hermes dont discriminate between running in a docker container and being the docker image.
- Closed#35026[Bug]: linux: update fails
- Closed#35835[Bug]: hermes update doesn't apply inside the Docker container.
- Closed#50620[Bug]: Inside Docker Fails to Configuration sub component settings
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.06: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab