Skip to content

feat: desktop FUSE journal durability and at-rest safety#533

Merged
FSM1 merged 23 commits into
mainfrom
feat/desktop-fuse-durability-at-rest-safety
Jun 20, 2026
Merged

feat: desktop FUSE journal durability and at-rest safety#533
FSM1 merged 23 commits into
mainfrom
feat/desktop-fuse-durability-at-rest-safety

Conversation

@FSM1

@FSM1 FSM1 commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Phase 52: Desktop FUSE Durability & At-Rest Safety

Hardens the desktop FUSE write journal against data loss, host-path leakage, unbounded growth, and at-rest plaintext exposure. Part of the v1.1 hardening block (Phases 50-55), closing tracked todo #9.

What changed

52-01 — at-rest safety basics (D-05, D-06)

  • Extend sanitize_error path-scrub to /var, /tmp, /private, and Windows drive-letter X:\Users\ so host paths never leak into tray/notification copy.
  • Replace three swallowed let _ = journal.remove sites with if let Err … warn logging so a failed removal cannot silently cause a later double-publish.

52-02 — ciphertext sidecar + encrypted-name journal shape (D-01, D-04)

  • Replace the in-JSON ciphertext_b64 blob with a 0600 <id>.bin sidecar referenced by sidecar_path + sidecar_sha256, eliminating the multi-GB serde_json allocation on the FS thread.
  • Rename plaintext filename/name to ECIES-encrypted filename_encrypted_hex / name_encrypted_hex (with #[serde(alias)] + #[serde(default)] one-time legacy replay compat).

52-03 — write-path size cap, filename encryption, off-thread durable-ack (D-01, D-04)

  • Reject files over MAX_JOURNAL_PAYLOAD_BYTES (2 GiB) in release() with EIO before any key generation.
  • ECIES-encrypt the filename/dir-name at write time (a failed name encrypt fails the write).
  • Move the heavy sidecar write + fsync off the FUSE callback thread; the callback blocks on a bounded recv_timeout before reply.ok() so the Phase-43 durable-ack contract (no false-ack) is preserved.

52-04 — replay sidecar read, name decryption, timeout, concurrent mount (D-03, D-04)

  • Replay reads ciphertext from the <id>.bin sidecar and verifies its SHA-256 before re-upload; a missing/empty path or hash mismatch retains the entry instead of uploading absent/corrupt ciphertext.
  • Decrypt names transiently with passthrough-once legacy compat (never re-persisted).
  • Bound every replay entry's network ops with tokio::time::timeout (mkdir 3x, upload 18x of NETWORK_TIMEOUT); a hung entry routes through record_failure.
  • Run replay concurrently via rt.spawn on both the Unix and Windows mount paths so the mount returns immediately.

52-05 — journal retention purge + Failed-entry GC (D-02)

  • WriteQueue::purge_vault removes every .json + .bin for one vault (all statuses), closing the cross-vault retention leak in the shared journal dir; wired into logout() before clear_keys().
  • WriteQueue::gc_failed_entries ages out parked Failed entries, trims oldest-first to a 500 MiB budget (counting the .bin), and cleans .bin orphans; run at mount, best-effort, never blocks the mount.

Verification

  • cargo test -p cipherbox-fuse — 64/64 green
  • cargo test -p cipherbox-sdk — 57/57 green (54 baseline + 3 new GC/purge tests: purge_vault_removes_all, gc_purges_old_failed, gc_purges_to_size_budget)
  • cargo check -p cipherbox-desktop --features fuse — clean
  • VERIFICATION: PASS (16/16 must-haves) · SECURITY: SECURED (16/16 threats, T-52-15/T-52-16 closed) · VALIDATION: compliant (12/12, 0 gaps)
  • Phase 51 hardening confirmed intact — zero zeroize/signature/sign lines removed; fail-closed signature handling and key zeroization untouched.

Notes

  • The --features winfsp check fails only in upstream windows_core crates on macOS (IMarshal/marshaler); validated locally under the fuse feature and relying on CI's Windows runner for winfsp.
  • The SDK E2E gate (TypeScript client to API IPNS round-trip) was not triggered — this phase is entirely Rust (FUSE journal/replay + the Rust cipherbox-sdk queue) and touches no TS publish/resolve, CAS sequencing, or key-lifecycle code.
  • Commits in this branch are unsigned (1Password unavailable for this run); may need re-signing before merge if branch protection requires signatures.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Journal now stores large encrypted payloads in secure sidecars and keeps only encrypted file/folder names in the journal, with a payload size cap.
    • Logout purges the current vault’s journal data; mount startup performs best-effort garbage collection for failed/orphaned items.
  • Bug Fixes
    • Replay validates sidecar integrity, decrypts names transiently with a one-time legacy fallback, and uses bounded network timeouts to prevent hangs.
    • Replay runs concurrently so mount isn’t blocked.
    • Journal cleanup/removal failures are now logged instead of silently ignored.
  • Chores
    • SDK release version bumped to 0.7.0.

FSM1 and others added 17 commits June 19, 2026 20:43
Entire-Checkpoint: 29e8f0ad16bd
Entire-Checkpoint: c5d1a25a475d
…ability-at-rest-safety

# Conflicts:
#	.planning/STATE.md
…ogging

Phase 52 Plan 01 (D-05, D-06):
- Extend sanitize_error path scrub to /var, /tmp, /private, and Windows
  drive-letter X:\Users\ so host paths never leak into tray/notification copy
- Replace three swallowed let _ = journal.remove sites with if let Err warn
  logging so a failed removal cannot silently cause a later double-publish
- Add sanitize_error_extended_paths and remove_failure_is_logged tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 52 Plan 02 (D-01, D-04):
- Replace in-JSON ciphertext_b64 with a 0600 <id>.bin sidecar referenced by
  sidecar_path + sidecar_sha256, eliminating the multi-GB serde_json alloc
- Rename plaintext filename/name to ECIES-encrypted filename_encrypted_hex /
  name_encrypted_hex with serde alias for one-time legacy replay compat
- Add put_with_sidecar streaming writer and sidecar-aware remove
- Add MAX_JOURNAL_PAYLOAD_BYTES, JOURNAL_GC_MAX_AGE_DAYS, JOURNAL_GC_MAX_SIZE_BYTES
- sidecar_path + sidecar_sha256 default so pre-Phase-52 inline entries still load

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rable-ack

Phase 52 Plan 03 (D-01, D-04):
- Reject files over MAX_JOURNAL_PAYLOAD_BYTES in release() with EIO before any
  key generation, so the OOM/stall path holds no sensitive material
- Build UploadFile/Mkdir journal entries with sidecar_path + sidecar_sha256 and
  ECIES-encrypted filename_encrypted_hex / name_encrypted_hex (failed name
  encrypt fails the write)
- Move the heavy ciphertext-sidecar write + fsync off the FUSE callback thread
  onto an OS thread, callback blocks on a bounded recv_timeout before reply.ok()
  so the Phase-43 durable-ack contract (no false-ack) is preserved
- Make NETWORK_TIMEOUT pub(crate) and add sha2 to the fuse crate

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oncurrent mount

Phase 52 Plan 04 (D-03, D-04):
- Replay reads ciphertext from the <id>.bin sidecar and verifies its SHA-256
  before re-upload; a missing/empty path or hash mismatch returns Err and
  retains the entry instead of uploading absent or corrupt ciphertext
- Decrypt filename_encrypted_hex / name_encrypted_hex transiently via
  decrypt_journal_name with passthrough-once legacy compat, never re-persisted
- Bound every replay entry's network ops with tokio::time::timeout (mkdir 3x,
  upload 18x of NETWORK_TIMEOUT); a hung entry routes through record_failure
- Run replay concurrently via rt.spawn on both the Unix and Windows mount paths,
  cloning key bytes before the Zeroizing move so the FS owns the sole copy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 52 Plan 05 (D-02):
- Add WriteQueue::purge_vault to remove every .json + .bin for one vault
  (all statuses) via the sidecar-aware remove, closing the cross-vault
  retention leak in the shared journal dir
- Add WriteQueue::gc_failed_entries: age-purge Failed entries, trim oldest-first
  to a size budget counting the .bin sidecar, and clean .bin orphans with no
  matching .json; best-effort, only Failed entries, never panics
- Wire purge_vault into logout() before clear_keys (reads root_ipns_name which
  clear_keys zeroes); document the future switch_account/delete_account hook
- Run gc_failed_entries at mount with JOURNAL_GC_MAX_AGE_DAYS /
  JOURNAL_GC_MAX_SIZE_BYTES, non-fatal so it never blocks the mount

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Collapse the three identical warn-and-passthrough blocks in decrypt_journal_name
into one closure, and merge the duplicated Unix/Windows path-scrub branches in
regex_replace_paths into a single is_path_start check with one skip loop.
Behavior-identical; cipherbox-fuse 64/64 and cipherbox-sdk 57/57 still green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CodeRabbit flagged that the winfsp mount path was missing the D-02
gc_failed_entries sweep present on the Unix path, leaking parked Failed
entries and orphaned .bin sidecars across mounts on Windows. Add the same
non-fatal mount-time GC before the concurrent replay spawn. Also correct the
STATE.md Current focus to point at Phase 52.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@FSM1, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 9 minutes and 30 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7076e079-576a-4865-a835-3d64451d63eb

📥 Commits

Reviewing files that changed from the base of the PR and between 9dba0e0 and c90394a.

📒 Files selected for processing (3)
  • .planning/ROADMAP.md
  • .planning/STATE.md
  • release-please-config.json

Walkthrough

Phase 52 hardens the FUSE write-journal and replay path against HARD-03 requirements. Ciphertext moves from inline JSON to 0600 <id>.bin sidecar files; journaled filenames and directory names are ECIES-encrypted at rest. A payload size cap is enforced before encryption. Durable-ack is moved off the FUSE callback thread using a bounded recv_timeout. Replay operations are wrapped with tokio::time::timeout and run concurrently with mount. GC and vault-purge hooks are wired into mount startup and logout. The SDK version advances to 0.7.0.

Changes

Phase 52: Desktop FUSE Durability & At-Rest Safety

Layer / File(s) Summary
SDK journal schema: new fields, constants, Cargo deps, re-exports
crates/sdk/src/queue.rs, crates/sdk/src/lib.rs, crates/sdk/Cargo.toml, release-please-config.json
Replaces ciphertext_b64/filename in JournalOp::UploadFile with sidecar_path, sidecar_sha256, and filename_encrypted_hex; replaces name in MkdirPublish with name_encrypted_hex; adds serde aliases for legacy compat. Introduces MAX_JOURNAL_PAYLOAD_BYTES, JOURNAL_GC_MAX_AGE_DAYS, and JOURNAL_GC_MAX_SIZE_BYTES constants. Adds sha2 and ecies workspace deps. Bumps SDK to 0.7.0.
SDK WriteQueue sidecar API: put_with_sidecar, remove, purge_vault, gc_failed_entries
crates/sdk/src/queue.rs
Adds sidecar_path_for, put_with_sidecar (streams ciphertext to 0600 .bin with fsync barriers; removes .bin on JSON write failure), sidecar-aware remove (deletes both files idempotently), purge_vault (removes all entries for a vault), gc_failed_entries (three-pass: age purge → size-budget oldest-first trim → orphan .bin cleanup), and now_ms() helper.
SDK queue tests
crates/sdk/src/queue.rs
Updates all existing test fixtures to sidecar/encrypted field shapes. Adds Phase 52 tests for legacy plaintext serde alias compat, absence of plaintext filenames and ciphertext_b64 in serialized JSON, ECIES round-trip, put_with_sidecar .bin-only storage with stale cleanup, purge_vault, and gc_failed_entries age/size/orphan behaviors.
Error path sanitization extension
crates/sdk/src/sync.rs
Extends regex_replace_paths to scrub /var/, /tmp/, /private/, and Windows X:\Users\... paths. Adds sanitize_error_extended_paths test covering new prefixes and boundary stopping.
FUSE write-side: size cap, sidecar fields, ECIES name encryption, removal logging
crates/fuse/src/journal_helpers.rs, crates/fuse/src/write_ops.rs, crates/fuse/Cargo.toml
In journal_helpers.rs: enforces MAX_JOURNAL_PAYLOAD_BYTES before encryption; populates sidecar ID, SHA-256, and ECIES-encrypted filename_encrypted_hex/name_encrypted_hex into journal ops. Makes NETWORK_TIMEOUT pub(crate). In write_ops.rs: replaces silently discarded mkdir journal removal result with log::warn!. Updates helper tests to assert sidecar fields and size cap behavior. Adds sha2 dependency.
FUSE durable-ack: off-thread put_with_sidecar with bounded recv_timeout
crates/fuse/src/read_ops.rs
In handle_release: spawns a std::thread to run put_with_sidecar; the FUSE callback thread blocks on recv_timeout(NETWORK_TIMEOUT * 18). Timeout or thread error returns EIO with no in-memory mutations applied. Simplifies the build_result wrapper.
FUSE replay hardening: sidecar verify, name decryption, per-entry timeouts, tests
crates/fuse/src/lib.rs
Adds decrypt_journal_name (ECIES hex-decode + passthrough-once legacy fallback). Updates replay_upload_entry to read ciphertext from sidecar_path and verify SHA-256; updates replay_mkdir_entry to accept name_encrypted_hex. Wraps both replay calls in tokio::time::timeout (mkdir 3×, upload 18×) routing to record_failure. Updates test fixtures to sidecar shape. Adds replay_reuploads_ciphertext, remove_failure_is_logged, replay_entry_timeout, and decrypt_journal_name_round_trip_and_legacy_compat tests.
Desktop integration: GC before mount, background replay, logout vault purge
apps/desktop/src-tauri/src/fuse/mod.rs, apps/desktop/src-tauri/src/fuse/windows/mod.rs, apps/desktop/src-tauri/src/commands/auth.rs
Calls gc_failed_entries(...) synchronously before replay (non-fatal, logs result). Replaces awaited replay_for_vault(...) with rt.spawn(...) so mount returns immediately. In logout: adds WriteQueue::purge_vault(&ipns) before state.clear_keys() under cfg(any(feature = "fuse", feature = "winfsp")); logs and continues on error.

Phase 52 Planning Documentation

Layer / File(s) Summary
Context, research, discussion, and patterns
.planning/phases/52-desktop-fuse-durability-at-rest-safety/52-CONTEXT.md, 52-RESEARCH.md, 52-DISCUSSION-LOG.md, 52-PATTERNS.md
Adds Phase 52 scope boundary, D-01–D-06 research findings, audit-trail decision log, and cross-repo pattern map for all affected files.
Execution plans 01–05
.planning/phases/52-desktop-fuse-durability-at-rest-safety/52-0[1-5]-PLAN.md
Five wave-ordered TDD plans covering path scrubbing + removal logging, journal schema migration, write-side sidecar durability, replay hardening, and GC/purge lifecycle. Each includes must-haves, threat model, and verification checklists.
Summaries, security, validation, and verification
.planning/phases/52-desktop-fuse-durability-at-rest-safety/52-0[1-5]-SUMMARY.md, 52-SECURITY.md, 52-VALIDATION.md, 52-VERIFICATION.md
Plan summaries with what-was-built, reconciliations, and test results. Security audit asserting 16/16 threat mitigations present. Validation strategy and verification report with 16/16 observable truths confirmed.
Roadmap and project state
.planning/ROADMAP.md, .planning/STATE.md
Replaces Phase 52 placeholder with wave breakdown; updates state to Phase 52 complete/shipping.

Sequence Diagram(s)

sequenceDiagram
    participant App as Desktop App
    participant Auth as auth.rs logout
    participant FuseMod as fuse/mod.rs mount
    participant WQ as WriteQueue
    participant RT as rt.spawn (replay task)
    participant FUSEFS as CipherBoxFS
    participant RO as read_ops handle_release
    participant ST as std::thread (sidecar writer)

    rect rgba(0, 100, 200, 0.5)
        note over FuseMod,FUSEFS: Mount startup
        FuseMod->>WQ: gc_failed_entries(age, size)
        WQ-->>FuseMod: removed count
        FuseMod->>RT: rt.spawn(replay_for_vault)
        FuseMod->>FUSEFS: construct + mount
        RT-->>RT: sidecar verify + ECIES decrypt + timeout(18× / 3×)
    end

    rect rgba(0, 150, 100, 0.5)
        note over RO,ST: File release (durable-ack)
        RO->>ST: std::thread::spawn(put_with_sidecar)
        ST->>ST: write id.bin (0600) + fsync
        ST->>ST: write id.json
        ST-->>RO: mpsc send Ok/Err
        alt within NETWORK_TIMEOUT * 18
            RO->>FUSEFS: reply.ok()
        else timeout
            RO->>FUSEFS: reply.error(EIO)
        end
    end

    rect rgba(200, 80, 0, 0.5)
        note over Auth,WQ: Logout
        Auth->>Auth: unmount + keychain delete
        Auth->>WQ: purge_vault(ipns)
        WQ-->>Auth: count / warn on err
        Auth->>Auth: state.clear_keys()
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • FSM1/cipher-box#487: Phase 43 established the existing WriteQueue journal schema and replay_for_vault handling in crates/sdk/src/queue.rs and crates/fuse/src/lib.rs — Phase 52 directly extends those same fields and replay paths with the sidecar model.
  • FSM1/cipher-box#491: Modifies crates/fuse/src/journal_helpers.rs and replay control-flow in crates/fuse/src/lib.rs, the same two files Phase 52 reworks for sidecar fields and ECIES name encryption.
  • FSM1/cipher-box#493: Modifies the desktop FUSE mount/replay pipeline in apps/desktop/src-tauri/src/fuse/mod.rs and crates/fuse/src/lib.rs replay logic, directly related to Phase 52's concurrent replay and mount hardening work.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: desktop FUSE journal durability and at-rest safety' accurately and clearly summarizes the main objective of the changeset, which implements Phase 52 hardening for the FUSE write journal with durability enhancements and security improvements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/desktop-fuse-durability-at-rest-safety

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added release:cipherbox-fuse:feat Minor version bump (new feature) for cipherbox-fuse release:cipherbox-sdk:feat Minor version bump (new feature) for cipherbox-sdk release:desktop:feat Minor version bump (new feature) for desktop labels Jun 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Release Preview

Package Bump Label Source
cipherbox-fuse minor release:cipherbox-fuse:feat Direct (feat commit)
cipherbox-sdk minor release:cipherbox-sdk:feat Direct (feat commit)
desktop minor release:desktop:feat Direct (feat commit)

@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown

Greptile Summary

This PR (Phase 52) hardens the desktop FUSE write journal with five focused hardening sub-phases: sidecar-based ciphertext storage (eliminating multi-GB serde_json allocations), ECIES encryption of filenames/dir-names at rest, off-thread durable-ack writes with a bounded timeout, replay sidecar integrity verification + concurrent network timeouts, and a gc_failed_entries / purge_vault GC+retention layer.

  • 52-01/52-05: sanitize_error extended to scrub /var, /tmp, /private, and Windows X:\\Users\\ paths; swallowed let _ = journal.remove sites replaced with if let Err … log::warn; purge_vault wired into logout(); gc_failed_entries called at mount (both Unix and Windows).
  • 52-02/52-03: JournalOp::UploadFile replaces inline ciphertext_b64 with a 0600 <id>.bin sidecar; filename/name replaced by filename_encrypted_hex/name_encrypted_hex; the sidecar fsync is moved off the FUSE callback thread with a 180-second bounded recv_timeout; files over 2 GiB are rejected before key generation.
  • 52-04: Replay verifies sidecar SHA-256 before re-upload, decrypts names transiently via decrypt_journal_name, bounds each entry's network ops with tokio::time::timeout, and spawns concurrently so mount returns immediately; the previous outside-diff P1 about legacy-inline ciphertext loss is resolved by migrate_legacy_inline in record_failure.

Confidence Score: 5/5

Safe to merge — no regressions found in the durable-ack contract, the sidecar write path, replay correctness, or the GC logic; all previously-reviewed issues are resolved.

The sidecar write path, JSON-first remove ordering, purge_vault best-effort loop, gc_failed_entries 3-pass logic, migrate_legacy_inline in record_failure, and the concurrent replay spawn were all reviewed and found correct. The two observations are minor logging and coverage gaps that do not affect data safety or correctness.

No files require special attention; the two suggestions are in crates/sdk/src/queue.rs (misleading log on purged entry) and crates/sdk/src/sync.rs (missing all-uppercase Windows path variant).

Important Files Changed

Filename Overview
crates/sdk/src/queue.rs Core journal queue — adds put_with_sidecar, sidecar_path_for, remove (json-first ordering), purge_vault (best-effort loop), gc_failed_entries (3-pass GC), migrate_legacy_inline (legacy-ciphertext migration in record_failure), and record_failure guard for purged entries. All previously-reviewed P1s are resolved.
crates/fuse/src/lib.rs Replay path: MkdirPublish and UploadFile ops wrapped with tokio::time::timeout (3x and 18x NETWORK_TIMEOUT); sidecar path re-derived from journal ID to prevent path-traversal; replay_for_vault spawned concurrently; journal.remove failures logged instead of swallowed; decrypt_journal_name added with ECIES-floor-based legacy passthrough.
crates/fuse/src/read_ops.rs release() path refactored: sidecar write + fsync moved off FUSE callback thread via OS thread spawn; callback blocks on bounded recv_timeout(NETWORK_TIMEOUT * 18); ciphertext moved into writer thread and returned through channel for live upload reuse. CR-04 durable-ack contract preserved.
crates/fuse/src/journal_helpers.rs build_upload_journal_entry: size cap guard added before read_all(); sidecar_path + sidecar_sha256 computed at construction time; filename and dir-name now ECIES-encrypted. build_mkdir_journal_entry likewise encrypts the dir name.
crates/sdk/src/sync.rs sanitize_error path-scrubbing extended to /var/, /tmp/, /private/, and Windows X:\Users\ / X:\users. Minor gap: all-uppercase \USERS\ variant not covered. Tests added for all new branches.
apps/desktop/src-tauri/src/commands/auth.rs logout() now calls WriteQueue::purge_vault before clear_keys(); correct ordering; Err handled gracefully with warn log.
apps/desktop/src-tauri/src/fuse/mod.rs Unix mount path: gc_failed_entries added before replay; replay now spawned via rt.spawn (concurrent, non-blocking). Key clones made before move into CipherBoxFS.
apps/desktop/src-tauri/src/fuse/windows/mod.rs Windows mount path: same gc_failed_entries + concurrent replay spawn pattern as Unix. GC runs synchronously before FS construction; replay spawned before CipherBoxFS is built.
crates/fuse/src/write_ops.rs mkdir journal remove after parent-publish now logs warning on failure instead of silently swallowing it. Minor focused change.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant FUSE as FUSE callback thread
    participant BG as OS background thread
    participant DISK as Disk (journal dir)
    participant RT as Tokio runtime
    participant NET as Network (IPFS/IPNS)

    Note over FUSE: release() called (file close)
    FUSE->>FUSE: size cap check (2 GiB max)
    FUSE->>FUSE: encrypt plaintext to ciphertext
    FUSE->>FUSE: ecies wrap_key filename to filename_encrypted_hex
    FUSE->>FUSE: build JournalEntry (sidecar_path + sidecar_sha256)
    FUSE->>BG: spawn OS thread put_with_sidecar
    BG->>DISK: write id.bin (0600, chunked, fsync)
    BG->>DISK: write id.json (fsync + parent dir sync)
    BG-->>FUSE: send Ok via channel
    FUSE->>FUSE: recv_timeout 180s durable ack
    FUSE-->>FUSE: reply.ok()
    FUSE->>RT: spawn upload task
    RT->>NET: upload ciphertext to CID
    RT->>NET: publish IPNS records
    RT->>DISK: journal.remove id

    Note over RT: On next mount replay path
    RT->>DISK: gc_failed_entries age size orphan purge
    RT->>DISK: load_all_for_vault entries
    RT->>DISK: read id.bin verify SHA-256
    RT->>DISK: decrypt_journal_name filename_encrypted_hex
    RT->>NET: re-upload ciphertext timeout 180s
    RT->>DISK: journal.remove id on success
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant FUSE as FUSE callback thread
    participant BG as OS background thread
    participant DISK as Disk (journal dir)
    participant RT as Tokio runtime
    participant NET as Network (IPFS/IPNS)

    Note over FUSE: release() called (file close)
    FUSE->>FUSE: size cap check (2 GiB max)
    FUSE->>FUSE: encrypt plaintext to ciphertext
    FUSE->>FUSE: ecies wrap_key filename to filename_encrypted_hex
    FUSE->>FUSE: build JournalEntry (sidecar_path + sidecar_sha256)
    FUSE->>BG: spawn OS thread put_with_sidecar
    BG->>DISK: write id.bin (0600, chunked, fsync)
    BG->>DISK: write id.json (fsync + parent dir sync)
    BG-->>FUSE: send Ok via channel
    FUSE->>FUSE: recv_timeout 180s durable ack
    FUSE-->>FUSE: reply.ok()
    FUSE->>RT: spawn upload task
    RT->>NET: upload ciphertext to CID
    RT->>NET: publish IPNS records
    RT->>DISK: journal.remove id

    Note over RT: On next mount replay path
    RT->>DISK: gc_failed_entries age size orphan purge
    RT->>DISK: load_all_for_vault entries
    RT->>DISK: read id.bin verify SHA-256
    RT->>DISK: decrypt_journal_name filename_encrypted_hex
    RT->>NET: re-upload ciphertext timeout 180s
    RT->>DISK: journal.remove id on success
Loading

Reviews (6): Last reviewed commit: "Merge branch 'main' into feat/desktop-fu..." | Re-trigger Greptile

Comment thread crates/sdk/src/queue.rs
Comment thread crates/sdk/src/queue.rs

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

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/fuse/src/journal_helpers.rs`:
- Around line 135-141: The file size validation check happens after read_all()
has already loaded the entire file into memory, which defeats the purpose of the
guard and can still cause memory allocation for oversized files. Retrieve the
file size from the handle's metadata before calling read_all(), check if the
file_size exceeds MAX_JOURNAL_PAYLOAD_BYTES, and only proceed to read_all() if
the file is within the size limit, otherwise return an error immediately.

In `@crates/fuse/src/lib.rs`:
- Around line 2241-2253: The error messages in this block include the raw
sidecar_path using {:?} formatting, which can leak sensitive system paths
through logs and error recording. Remove the sidecar_path parameter from both
error messages: the map_err closure when reading the ciphertext sidecar and the
error returned for the hash mismatch check. Retain the descriptive error
information (like the actual error details from reading and the hash values for
the mismatch case) but exclude the path itself to prevent exposing system
directories.

In `@crates/fuse/src/read_ops.rs`:
- Around line 838-847: The error handling in the recv_timeout match statement
(specifically the Timeout and Disconnected cases) returns errors immediately
without ensuring the detached writer thread has definitely finished. Since the
spawned thread can still be executing put_with_sidecar and complete a journal
commit after we return an error, this creates a race condition where a late
.json commit replays the write on the next mount despite reporting failure to
the OS. Before returning an error in these failure cases, either make the
visible journal commit cancellable or staged so it doesn't proceed if the close
fails, or add logic to wait for a definitive outcome from the writer thread that
confirms it has truly stopped before returning the error response.
- Around line 825-832: The code is unnecessarily cloning result.ciphertext which
can duplicate up to 2 GiB of data. Instead of cloning put_ciphertext for the
spawned thread, change the channel type from Result<(), String> to include the
ciphertext in the return value, then move result.ciphertext directly into the
spawned thread (not clone it). After put_journal.put_with_sidecar completes,
have the spawned thread send back both the durability result and the ciphertext
through the channel so it can be reused for the later upload thread without
duplication.

In `@crates/sdk/src/queue.rs`:
- Around line 403-409: The purge_vault method removes entries, but detached
upload workers that hold a journal_entry_snapshot can still call record_failure
later, which recreates the purged entry without its sidecar files. To fix this,
modify the record_failure method to check whether the entry file still exists
before attempting to put the entry back into the queue; if the file was already
removed by purge_vault, record_failure should become a no-op instead of
recreating the entry. This prevents stale workers from resurrecting purged
entries with incomplete data.
- Around line 49-67: Add a new compat field called `legacy_ciphertext_b64` to
capture the old inline ciphertext during deserialization of pre-Phase-52
entries. Mark this field with both `#[serde(default)]` to allow deserialization
of entries without it, and `#[serde(skip_serializing)]` to prevent it from being
written back to JSON on serialization. This ensures that when old entries with
inline `ciphertext_b64` are loaded, the ciphertext is preserved in memory for
one-time passthrough replay before the entry is discarded, rather than being
silently dropped during deserialization.
- Around line 273-276: The put_with_sidecar method writes the sidecar file to
self.sidecar_path_for(entry.id) but does not validate that entry.sidecar_path
matches the actual persisted path before committing the JournalEntry to JSON.
Before persisting the entry, validate that entry.sidecar_path is consistent with
the computed bin_path from self.sidecar_path_for(id), or update the entry to
reflect the actual sidecar path that was written. This ensures that during
replay, the persisted entry points to the correct sidecar file location that was
just fsynced.
- Around line 321-342: The remove method currently removes the binary sidecar
file (bin_path) before removing the JSON file (json_path). This ordering creates
a window where a crash could leave an orphaned .json entry with no payload,
causing issues on replay. Reverse the order by moving the
std::fs::remove_file(&json_path) call and its fsync logic to execute before the
sidecar removal, so the JSON file is removed and synced first. This ensures that
if a crash occurs, only an orphaned .bin file remains, which is safe since
garbage collection can clean it up without triggering replay.
- Around line 293-301: The code in the journal sidecar write section does not
clean up partial binary files when write_all or sync_all operations fail,
leaving orphaned `.bin` files until garbage collection runs. Wrap the entire
bin_file write loop and sync_all calls in error handling that deletes the
sidecar file when either operation fails. Ensure that when map_err is called on
write_all for the chunk writing loop or on sync_all, the code attempts to remove
the bin_file before returning the error, so partial sidecars are cleaned up
immediately rather than waiting for a later GC pass.

In `@crates/sdk/src/sync.rs`:
- Around line 344-394: The test function sanitize_error_extended_paths does not
cover case-insensitive Windows paths, specifically lowercase drive letters and
lowercase directory names. Add two additional assert_eq! test cases within the
sanitize_error_extended_paths function after the existing Windows drive-letter
path tests: one testing a lowercase drive letter followed by an uppercase path
component (e.g., c:\Users\alice\file), and another testing an uppercase drive
letter followed by a lowercase "users" directory (e.g., C:\users\bob\file). Both
should verify that the sanitize_error function correctly replaces the path with
[path] regardless of case.
- Around line 271-285: The Windows path matching logic for the is_path_start
condition is case-sensitive and only matches uppercase drive letters and paths
with uppercase backslash-Users-backslash. To fix this, change the check from
c.is_ascii_uppercase() to c.is_ascii_alphabetic() to accept both uppercase and
lowercase drive letters, and modify the path comparison logic to use
case-insensitive matching (convert the substring to lowercase before comparison
or use a method that performs case-insensitive comparison) so that variations
like c:\users\, C:\users\, and c:\Users\ are all properly detected and scrubbed
to [path].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 21cbc484-649e-410e-af75-27f4ba14bc76

📥 Commits

Reviewing files that changed from the base of the PR and between 65ebb0f and 2e15499.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (32)
  • .planning/ROADMAP.md
  • .planning/STATE.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-01-PLAN.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-01-SUMMARY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-02-PLAN.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-02-SUMMARY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-03-PLAN.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-03-SUMMARY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-04-PLAN.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-04-SUMMARY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-05-PLAN.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-05-SUMMARY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-CONTEXT.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-DISCUSSION-LOG.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-PATTERNS.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-RESEARCH.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-SECURITY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-VALIDATION.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-VERIFICATION.md
  • apps/desktop/src-tauri/src/commands/auth.rs
  • apps/desktop/src-tauri/src/fuse/mod.rs
  • apps/desktop/src-tauri/src/fuse/windows/mod.rs
  • crates/fuse/Cargo.toml
  • crates/fuse/src/journal_helpers.rs
  • crates/fuse/src/lib.rs
  • crates/fuse/src/read_ops.rs
  • crates/fuse/src/write_ops.rs
  • crates/sdk/Cargo.toml
  • crates/sdk/src/lib.rs
  • crates/sdk/src/queue.rs
  • crates/sdk/src/sync.rs
  • release-please-config.json

Comment thread crates/fuse/src/journal_helpers.rs Outdated
Comment thread crates/fuse/src/lib.rs Outdated
Comment thread crates/fuse/src/read_ops.rs Outdated
Comment thread crates/fuse/src/read_ops.rs
Comment thread crates/sdk/src/queue.rs
Comment thread crates/sdk/src/queue.rs Outdated
Comment thread crates/sdk/src/queue.rs Outdated
Comment thread crates/sdk/src/queue.rs
Comment thread crates/sdk/src/sync.rs
Comment thread crates/sdk/src/sync.rs
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.42362% with 153 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.99%. Comparing base (2f5f4a5) to head (c90394a).

Files with missing lines Patch % Lines
crates/fuse/src/lib.rs 69.77% 68 Missing ⚠️
crates/sdk/src/queue.rs 92.95% 36 Missing ⚠️
apps/desktop/src-tauri/src/fuse/mod.rs 0.00% 28 Missing ⚠️
crates/fuse/src/read_ops.rs 66.66% 9 Missing ⚠️
apps/desktop/src-tauri/src/commands/auth.rs 0.00% 8 Missing ⚠️
crates/fuse/src/write_ops.rs 0.00% 3 Missing ⚠️
crates/fuse/src/journal_helpers.rs 98.85% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #533      +/-   ##
==========================================
+ Coverage   64.83%   67.99%   +3.15%     
==========================================
  Files         143      159      +16     
  Lines       11147    18955    +7808     
  Branches     1258     1258              
==========================================
+ Hits         7227    12888    +5661     
- Misses       3675     5822    +2147     
  Partials      245      245              
Flag Coverage Δ
api 85.33% <ø> (ø)
api-client 85.33% <ø> (ø)
core 85.33% <ø> (ø)
crypto 85.33% <ø> (ø)
desktop 17.95% <0.00%> (-13.38%) ⬇️
rust 64.63% <86.80%> (?)
sdk 85.33% <ø> (ø)
sdk-core 85.33% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
crates/sdk/src/sync.rs 37.50% <100.00%> (ø)
crates/fuse/src/journal_helpers.rs 91.72% <98.85%> (ø)
crates/fuse/src/write_ops.rs 23.92% <0.00%> (ø)
apps/desktop/src-tauri/src/commands/auth.rs 0.00% <0.00%> (ø)
crates/fuse/src/read_ops.rs 15.44% <66.66%> (ø)
apps/desktop/src-tauri/src/fuse/mod.rs 15.20% <0.00%> (+15.19%) ⬆️
crates/sdk/src/queue.rs 95.65% <92.95%> (ø)
crates/fuse/src/lib.rs 47.88% <69.77%> (ø)

... and 65 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- check payload size cap from temp-file metadata before read_all
- avoid cloning ciphertext for sidecar writer; move and return via channel
- scrub sidecar host paths from replay error strings
- preserve legacy inline ciphertext for one-time passthrough replay
- validate sidecar path before committing journal JSON
- clean up partial sidecar on write/fsync failure
- remove journal JSON before its sidecar to avoid dangling replay trigger
- make purge_vault best-effort instead of fail-fast
- no-op record_failure when the entry file was already purged
- case-insensitive Windows path scrubbing plus tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@FSM1

FSM1 commented Jun 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/sdk/src/queue.rs (1)

611-635: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Close the purge-vs-failure TOCTOU window.

The exists() guard only handles entries purged before Line 617. If purge_vault unlinks the JSON after that check but before the later put/update_status, record_failure can still recreate a logged-out vault entry, often after its sidecar was deleted. Use a no-create rewrite/open, tombstone, or cancellation barrier so missing-after-purge cannot be recreated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/sdk/src/queue.rs` around lines 611 - 635, The exists() check in
record_failure creates a race condition window where purge_vault can delete the
JSON file after the check but before put() or update_status() are called,
allowing the purged entry to be recreated without its sidecar. Modify the put()
and update_status() methods to safely handle the case where the entry file is
missing after the initial check, either by using atomic open flags that prevent
file creation if it doesn't exist, or by adding defensive existence checks
within put() and update_status() before performing their writes so that a
post-purge deletion cannot cause resurrection of the entry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/fuse/src/journal_helpers.rs`:
- Around line 683-696: Remove the local cap_guard closure that reimplements the
payload size check logic, as it doesn't actually test the real implementation.
Instead, create an oversized sparse or truncated temporary file handle and call
the actual build_upload_journal_entry function directly with this handle to
verify that the function properly rejects payloads exceeding the cap before
attempting to read them, which will catch regressions where the size check is
inadvertently removed or the read operation happens before the check.

In `@crates/fuse/src/lib.rs`:
- Around line 3498-3518: The test is using permission-based failure simulation
to trigger an error when calling journal.remove(id), but this approach is
unreliable and environment-dependent since root or capability-elevated Unix
runners can still unlink read-only directories. Instead of manipulating
directory permissions with set_mode(0o500), create an actual directory at the
path `<id>.json` before calling journal.remove(id); this will cause remove_file
to deterministically fail with a non-NotFound error (attempting to remove a file
when a directory exists at that path) without relying on permission checks.
Remove all the permission manipulation code including the std::fs::metadata
calls, perms.set_mode calls, and std::fs::set_permissions calls, and replace
them with a single call to std::fs::create_dir that creates a directory at the
target path.
- Around line 2149-2168: The decrypt_journal_name function currently treats all
decryption and UTF-8 conversion failures as legacy plaintext, allowing corrupted
Phase-52 encrypted data to be replayed as hex blobs. Change the return type from
String to Result<String, String>, and refactor the logic so that only hex decode
failures (which indicate pre-Phase-52 plaintext) use the legacy passthrough
fallback, while errors from cipherbox_crypto::ecies::unwrap_key and
String::from_utf8 conversion failures route through record_failure instead,
since these indicate corrupted Phase-52 data that should not be replayed.
- Around line 1545-1573: The sidecar_path field is being used directly from the
persisted journal entry in the replay pattern match, but this creates a security
issue where a tampered or stale entry could point to the wrong file. Instead of
trusting the persisted sidecar_path value, derive it programmatically using
journal.sidecar_path_for with the entry ID. For non-legacy entries in this match
arm, replace the direct usage of sidecar_path with a call to
journal.sidecar_path_for(&entry.id) before passing it to the replay_upload_entry
function call. If the entry is legacy, apply an alternative approach such as
canonicalizing and validating that the path is in the expected format
<journal_dir>/<id>.bin.

