Commit Graph

2 Commits

Author SHA1 Message Date
kshitij 43fc86562c fix(utils): tighten create_mode semantics and close the yaml 0600 transit window
Post-review fixes on the preserve_mode/create_mode follow-up:

- create_mode is now applied ONLY when the target does not exist, on
  both atomic_write_text and atomic_yaml_write. Previously
  atomic_write_text(path, s, create_mode=X) without preserve_mode would
  silently chmod an EXISTING file to X (docstring/code mismatch, latent
  trap -- no caller relied on it), and a stat failure on an existing
  file could fall through to create_mode instead of leaving the mode
  alone.

- atomic_yaml_write now fchmods the temp fd BEFORE the replace when a
  mode is known, matching atomic_write_text: a freshly created
  distribution.yaml no longer transits through mkstemp's 0600 (a crash
  between replace and chmod could previously leave it 0600 forever).
  The post-replace _restore_file_mode stays as the Windows path.

- fchmod moved inside the fdopen context in atomic_write_text, so a
  raising fchmod can no longer leak the fd.

Tests: create_mode-never-rewrites-existing guard (mutation-checked) and
a monkeypatch.delattr(os, 'fchmod') test covering the Windows
post-replace branch that the win32 module skip left uncovered.
2026-08-06 05:00:17 +05:30
kshitij 3556728a54 refactor(utils): move mode+owner preservation into atomic_write_text
Follow-up to the salvaged #79323 commits. The three hand-rolled
stat -> atomic_write_text -> chmod blocks (xai migration, uninstaller
shell-rc rewrite, dashboard SOUL.md editor) collapse into an opt-in
preserve_mode=True kwarg on utils.atomic_write_text, plus create_mode=
on both atomic_write_text and atomic_yaml_write for first-create paths
(SOUL.md first save, write_manifest's allowlist create path).

Beyond deduplication this closes two gaps the hand-rolled copies had:

- Owner preservation: the old in-place writes kept the inode, so file
  ownership survived root-run rewrites for free. atomic_write_text
  swaps in a new inode owned by the writing user, and the hand-rolled
  blocks restored only the mode -- a root-run 'hermes migrate xai' or
  sudo uninstall on a user-owned Docker/NAS volume would flip
  config.yaml / ~/.zshrc ownership to root. preserve_mode now routes
  through the same _preserve_file_owner/_restore_file_owner helpers
  atomic_yaml_write and atomic_json_write already use.

- chmod-after-replace window: the mode is applied to the temp fd via
  fchmod BEFORE the replace (mirroring atomic_json_write's mode= param),
  so the target never transits through mkstemp's 0600.

Also removes write_manifest's caller-side existed/chmod block (and its
small TOCTOU) in favor of atomic_yaml_write(create_mode=0o644), and
corrects the SOUL.md mode comment (the default profile's runtime seeder
does run it through _secure_file; named profiles do not).

preserve_mode defaults to False so the existing callers (memory store,
skill manager, cron, agent importer) keep their current semantics.

New tests in tests/test_atomic_write_text_metadata.py cover mode
preservation, owner restore through symlinks, fchmod-before-replace,
create_mode on both writers, and no-behavior-change without opt-in;
all mutation-checked.
2026-08-06 05:00:17 +05:30