Skip to content

fix(fs): atomic writes through one sink; file_write takes the path lock - #97

Merged
M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/fs-atomic-write-sink
Oct 23, 2026
Merged

M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/fs-atomic-write-sink

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Summary

The filesystem tools wrote files in place with tokio::fs::write. That truncates the target and then writes it. A crash or kill in between leaves a truncated file. The write also follows a symlink, so a link swapped in after a tool's "refusing to write through symlink" check sent the write outside the workspace. Only update_memory_md had an atomic temp-file-and-rename write, and it was private to that tool. file_write also skipped the per-path lock that edit_file and apply_patch take, so its stale-read check and its write could interleave with another tool's edit of the same file.

  • atomic_write now lives in file_sink.rs, and every tool write goes through it. That covers OsFileSink (used by file_write and edit_file), the apply_patch write and its restore-on-failure, csv_export, and update_memory_md.
  • The temp file is opened with create_new, so it never reuses or follows an existing path. It receives the target's permissions before the rename, otherwise an edited script would lose +x. It is synced before the rename and removed if any step fails.
  • file_write now takes file_state::acquire_path_lock before its stale/partial-read checks and holds it through the write.

Related issue

None.

API or behavior changes

  • Tool writes are now atomic: a reader or a crash sees the old file or the complete new one.
  • A symlink at the target path is replaced by the new file. Before, the write followed the link. The link's target is no longer touched.
  • The rename installs a new inode. Other hard links to the file keep the old contents, and the file ends up owned by the user the host runs as. Mode bits are kept.
  • update_memory_md's staging error now reads Failed to atomically write <file>: stage temp file: …. Before, it read Failed to stage temp file for <file>: ….
  • No public API change: atomic_write and the sink are crate-private.

Validation

