diff --git a/Dockerfile b/Dockerfile index 08a5b6a275..6ed111f5b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,8 +66,14 @@ RUN cd web && npm run build && \ # ---------- Permissions ---------- # Make install dir world-readable so any HERMES_UID can read it at runtime. # The venv needs to be traversable too. +# node_modules trees additionally need to be writable by the hermes user +# so the runtime `npm install` triggered by _tui_need_npm_install() in +# hermes_cli/main.py succeeds (see #18800). /opt/hermes/web is build-time +# only (HERMES_WEB_DIST points at hermes_cli/web_dist) and is intentionally +# not chowned here. USER root -RUN chmod -R a+rX /opt/hermes +RUN chmod -R a+rX /opt/hermes && \ + chown -R hermes:hermes /opt/hermes/ui-tui /opt/hermes/node_modules # Start as root so the entrypoint can usermod/groupmod + gosu. # If HERMES_UID is unset, the entrypoint drops to the default hermes user (10000). diff --git a/tests/tools/test_dockerfile_node_modules_perms.py b/tests/tools/test_dockerfile_node_modules_perms.py new file mode 100644 index 0000000000..56243248ab --- /dev/null +++ b/tests/tools/test_dockerfile_node_modules_perms.py @@ -0,0 +1,39 @@ +"""contract test: dockerfile chowns runtime node_modules trees to hermes + +regression guard for #18800. the container drops privileges to the hermes +user (uid 10000) in entrypoint.sh, then the TUI launcher's +_tui_need_npm_install() trips on every startup (see the +npm_config_install_links=false comment in the Dockerfile) and runs +`npm install` in /opt/hermes/ui-tui. that install fails with EACCES unless +the runtime node_modules trees are owned by hermes. +""" +from __future__ import annotations + +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +DOCKERFILE = REPO_ROOT / "Dockerfile" + + +def test_dockerfile_chowns_runtime_node_modules_to_hermes_user() -> None: + text = DOCKERFILE.read_text() + + chown_lines = [ + line for line in text.splitlines() + if "chown" in line and "hermes:hermes" in line + ] + assert chown_lines, ( + "Dockerfile must contain a chown -R hermes:hermes for the runtime " + "node_modules trees; see #18800" + ) + + chown_block = "\n".join(chown_lines) + + # both runtime-mutable trees must be passed to the chown command. + # /opt/hermes/web is intentionally excluded: it is build-time only, + # because HERMES_WEB_DIST points at hermes_cli/web_dist for runtime. + for required_path in ("/opt/hermes/ui-tui", "/opt/hermes/node_modules"): + assert required_path in chown_block, ( + f"{required_path} must be passed to a chown -R hermes:hermes " + f"command in the Dockerfile (see #18800)" + )