In `@crates/sdk/src/queue.rs`:
- Around line 282-346: The put_with_sidecar method writes ciphertext bytes to
disk and persists the caller-provided sidecar_sha256 without validating that the
hash matches the actual bytes being written. Compute the SHA-256 hash of the
ciphertext parameter after successfully writing and fsyncing it, then extract
the sidecar_sha256 from the UploadFile variant in entry.op and compare them
before calling self.put(entry). Return an error with a descriptive message if
the hashes do not match, ensuring the JSON entry is only written when the hash
verification succeeds.
- Around line 68-79: The `legacy_ciphertext_b64` field uses `skip_serializing`,
which prevents it from being written back to disk when an entry is re-persisted.
When a pre-Phase-52 entry with inline ciphertext undergoes a status or retry
rewrite, the field is lost in memory but never persisted to a sidecar, resulting
in a missing-payload entry. Before any status change, retry modification, or
re-put operation on an entry containing `legacy_ciphertext_b64` data, migrate
the legacy ciphertext bytes to the canonical `.bin` sidecar file format. This
ensures the payload is persisted in the sidecar before the in-memory field is
dropped during serialization, preventing data loss on transient failures.
- Around line 509-517: The malformed JSON error handling in the deserialization
section (where Err(e) is caught in the serde_json::from_slice call) currently
only logs and skips the malformed entry, but this leaves the matching sidecar
file intact because the JSON file still exists. To fix this, track which JSON
stems are successfully parsed (store valid entry identifiers), and ensure that
in pass 3 of the GC logic (around line 556-565), treat malformed JSON entries as
having no live owner so the matching sidecar file can be removed. Either
maintain a collection of parseable JSON stems that pass 3 can check, or actively
quarantine/remove malformed JSON and sidecar pairs together during the error
handling to allow GC to clean them up.

---

Outside diff comments:
In `@crates/sdk/src/queue.rs`:
- Around line 611-635: The exists() check in record_failure creates a race
condition window where purge_vault can delete the JSON file after the check but
before put() or update_status() are called, allowing the purged entry to be
recreated without its sidecar. Modify the put() and update_status() methods to
safely handle the case where the entry file is missing after the initial check,
either by using atomic open flags that prevent file creation if it doesn't
exist, or by adding defensive existence checks within put() and update_status()
before performing their writes so that a post-purge deletion cannot cause
resurrection of the entry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 00713255-692d-4fc3-8e2b-bce0fe614365

📥 Commits

Reviewing files that changed from the base of the PR and between 65ebb0f and 1bece5b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (33)
  • .planning/ROADMAP.md
  • .planning/STATE.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-01-PLAN.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-01-SUMMARY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-02-PLAN.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-02-SUMMARY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-03-PLAN.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-03-SUMMARY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-04-PLAN.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-04-SUMMARY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-05-PLAN.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-05-SUMMARY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-CONTEXT.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-DISCUSSION-LOG.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-PATTERNS.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-RESEARCH.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-SECURITY.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-VALIDATION.md
  • .planning/phases/52-desktop-fuse-durability-at-rest-safety/52-VERIFICATION.md
  • .planning/todos/completed/2026-06-18-fuse-journal-growth-and-replay-timeout.md
  • apps/desktop/src-tauri/src/commands/auth.rs
  • apps/desktop/src-tauri/src/fuse/mod.rs
  • apps/desktop/src-tauri/src/fuse/windows/mod.rs
  • crates/fuse/Cargo.toml
  • crates/fuse/src/journal_helpers.rs
  • crates/fuse/src/lib.rs
  • crates/fuse/src/read_ops.rs
  • crates/fuse/src/write_ops.rs
  • crates/sdk/Cargo.toml
  • crates/sdk/src/lib.rs
  • crates/sdk/src/queue.rs
  • crates/sdk/src/sync.rs
  • release-please-config.json

Comment thread crates/fuse/src/journal_helpers.rs Outdated
Comment thread crates/fuse/src/lib.rs
Comment thread crates/fuse/src/lib.rs Outdated
Comment thread crates/fuse/src/lib.rs Outdated
Comment thread crates/sdk/src/queue.rs
Comment thread crates/sdk/src/queue.rs
Comment thread crates/sdk/src/queue.rs
Comment thread crates/fuse/src/lib.rs Outdated
FSM1 and others added 2 commits June 21, 2026 00:32
decrypt_journal_name distinguished a pre-Phase-52 plaintext name from a
Phase-52 ECIES ciphertext using hex-validity alone. A legacy filename that is
itself pure even-length hex — a hyphen-less UUID, a SHA-1/SHA-256 digest, a git
object id — hex-decodes, fails ecies::unwrap_key, and returns Err, parking the
entry as Failed until gc_failed_entries age-purges it (sidecar .bin and all),
destroying the captured write.

Gate the ECIES decrypt path on a length floor: a genuine ECIES name is always
ephemeral_pubkey || nonce || tag || ciphertext, and in-place bit-rot does not
shrink it, so any hex value shorter than ECIES_MIN_CIPHERTEXT_SIZE cannot be one
and is passed through verbatim. Corruption of a real, long-enough ECIES name
still returns Err and is retained for retry.

Resolves Greptile P1 on PR #533.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@FSM1 FSM1 merged commit b3511af into main Jun 20, 2026
28 checks passed
@FSM1 FSM1 deleted the feat/desktop-fuse-durability-at-rest-safety branch June 20, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:cipherbox-fuse:feat Minor version bump (new feature) for cipherbox-fuse release:cipherbox-sdk:feat Minor version bump (new feature) for cipherbox-sdk release:desktop:feat Minor version bump (new feature) for desktop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant