diff --git a/nanobot/utils/helpers.py b/nanobot/utils/helpers.py index 86b4789d..3925e785 100644 --- a/nanobot/utils/helpers.py +++ b/nanobot/utils/helpers.py @@ -5,6 +5,7 @@ import json import os import re import shutil +import stat import time import uuid from contextlib import suppress @@ -454,8 +455,13 @@ def _cleanup_tool_result_buckets(root: Path, current_bucket: Path) -> None: def _write_text_atomic(path: Path, content: str) -> None: tmp = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp") + existing_mode: int | None = None + with suppress(OSError): + existing_mode = stat.S_IMODE(path.stat().st_mode) try: with open(tmp, "w", encoding="utf-8") as f: + if existing_mode is not None: + os.chmod(tmp, existing_mode) f.write(content) f.flush() os.fsync(f.fileno()) diff --git a/tests/config/test_config_atomic_save.py b/tests/config/test_config_atomic_save.py index f2ac2367..4ebde1ea 100644 --- a/tests/config/test_config_atomic_save.py +++ b/tests/config/test_config_atomic_save.py @@ -3,6 +3,8 @@ from __future__ import annotations import json +import os +import stat from pathlib import Path import pytest @@ -18,6 +20,17 @@ def test_save_config_round_trips(tmp_path: Path) -> None: assert loaded.agents.defaults.model +@pytest.mark.skipif(os.name == "nt", reason="Windows does not expose POSIX file modes") +def test_save_config_preserves_existing_file_mode(tmp_path: Path) -> None: + path = tmp_path / "config.json" + path.write_text("{}", encoding="utf-8") + path.chmod(0o600) + + save_config(Config(), path) + + assert stat.S_IMODE(path.stat().st_mode) == 0o600 + + def test_save_config_preserves_existing_file_when_write_fails( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: