fix(utils): handle EXDEV in atomic_replace for cross-filesystem symlinks - #36856
Closed
maxmilian wants to merge 1 commit into
Closed
fix(utils): handle EXDEV in atomic_replace for cross-filesystem symlinks#36856maxmilian wants to merge 1 commit into
maxmilian wants to merge 1 commit into
Conversation
When a managed deployment symlinks ~/.hermes/config.yaml (or SOUL.md / auth.json) to a file on a different filesystem, atomic_replace resolved the symlink to a real path on the other device while the temp file stayed staged next to the symlink. os.replace cannot rename across devices, so it raised EXDEV and broke every atomic write to that target. atomic_replace now catches EXDEV and re-stages the bytes on the target's own filesystem (copy + fsync + os.replace within that fs) before replacing there, keeping the write atomic. Non-EXDEV OSErrors still propagate untouched. Regression tests (5) in test_atomic_replace_symlinks.py: cross-device symlink fallback, atomic_json_write over a cross-device symlink, staged temp lands on the target filesystem, copy-failure leaves the target intact with no leak, and non-EXDEV OSErrors are not swallowed. Fixes NousResearch#36653 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maxmilian
force-pushed
the
fix/atomic-replace-exdev-fallback
branch
from
June 1, 2026 15:04
c013597 to
a9ffb6b
Compare
maxmilian
marked this pull request as ready for review
June 1, 2026 15:28
13 tasks
Contributor
Author
|
Closing — |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
atomic_replace(used by everyatomic_json_write/atomic_yaml_writesite) failed withOSError: [Errno 18] EXDEVwhen the target was a symlink whose real file lives on a different filesystem — e.g. a managed deployment that symlinks~/.hermes/config.yaml/SOUL.md/auth.jsonto a git-tracked profile package on another mount. Every atomic write to such a target blew up.Fixes #36653.
The bug
atomic_*_writestages the temp file inpath.parent(next to the symlink, on filesystem A).atomic_replacethen resolves the symlink to its real target (on filesystem B) and callsos.replace(tmp, real_path)— which can't rename across devices, so it raisesEXDEV. The non-symlink path is unaffected (temp and target share a directory, hence a filesystem).Changes
utils.py:atomic_replacenow catchesEXDEVand delegates to a new_replace_across_deviceshelper, which re-stages the bytes on the target's own filesystem (copy +fsync+os.replacewithin that fs) so the swap is still a real atomic rename. The original cross-device temp is always cleaned up. Non-EXDEVOSErrors propagate unchanged.Tests
tests/test_atomic_replace_symlinks.py— 3 new regression tests (11 total pass):atomic_json_writesurvives a cross-device symlink targetEXDEVOSErroris re-raised and leaves the target untouchedtest_atomic_json_write.py/test_atomic_yaml_write.py(17) still pass.ruff check(PLW1514) clean;scripts/check-windows-footguns.pyclean (binary mode +errno.EXDEV, cross-platform safe).Scope
atomic_replacechanges; no caller signatures touched. Permission-restore (_restore_file_mode) is unaffected — it still targets the returned real path.errno.EXDEVsemantics; on Windows the prior behavior is preserved unchanged (the fallback simply may not trigger for cross-volume moves).