Skip to content
This repository was archived by the owner on May 13, 2026. It is now read-only.

fix(config): atomic-rename plaintext config writes - #890

Merged
shiba4life merged 1 commit into
mainfrom
fix/atomic-writes-plaintext-config
May 5, 2026
Merged

fix(config): atomic-rename plaintext config writes#890
shiba4life merged 1 commit into
mainfrom
fix/atomic-writes-plaintext-config

Conversation

@shiba4life

Copy link
Copy Markdown
Collaborator

Summary

Plaintext config writers (save_node_config, IngestionConfig::save_to_file, write_saved_to_disk) all use bare std::fs::write, so a daemon crash mid-save leaves a partial JSON file and the next boot fails to parse it. This is the same durability problem PR #868 solved for sensitive files; this PR ports the pattern to plaintext config without routing through sensitive_io — keeping the files debuggable via cat.

This is the narrow alternative to the node_config.json wrap that PR #888 dropped from scope. PRs #885 and #887 already moved the secret-bearing files (bootstrap marker, anthropic key store, exemem creds) onto sensitive_io::write_atomic_0600, so this only deals with non-secret JSON.

Changes

  • New src/utils/fs_atomic.rs module with write_atomic(path, data, mode: Option<u32>). Mode None lets the umask apply (typically 0o644); Some(m) pins the Unix mode (used for sensitive files at 0o600).
  • sensitive_io::write_atomic_0600 becomes a thin wrapper that calls fs_atomic::write_atomic(path, data, Some(0o600)). Signature, visibility, and behavior unchanged — no callers updated.
  • save_node_config, IngestionConfig::save_to_file, and write_saved_to_disk migrate from std::fs::write to fs_atomic::write_atomic(path, data, None).

Why a separate module instead of widening sensitive_io

sensitive_io is the integration point for the keychain encryption layer (os-keychain feature). Reusing it for plaintext config would either: (a) encrypt node_config.json / ingestion_config.json — breaking cat debugging and the manual-edit workflow operators rely on, or (b) require a mode parameter that doesn't fit sensitive_io's "always use the secure path" abstraction. Splitting the atomicity primitive out cleanly separates the durability concern (whether the rename is crash-safe) from the confidentiality concern (whether the bytes are encrypted).

Test plan

  • cargo clippy --workspace --all-targets -- -D warnings — clean (the apple_import.rs build_attendee_ingestion_records macOS-local dead-code lint is a known pre-existing issue that doesn't fire in Linux CI).
  • cargo check --workspace — clean.
  • cargo test --workspace — all changed-area tests pass (51/51 in the sensitive_io / ingestion::config / fold_node::config slice). One unrelated pre-existing flake in restore_status_* reproduces on main too — it's a FOLDDB_HOME env-var race between test modules with separate per-module locks.
  • New fs_atomic tests cover: explicit mode (0o600 regression guard for the wrapper), default mode (asserts plaintext writes are NOT 0o600), no leftover <path>.tmp after success, and overwrite atomicity.

🤖 Generated with Claude Code

Today, save_node_config, IngestionConfig::save_to_file, and
write_saved_to_disk all use bare std::fs::write — a daemon crash
mid-save leaves a partial JSON file, and the next boot fails to parse
it. PR #868 fixed this for sensitive files via tmpfile + fsync +
rename; this ports the pattern to plaintext config without routing
through sensitive_io (which would also encrypt the file and break
debugging via cat).

This is the narrow alternative to the node_config.json wrap that PR
#888 dropped from scope. PRs #885 and #887 already moved the
secret-bearing files (bootstrap marker, anthropic key store, exemem
creds) to sensitive_io::write_atomic_0600, so this only deals with
non-secret JSON.

Implementation:
- Lift the atomicity primitive out of sensitive_io.rs into a new
  src/utils/fs_atomic.rs with an optional Unix mode argument.
- sensitive_io::write_atomic_0600 delegates to it with Some(0o600);
  signature unchanged so no caller updates needed.
- save_node_config, IngestionConfig::save_to_file, and
  write_saved_to_disk pass mode=None so the umask applies (typically
  0o644) — these are plaintext config files, not secrets.

New tests in fs_atomic cover both the explicit-mode path (regression
guard for the 0o600 wrapper) and the umask-default path (asserts
plaintext writes don't accidentally lock down to 0o600). Existing
sensitive_io and ingestion::config tests pass unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shiba4life
shiba4life enabled auto-merge May 5, 2026 20:56
@shiba4life
shiba4life added this pull request to the merge queue May 5, 2026
Merged via the queue into main with commit 8cc8c61 May 5, 2026
12 checks passed
@shiba4life
shiba4life deleted the fix/atomic-writes-plaintext-config branch May 5, 2026 21:07
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant