fix(utils): new non-secret atomic writes follow the process umask again

Every hand-rolled writer this PR folded into utils._atomic_write created a
NEW file with write_text()/open("w"), i.e. at 0o666 masked by the umask
(0644 under 022). The canonical helper publishes through mkstemp, whose
temp is 0600, and with no explicit mode and no existing target to copy bits
from it left that 0600 in place - so debug, model_catalog, profiles,
breadcrumbs, worktree_ops, web_result_cache, plugin_compat, write_approval,
rich_sent_store, active_sessions and the google_meet state files were
silently tightened to owner-only, the volume-mount hazard
_restore_file_metadata's own docstring warns about. Undeclared in the PR.

Fix at the canonical: when mode is None and the target does not exist,
apply default_new_file_mode() (0o666 masked by the umask, read via the
umask two-call trick with a transient 0o077 so a racing thread can only get
a tighter file). The helper is hermes_cli/backup._default_new_file_mode
moved into utils and reused. Secret writers (mode=0o600) are 0600 before,
during and after as before; an existing target keeps its bits; on non-POSIX
the helper returns None so nothing is chmod'd.
This commit is contained in:
teknium1
2026-09-13 00:08:06 -07:00
committed by Teknium
parent 4157494d2f
commit 714013c493
3 changed files with 55 additions and 19 deletions
+26
View File
@@ -84,3 +84,29 @@ def test_surrogate_escaped_strings_round_trip_through_atomic_json_write(tmp_path
assert json.loads(target.read_bytes()) == payload
assert _leftovers(target.parent, target.name) == []
@pytest.mark.linux_only
def test_new_non_secret_file_follows_umask_while_secret_and_existing_modes_hold(tmp_path):
"""The writers this helper replaced created files at process umask; only ``mode=`` tightens."""
import os
import stat
from utils import atomic_json_write
old_umask = os.umask(0o022)
try:
fresh = tmp_path / "cache.json"
atomic_json_write(fresh, {"a": 1})
assert stat.S_IMODE(fresh.stat().st_mode) == 0o644, "new non-secret file must not inherit mkstemp's 0600"
secret = tmp_path / "creds.json"
atomic_json_write(secret, {"token": "x"}, mode=0o600)
assert stat.S_IMODE(secret.stat().st_mode) == 0o600
existing = tmp_path / "state.json"
existing.write_text("{}", encoding="utf-8")
os.chmod(existing, 0o640)
atomic_json_write(existing, {"b": 2})
assert stat.S_IMODE(existing.stat().st_mode) == 0o640
finally:
os.umask(old_umask)