fix(credentials): write atomically through symlinks - #685
Merged
Conversation
Atomic rename replaces a destination symlink itself, leaving the credential referent stale. Resolve final symlink chains before staging so relative, absolute, dangling, and Windows volume-rooted targets keep write-through semantics while preserving whole-file replacement, permissions, and cycle failure behavior. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
cursor Bot
pushed a commit
that referenced
this pull request
Aug 30, 2026
Lands cursor/eval-frontier-4ffc on the 281 cut: OpenCode --dir / dsh harnesses, in-house fixtures, frontier SVGs, list-price plot, hardlink graff-pinned + detached learn init, scripted-REPL usage footer, and the #685 atomic credential write already on main. Conflicts in graff-evals/README.md and harnesses.json kept both Pi-on-SuperGrok and the new in-house / OpenCode / dsh entries. Do not tag. Do not merge to main from this commit.
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.
What changed
..; retain correct absolute, dangling, chained, and Windows drive/UNC-rooted semantics...traversal, cycles, permissions, and Windows volume roots.Why
Problem / failure mode
createFileAtomic(..., replace = true)renames over the named directory entry. When that entry is a symlink such assettings.json -> real.json, it replaces the symlink itself and leavesreal.jsonstale. Credential readers following the link then continue to observe old tokens even though the save reported success.Reason for this approach
Resolving only the final symlink chain before staging lets the existing atomic writer place its temporary file beside the actual referent and rename there. Readers still see either the complete old referent or the complete new referent, while the user's symlink remains intact. Centralizing this in
credential_store.replaceFileapplies the policy consistently to interactive login, silent refresh/subagent recovery, key storage, MCP OAuth/cache, fallback settings, and model/router/pricing caches used by the TUI runtime.Constraints and trade-offs
This is issue #405 item 1 only. It intentionally does not add the separate
keys_clilive-platform coverage or directory fsync requested by items 2–3. Like other portable path-based filesystem operations, symlink resolution is a snapshot and is not linearizable against another process concurrently retargeting links; Zig 0.16 exposes no portable atomic follow-final-link-and-rename primitive. The existing crash/process-concurrency whole-file guarantee remains unchanged.Rejected alternatives
readLinkfollowed bycreateFileAtomic(the approach in PR fix: stream foreground bash and close the small follow-up batch #511) misanchors relative targets, misses chains, and does not define dangling, cycle,.., or Windows volume-rooted behavior.Verification
zig build test: 1,758 passed, 1 skipped (1,759 total)zig build tui-test: 459/459 passedzig build -Dtarget=x86_64-linux: passedzig build -Dtarget=x86_64-windows: passed#405filter: 54/54 passedAddresses #405 item 1 only; items 2–3 remain open.