From b78abb4710f3fceb4efafca255875bc94d614986 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 12 Sep 2026 06:31:02 -0700 Subject: [PATCH] fix(config): stop forcing 0700 on HERMES_HOME inside containers Every start (and every `ensure_hermes_home` from a sibling CLI invocation) chmod'd the data directory to 0700, wiping group/other bits and the ACL mask on a bind mount shared with other containers (hermes-webui, Nix desktop + dashboard). _secure_file already skipped containers for this reason; _secure_dir did not. In a container the directory mode is now left to the operator unless HERMES_HOME_MODE is set explicitly, which is still applied. cron/jobs.py had its own 0700/0600 copies that bypassed the managed/container rules; they now delegate to the shared helpers so cron/output stops re-locking the mount as well. Fixes #10757 --- cron/jobs.py | 14 +++++++------- hermes_cli/config.py | 16 ++++++++++++---- tests/cron/test_file_permissions.py | 16 ++++++++++++++++ website/docs/user-guide/docker.md | 4 ++++ 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/cron/jobs.py b/cron/jobs.py index f0e790f353..058cf86df3 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -524,16 +524,16 @@ def _is_recoverable_error_job(job: Dict[str, Any]) -> bool: def _secure_dir(path: Path): - """Set directory to owner-only access (0700). No-op where chmod is unsupported (Windows).""" - with contextlib.suppress(OSError, NotImplementedError): - os.chmod(path, 0o700) + """Owner-only (0700) via the shared helper, so cron/ and cron/output honor the same managed/ + container/HERMES_HOME_MODE rules as the rest of HERMES_HOME (#10757).""" + from hermes_cli.config import _secure_dir as _shared_secure_dir + _shared_secure_dir(path) def _secure_file(path: Path): - """Set file to owner-only read/write (0600). No-op where chmod is unsupported (Windows).""" - with contextlib.suppress(OSError, NotImplementedError): - if path.exists(): - os.chmod(path, 0o600) + """Owner-only (0600) via the shared helper (managed/container skip included).""" + from hermes_cli.config import _secure_file as _shared_secure_file + _shared_secure_file(path) def _preserve_file_ownership(path: Path, before: Optional[os.stat_result]) -> None: diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 3d8a8ba143..05615a9b27 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -549,9 +549,10 @@ def _chown_to_hermes_uid(path) -> None: def _secure_dir(path): - """chmod a directory owner-only (0700) and apply HERMES_UID/GID ownership. No-op when managed. - HERMES_HOME_MODE (e.g. 0701) overrides the mode so a web server can traverse HERMES_HOME to - a served subdirectory without directory listings. + """chmod a directory owner-only (0700) and apply HERMES_UID/GID ownership. No-op when managed; + in a container only an explicit HERMES_HOME_MODE is applied. HERMES_HOME_MODE (e.g. 0701) + overrides the mode so a web server can traverse HERMES_HOME to a served subdirectory without + directory listings. Also applies ``HERMES_UID``/``HERMES_GID``-based ownership when those env vars are set (#34107 — Docker deployments need this so profile subdirs created at runtime by kanban workers don't land as root:root @@ -559,8 +560,15 @@ def _secure_dir(path): """ if is_managed(): return + explicit_mode = os.environ.get("HERMES_HOME_MODE", "").strip() + # Same skip as _secure_file: a bind-mounted data dir is often shared with sibling containers + # running as other UIDs (web UI, permissions fixers); forcing 0700 on it locks them out on every + # start (#10757). An explicit HERMES_HOME_MODE is the operator's choice and is still applied. + if _is_container() and not explicit_mode: + _chown_to_hermes_uid(path) + return try: - mode = int(os.environ.get("HERMES_HOME_MODE", "").strip() or "700", 8) + mode = int(explicit_mode or "700", 8) except ValueError: mode = 0o700 try: diff --git a/tests/cron/test_file_permissions.py b/tests/cron/test_file_permissions.py index 20f57a82b7..9bb53efa5f 100644 --- a/tests/cron/test_file_permissions.py +++ b/tests/cron/test_file_permissions.py @@ -125,6 +125,22 @@ class TestSecureHelpers(unittest.TestCase): from cron.jobs import _secure_file _secure_file(Path("/nonexistent/path/file.json")) # Should not raise + def test_secure_dir_preserves_operator_mode_in_container(self): + """A bind-mounted data dir shared with sibling containers must keep the operator's mode; + an explicit HERMES_HOME_MODE is still honored (#10757).""" + from cron.jobs import _secure_dir + with tempfile.TemporaryDirectory() as tmp: + d = Path(tmp) / "cron" + d.mkdir() + os.chmod(d, 0o755) + with patch.dict(os.environ, {"HERMES_CONTAINER": "1"}, clear=False): + os.environ.pop("HERMES_HOME_MODE", None) + _secure_dir(d) + self.assertEqual(stat.S_IMODE(os.stat(d).st_mode), 0o755) + os.environ["HERMES_HOME_MODE"] = "0701" + _secure_dir(d) + self.assertEqual(stat.S_IMODE(os.stat(d).st_mode), 0o701) + if __name__ == "__main__": unittest.main() diff --git a/website/docs/user-guide/docker.md b/website/docs/user-guide/docker.md index dbdb77cb21..11ad58a237 100644 --- a/website/docs/user-guide/docker.md +++ b/website/docs/user-guide/docker.md @@ -817,6 +817,10 @@ docker run -d \ `docker exec hermes ` automatically drops to UID 10000 too — see [`docker exec` automatically drops to the `hermes` user](#docker-exec-automatically-drops-to-the-hermes-user) for details and the per-invocation opt-out. +### Shared data directory keeps resetting to `0700` + +Outside a container Hermes locks `HERMES_HOME` (and its `cron/`, `sessions/`, `logs/`, `memories/` subdirectories) to owner-only `0700` on every start. Inside a container it leaves directory modes alone, so a bind mount shared with a sibling container running as a different UID (a web UI, a permissions fixer) keeps whatever mode and ACLs you set on the host. To force a specific directory mode anyway, set `HERMES_HOME_MODE` (octal, e.g. `HERMES_HOME_MODE=0755`); it is applied in containers too. + ### "Permission denied" on every `docker exec` (install dir locked to 0700) Images built before late August 2026 had a bug where writing a credential file directly under `/opt/hermes` restricted that directory to `0700`, locking the `hermes` user (UID 10000) out of the install tree. Every new `docker exec` then fails with `Permission denied`.