diff --git a/ai/Dockerfile b/ai/Dockerfile index c2a0b08..45806e7 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -53,6 +53,55 @@ urllib.request.urlretrieve(url, base + '/en_US-ryan-high.onnx') urllib.request.urlretrieve(url + '.json', base + '/en_US-ryan-high.onnx.json') PYEOF +# ---------- Patch atomic writes to preserve file permissions ---------- +# Fixes https://github.com/NousResearch/hermes-agent/issues/14181 +# tempfile.mkstemp() creates files as 0600; os.replace() preserves that mode, +# so group-readable files silently collapse to owner-private 0600. +# This affects: skills, sessions, memories, and any file written atomically. +RUN /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF' +import os + +patches = [ + ("/opt/hermes/tools/skill_manager_tool.py", [ + ("# Restore existing file mode if present", True), # already patched + ]), + ("/opt/hermes/tools/skills_sync.py", [ + ("# Restore existing file mode if present", True), # already patched + ]), +] + +for fpath, checks in patches: + if not os.path.exists(fpath): + print(f"SKIP {fpath} (not found)") + continue + with open(fpath) as f: + code = f.read() + all_ok = all(marker in code for marker, _ in checks) + if all_ok: + print(f"OK {fpath} (already patched)") + continue + print(f"PATCH {fpath}") + # _atomic_write_text in skill_manager_tool.py + code = code.replace( + " os.replace(temp_path, file_path)", + " if file_path.exists():\n" + " existing_mode = file_path.stat().st_mode\n" + " os.chmod(temp_path, existing_mode)\n" + " os.replace(temp_path, file_path)", + ) + # _write_manifest in skills_sync.py + code = code.replace( + " os.replace(tmp_path, MANIFEST_FILE)", + " if MANIFEST_FILE.exists():\n" + " existing_mode = MANIFEST_FILE.stat().st_mode\n" + " os.chmod(tmp_path, existing_mode)\n" + " os.replace(tmp_path, MANIFEST_FILE)", + ) + with open(fpath, 'w') as f: + f.write(code) + print(f"DONE {fpath}") +PYEOF + # ---------- Runtime ---------- ENV HERMES_HOME=/opt/data ENV PATH="/opt/data/.local/bin:${PATH}"