Commands actually run, with their outcome:

  • cargo fmt --all -- --check: rustfmt --edition 2024 on the touched files only.
  • N/A: cargo clippy --all-targets --all-features -- -D warnings was left to CI.
  • N/A: cargo build --all-targets --all-features was left to CI.
  • N/A: cargo test --all-features was left to CI. Only these tests ran locally:
    • a_write_replaces_a_symlink_instead_of_following_it before the fix failed (the link's target was overwritten: left: "new" right: "secret"); after the fix it passed;
    • a_write_keeps_the_targets_permissions passed before and after, because an in-place write keeps the inode. It pins the permission copy: with the set_permissions step removed on purpose it failed (left: 420 right: 488, i.e. 0644 against 0750), and with the step restored it passed;
    • all file_sink, apply_patch, csv_export, update_memory_md and file_write tests passed (92).

Tests

  • New filesystem/file_sink_tests.rs:
    • a_write_replaces_a_symlink_instead_of_following_it (unix): writing to a path that is a symlink to a file outside the directory leaves the outside file unchanged and leaves a regular file at the path.
    • a_write_keeps_the_targets_permissions (unix): a 0750 file keeps its mode, gets the new contents, and no temp file is left behind.
  • update_memory_md/mod_tests.rs::atomic_write_reports_staging_and_rename_failures: the staging assertion now matches the shared helper's message.

Not tested: the file_write path lock. It is the same acquire_path_lock call edit_file makes, and a race test for it would depend on timing.

Documentation

atomic_write's doc comment states the rename semantics: symlink replaced, hard links split, owner changes, mode kept.

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints
  • No secrets, tokens, or .env contents in the diff or the description

@tinysweeper

tinysweeper Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Reviewed head: 4d42cbac95222ae3493bbcbdb13e7e132f74fe9e

Verdict: Ready for maintainer review · Priority: none
Coverage: Reviewed 7 of 7 files.
Pull request purpose (title): fix(fs): atomic writ…

Diagnostics and supporting details

Full summary

Pull request purpose (title): fix(fs): atomic writes through one sink; file_write takes the path lock. Tiny Sweeper completed its review; deterministic results follow.

Review snapshot

Change surface Files Review signal Count
Production 5 Active findings 0
Tests 2 Noted findings 0
Documentation 0 Resolved findings 0
Configuration 0 Pending checks/questions 0

Completeness: Complete
Test assessment: Test coverage is assessed from changed tests and lane evidence; execution is not claimed without trusted check data.

What changed

All filesystem tool writes now go through one shared atomic writer instead of in-place truncating writes. `atomic_write` in `crates/tinytools-std/src/filesystem/file_sink.rs` opens a fresh same-directory sibling temp file with `create_new`, writes the contents, copies the existing target's permissions, syncs, then renames over the target, removing the temp file on any failure; a parentless path is rejected with `InvalidInput`. `OsFileSink::write`, `apply_patch`'s write loop and `restore_originals`, `csv_export`, and `update_memory_md` (which drops its private implementation and counter and delegates, wrapping the error with the file name) are the callers. `file_write` now acquires the per-path lock before its stale-read guard and holds it through the write. Net behavior change: symlink targets are replaced rather than followed, hard links split, ownership becomes the writing user's, and existing mode bits are preserved. The description discloses these changes and what was not tested.

Features

  • Added — Shared atomic write helper in file_sink: A single crate-private `atomic_write` stages contents in a `create_new` sibling temp file in the target directory, copies the target's permissions before the rename, syncs, renames over the target, and removes the temp file on failure — giving crash-safe replacement and preventing temp-file reuse or symlink following during staging. (crates/tinytools-std/src/filesystem/file_sink.rs pub(super) struct OsFileSink;, crates/tinytools-std/src/filesystem/file_sink_tests.rs)
  • Modified — apply_patch writes and restores atomically: Multi-file patch application writes each file via the shared helper, and the restore-on-failure path restores originals (or removes newly created files) through the same helper, so a partial multi-file patch is less likely to leave truncated content behind. (crates/tinytools-std/src/filesystem/apply_patch/mod.rs impl ApplyPatchTool {, crates/tinytools-std/src/filesystem/apply_patch/mod.rs async fn restore_originals(written: &[&FileBuffer]) -> Vec<String> {)
  • Modified — csv_export writes atomically: CSV output is written through the shared helper, so an interrupted export leaves the prior file or the complete new file rather than a truncated CSV. (crates/tinytools-std/src/filesystem/csv_export/mod.rs impl CsvExportTool {)
  • Internal refactor — update_memory_md delegates to shared helper: `update_memory_md` removes its private atomic-write implementation and sequence counter and delegates to the shared helper, wrapping errors with the target file name; the staging error text changes to `Failed to atomically write <file>: stage temp file: …`. (crates/tinytools-std/src/filesystem/update_memory_md/mod.rs use async_trait::async_trait;, crates/tinytools-std/src/filesystem/update_memory_md/mod.rs async fn acquire_cross_process_write_lock(workspace_dir: &Path) -> anyhow::Resul, crates/tinytools-std/src/filesystem/update_memory_md/mod.rs const ALLOWED_FILES: &[&str] = &["MEMORY.md", "SKILL.md"];)
  • Modified — file_write takes the per-path lock: `file_write` now acquires `file_state::acquire_path_lock` for the resolved target before its stale/partial-read checks and holds it through the write, preventing interleaving with another tool's edit of the same path. (crates/tinytools-std/src/filesystem/file_write/mod.rs impl FileWriteTool {)

Tests

  • modified test — The update_memory_md staging-and-rename failure test now matches the shared helper's `stage temp file` error text rather than the old `Failed to stage temp file` wording.: Correctly retargeted to the delegated helper's message. The previous review's tests lane noted a gap in exercising a failure of the apply_patch restore write; the current tests lane was answered with no reportable defects. (crates/tinytools-std/src/filesystem/update_memory_md/mod_tests.rs async fn atomic_write_reports_staging_and_rename_failures() {, crates/tinytools-std/src/filesystem/apply_patch/mod.rs async fn restore_originals(written: &[&FileBuffer]) -> Vec<String> {)

Findings

No active actionable findings.

Before merge

None.

How this fits together

flowchart LR
  n0["restore_originals<br/>changed"]:::changed
  n1["RefusingSink<br/>changed"]:::changed
  n2["FileWriteTool<br/>changed"]:::changed
  n3["acquire_cross_process_write_lock<br/>changed"]:::changed
  n4["execute_in_context"]:::impacted
  n5["write"]:::impacted
  n6["ToolResult"]:::impacted
  n7["FileSink"]:::impacted
  n8["execute_in_context"]:::impacted
  n9["execute_in_context"]:::impacted
  n0 -->|calls| n5
  n1 -->|implements| n7
  n2 -->|uses| n7
  n3 -->|calls| n5
  n4 -->|calls| n0
  n4 -->|calls| n5
  n4 -->|uses| n6
  n8 -->|calls| n5
  n8 -->|uses| n6
  n9 -->|calls| n5
  n9 -->|uses| n6
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading
Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Critique findings were capped at Likely confidence because retrieval was degraded: Count temp files only, not the directory fixture. (1 model finding retained at no more than Likely confidence because retrieval was degraded) _The code index is behind this pull request (indexed at `28ea0933b938`), so retrieved context may be out of date._

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: This adds only tests for the OS file sink's atomic write path; it introduces no production code, no new network or subprocess calls, and no secrets. The assertions match `atomic_write`'s documented behaviour (temp file in the same directory, permission copy, rename over symlink, InvalidInput for a parentless path). Nothing reportable. _The code index is behind this pull request (indexed at `28ea0933b938`), so retrieved context may be out of date._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The tests lane was answered and found the new tests pin the concrete behaviors the atomic write introduces — symlink replacement, permission preservation across the rename, no temp-file litter after failed stage or rename, and the error text callers match on — with no reportable defect.
  • Lane summary: The new `file_sink_tests.rs` pins the concrete behaviours the atomic write introduces — symlink replacement rather than following, permission preservation across the rename, no temp-file litter after a failed stage or failed rename, and the error text callers match on — with assertions that fail if the implementation regresses. Behavioural change is real (apply_patch, csv_export, file_write and update_memory_md now route through `atomic_write`), and the existing tool tests plus these new ones exercise the failure paths. No reportable defect found. _The code index is behind this pull request (indexed at `28ea0933b938`), so retrieved context may be out of date._

commits

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The commits lane was answered and reported no sensitive content in committed files.
  • Lane summary: No sensitive content in committed files. _The code index is behind this pull request (indexed at `28ea0933b938`), so retrieved context may be out of date._

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The description lane was answered and found the tests consistent with the repo's testing rules (sibling `_tests.rs` file, descriptive names, failure paths covered) and the assertions matching `atomic_write`'s implementation and error strings.
  • Lane summary: The new test file `file_sink_tests.rs` exercises the shared atomic write: symlink replacement, permission preservation, and failure cleanup, and its assertions match `atomic_write`'s implementation and error strings. The tests are consistent with the repo's testing rules (sibling `_tests.rs` file, descriptive names, failure paths covered), and I found nothing that would fail or misbehave. _The code index is behind this pull request (indexed at `28ea0933b938`), so retrieved context may be out of date._
Evidence and run details
  • Models: ladder/vectors-oai3, deepseek-v4.1-flash
  • Spend: $0.000614
  • Tokens: 197235 input · 4741 output · 35328 cached · 529 embedding
  • Updated: 2026-10-22T23:30:37Z
Head State Pass summary
5ea28337ec39 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-10-22T23:19:33Z)
4d42cbac9522 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-10-22T23:30:37Z)

tinysweeper 0.1.2

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: edc1f6a1-1ff1-471f-8e85-010850ed8646
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

Diagnostics

Total spend: $0.0006

@M3gA-Mind
M3gA-Mind merged commit a95c48f into tinyhumansai:main Oct 23, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant