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.
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.