fix(config): preserve permissions during atomic save

This commit is contained in:
Xubin Ren 2026-07-21 15:44:59 +08:00
parent 28102382af
commit b2cf37da4a
2 changed files with 19 additions and 0 deletions

View File

@ -5,6 +5,7 @@ import json
import os import os
import re import re
import shutil import shutil
import stat
import time import time
import uuid import uuid
from contextlib import suppress 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: def _write_text_atomic(path: Path, content: str) -> None:
tmp = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp") 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: try:
with open(tmp, "w", encoding="utf-8") as f: with open(tmp, "w", encoding="utf-8") as f:
if existing_mode is not None:
os.chmod(tmp, existing_mode)
f.write(content) f.write(content)
f.flush() f.flush()
os.fsync(f.fileno()) os.fsync(f.fileno())

View File

@ -3,6 +3,8 @@
from __future__ import annotations from __future__ import annotations
import json import json
import os
import stat
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -18,6 +20,17 @@ def test_save_config_round_trips(tmp_path: Path) -> None:
assert loaded.agents.defaults.model 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( def test_save_config_preserves_existing_file_when_write_fails(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None: