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
This commit is contained in:
14
cron/jobs.py
14
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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -817,6 +817,10 @@ docker run -d \
|
||||
|
||||
`docker exec hermes <cmd>` 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`.
|
||||
|
||||
Reference in New Issue
Block a user