feat(desktop): macOS desktop catch-up and hybrid metadata fix#148
Conversation
- Add hkdf.rs with derive_vault_ipns_keypair, derive_file_ipns_keypair, derive_registry_ipns_keypair using HKDF-SHA256 with domain-separated info - Add aes_ctr.rs with encrypt_aes_ctr, decrypt_aes_ctr, decrypt_aes_ctr_range using Ctr64BE matching Web Crypto API length:64 - Add hkdf, sha2, aes, ctr crate dependencies to Cargo.toml - Register aes_ctr and hkdf modules in crypto/mod.rs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- M-11: Manual Debug impls for auth DTOs redact tokens and keys - L-2: URL-encode IPNS name query parameter via urlencoding crate - L-4: Remove console.log that leaked private key length - L-5: Zeroize Ed25519 key_bytes stack copy after SigningKey creation - L-7: Sanitize sync daemon error messages before tray display Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ests - Add FilePointer, FolderChildV2, FolderMetadataV2, AnyFolderMetadata types - Add FileMetadata with serde default "GCM" for encryptionMode field - Add decrypt_any_folder_metadata for v1/v2 version-dispatched parsing - Add encrypt_file_metadata and decrypt_file_metadata functions - Add 20 new tests: HKDF derivation, AES-CTR roundtrip/range, v2 types, FileMetadata serde, camelCase serialization, encrypt/decrypt roundtrips - All 113 tests pass (cargo test) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- clap CLI arg parsing gated by #[cfg(debug_assertions)] - dev_key stored in AppState, accessible via get_dev_key Tauri command - dev_key zeroized on clear_keys() (logout) - Release builds compile out the CLI module entirely Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - HKDF derivation functions and AES-CTR primitives - v2 folder schema types + FileMetadata type + tests SUMMARY: .planning/phases/11.1-macos-desktop-catch-up/11.1-01-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - Task 1: Security fixes M-11, L-2, L-4, L-5, L-7 - Task 2: --dev-key CLI argument for headless auth SUMMARY: .planning/phases/11.1-macos-desktop-catch-up/11.1-02-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace random Ed25519 keygen with HKDF deterministic derivation - initialize_vault now calls crypto::hkdf::derive_vault_ipns_keypair - IPNS name derived directly from HKDF output (no separate derivation) - fetch_and_decrypt_vault verifies stored key matches HKDF derivation - Backward compatible: warns on mismatch but proceeds with stored key Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace PnP Modal SDK (@web3auth/modal) with MPC Core Kit - Add @toruslabs/tss-dkls-lib TSS library dependency - Rewrite auth.ts: Core Kit loginWithJWT via CipherBox identity provider - Google OAuth via Google Identity Services (GIS) with 60s timeout - Email OTP via CipherBox backend send-otp/verify-otp endpoints - REQUIRED_SHARE status handling for MFA-enabled accounts - Add vite-env.d.ts for import.meta.env type support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite main.ts: CipherBox-branded login with Google OAuth + Email OTP - Replace single [CONNECT] button with full login form - Google sign-in via GIS, email OTP with send-code/verify flow - REQUIRED_SHARE stub UI for MFA-enabled accounts (full MFA in Plan 05) - Dev-key auth flow via test-login endpoint for debug builds - Update commands.rs loginType from 'social' to 'corekit' - Add Clone/Debug derives + to_v1() helper to AnyFolderMetadata - Include uncommitted v2 folder metadata handling in FUSE layer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - Core Kit SDK setup and auth.ts rewrite - Login UI in main.ts + Rust loginType update SUMMARY: .planning/phases/11.1-macos-desktop-catch-up/11.1-04-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - Task 1: HKDF vault initialization in commands.rs - Task 2: v2 folder metadata + CTR decryption in FUSE layer SUMMARY: .planning/phases/11.1-macos-desktop-catch-up/11.1-03-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add loginWithWallet() in auth.ts using injected EIP-1193 provider - Build EIP-4361 SIWE message matching web app format exactly - Nonce from GET /auth/identity/wallet/nonce, verify via POST /auth/identity/wallet - Add "Connect Wallet" button to login UI between Google and Email - Wire wallet button with error handling and REQUIRED_SHARE support - Document Tauri webview limitation for browser extensions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- DeviceEntry and DeviceRegistry types matching web app TypeScript definitions - ECIES-encrypted registry CRUD: register, fetch, decrypt - HKDF-derived IPNS keypair for deterministic registry discovery - Persistent device ID via macOS Keychain - 7 unit tests for serialization, deserialization, and helpers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 1/1 - SIWE wallet login implementation SUMMARY: .planning/phases/11.1-macos-desktop-catch-up/11.1-07-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add tokio::spawn wrapper for register_device after step 8 (mark authenticated) - Fire-and-forget pattern: failures logged but never block login - Uses cloned Arc<ApiClient>, private key, and public key for spawned task - Desktop device appears in web app's Authorized Devices view after first login Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - Device registry types and service module - Non-blocking registry call in auth flow SUMMARY: .planning/phases/11.1-macos-desktop-catch-up/11.1-06-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- replace stub with full recovery phrase + approval UI - recovery path: 24-word mnemonic textarea with validation - approval path: bulletin board request + 3s polling - cancel button cleans up approval request + ephemeral key - back-to-login cleans up active approval before logout - import inputRecoveryPhrase, requestDeviceApproval, etc. - add Phase 11.2 TODO for approval listener after auth Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - MFA recovery and device approval in auth.ts - REQUIRED_SHARE UI in main.ts SUMMARY: .planning/phases/11.1-macos-desktop-catch-up/11.1-05-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7 plans executed across 3 waves. Verified 10/10 must-haves: - HKDF vault IPNS derivation, AES-CTR decryption, v2 folder metadata - Core Kit auth migration, MFA recovery, device registry, SIWE wallet Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Tauri placeholder icons with CipherBox-branded assets: - Tray icon: >_ prompt symbol as macOS template image (22x22, 44x44 @2x) - App icons: green > on black background (32x32, 128x128, 128x128@2x, icns, ico) - Load tray icon from embedded bytes with icon_as_template(true) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update login page to match web app's terminal aesthetic: - Black background (#000000) with green text (#00D084) - JetBrains Mono font, > prompt prefix on title - Green-on-dark button and input styles with matching hover/focus states - Remove wallet connect button (not available in Tauri webview) - Update MFA challenge UI and loading/error screens to match Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GIS One Tap doesn't work in Tauri webview (no Google session, unregistered origin). Switch to OAuth2 implicit flow that opens Google consent page in a popup, with localStorage polling for the ID token callback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend expects uncompressed secp256k1 public key (130 hex chars, 0x04 prefix) matching Web3Auth JWT. Was incorrectly sending compressed format (66 hex chars). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add dotenvy to load .env from the desktop app root, sharing VITE_* vars between webview and Rust backend. API URL now resolves: CIPHERBOX_API_URL > VITE_API_URL > localhost default. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix three FUSE-T NFS issues: - create() marks parent as mutated to prevent background refresh from wiping newly created files - release() uploads new files (empty CID) even when not dirty, fixing touch+release without write - open() downloads content synchronously instead of async prefetch, preventing NFS EIO retry exhaustion on large files - Add trace logging to all FUSE callbacks for debugging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add detailed instructions for --dev-key CLI flag, including: - Requirements (VITE_TEST_LOGIN_SECRET, API NODE_ENV) - Usage examples with local API - Note that staging doesn't support it yet (NODE_ENV=production) - Add VITE_TEST_LOGIN_SECRET to .env.example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two key FUSE-T NFS fixes: 1. Background IPNS refresh now uses merge-only mode: existing children not in remote metadata are preserved instead of removed. This prevents files from disappearing when IPNS publish hasn't propagated yet. Initial mount still does full replace. 2. read() now downloads content synchronously as fallback when cache misses. This handles NFSv3 stateless behavior where FUSE-T may call read() without a prior open(), and cache eviction scenarios. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
FUSE-T uses NFSv4 with hard mount and timeo=10 (1-second timeout). The previous open() blocked the single NFS thread for content download (up to 120s), causing the mount to enter "not responding" state where no READ operations could be processed. Fix: open() now returns immediately and starts an async background prefetch via the existing content_tx channel. read() serves from cache if prefetch completed, otherwise falls back to synchronous download. This keeps the NFS thread responsive and prevents mount degradation. Also extracts fetch_and_decrypt_content_async() helper to share download+decrypt logic between prefetch tasks and read() fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
macOS NFS client may send filenames in NFC or NFD Unicode form. Our inode table stores names from metadata which may also be in either form. This mismatch causes files with accented characters (e.g., Muyè) to fail lookup — ls shows the file but open/stat fails. Fix: normalize all names to NFC in InodeTable insert/find_child/remove and in the rename callback. This ensures consistent HashMap keys regardless of which Unicode normalization form the NFS client uses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract shared `complete_auth_setup` helper from `handle_auth_complete` and add `handle_test_login_complete` (debug-only) that skips /auth/login. Fixes three bugs in handleDevKeyAuth: - Use accessToken from test-login response (not idToken) - Use server-generated privateKeyHex (not CLI dev key) so keypair matches the user record for vault ECIES operations - Call handle_test_login_complete instead of handle_auth_complete Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dev-key mode was broken because no webview window was created on startup (only created when user clicks Login in tray). The JS auth flow never ran. - Auto-create hidden webview in setup() when dev-key is present - Add skip_keychain flag to complete_auth_setup to avoid macOS Keychain permission popups during automated test-login flow - Import tauri::Manager trait for handle.state() access Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WIP FUSE improvements for better NFS responsiveness: - Proactive content prefetch on readdir (offset=0): starts downloading and decrypting all child file contents in background - Write-open checks content cache before falling back to sync download - read() cache miss uses poll-based approach (100ms increments, 3s max) instead of blocking sync download to avoid stalling NFS thread Contains eprintln debug lines for ongoing debugging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… reads macOS NFS client has a kernel bug where WRITE RPCs never reach FUSE-T's NFS server for newly created files, causing permanent process hangs (confirmed by FUSE-T author, reported to Apple). Switch to FUSE-T's SMB backend which correctly delivers write callbacks. Fix SMB-specific issues: - opendir must return non-zero file handles (SMB treats fh=0 as invalid) - Vendor patched fuser with peek-based receive() that handles Unix domain socket fragmentation — stock fuser assumes /dev/fuse atomic message delivery, but FUSE-T communicates via socket where large messages arrive in fragments and multiple messages can coalesce Tested: 50MB and 100MB file write+readback with matching checksums, 20 rapid small file creates, directory operations, file deletion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… in debug - Set reqwest timeout to 120s (was unlimited) with 10s connect timeout to prevent FUSE thread hangs from slow API responses - Move device registry tokio::spawn after FUSE mount to prevent HTTP connection pool starvation during pre-populate - Skip macOS Keychain in debug builds for device ID — each rebuild produces a new binary signature, triggering repeated permission prompts - Remove debug eprintln statements from API client Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add learnings entry covering the NFS write bug discovery, SMB backend switch, fuser socket patch, and what worked vs what didn't. Rewrite desktop CLAUDE.md with comprehensive FUSE architecture docs, known limitations, debugging commands, and detailed porting notes for Linux (kernel FUSE) and Windows (WinFSP/Dokan) implementations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Area: crypto Priority: urgent Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Web app writes version:"v2" folder metadata containing both v1-style inline FileEntry children (with cid, fileKeyEncrypted) and v2-style FilePointer children (with fileMetaIpnsName). The Rust deserializer rejected this hybrid format, causing the FUSE mount to show empty. Fix: make FileEntry fields optional via serde defaults, add file_meta_ipns_name field, and detect pointer entries in populate_folder. Also adds V2→V1 fallback in decrypt_any_folder_metadata and debug logging for decrypted metadata JSON. These workarounds are documented in the todo for removal once the web app metadata versioning is fixed at the source. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. WalkthroughAdds macOS desktop Phase 11.1 work: deterministic HKDF IPNS key derivation, v2 folder metadata with per-file IPNS pointers and eager resolution, AES‑CTR file decryptions and range reads, encrypted device registry, Core Kit/SIWE/MFA auth changes, vendored fuser 0.16 with a peek-based socket receive patch, FUSE-layer integration, tests, CI and docs. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant DesktopApp as Desktop App (Webview)
participant Tauri as Tauri Backend
participant Crypto as Crypto (HKDF/AES)
participant IPFS as IPFS/IPNS
participant Inode as Inode Table
User->>DesktopApp: Request mount after auth
DesktopApp->>Tauri: handle_auth_complete(access_token, private_key)
Tauri->>Crypto: derive_vault_ipns_keypair(private_key)
Crypto-->>Tauri: (ipns_private_key, ipns_public_key, ipns_name)
Tauri->>IPFS: fetch metadata for ipns_name
IPFS-->>Tauri: encrypted folder metadata (v1 or v2)
Tauri->>Crypto: decrypt_any_folder_metadata(encrypted, folder_key)
Crypto-->>Tauri: AnyFolderMetadata
alt v2 with FilePointers
Tauri->>Inode: populate_folder_v2(metadata_v2)
Inode-->>Tauri: unresolved FilePointers list
loop per pointer
Tauri->>IPFS: resolve pointer (IPNS → CID)
IPFS-->>Tauri: encrypted FileMetadata
Tauri->>Crypto: decrypt_file_metadata(encrypted, folder_key)
Crypto-->>Tauri: FileMetadata (cid,key,iv,size,mode)
Tauri->>Inode: resolve_file_pointer(ino, cid, key, iv, size, mode)
end
else v1
Tauri->>Inode: populate_folder(metadata_v1)
end
Tauri->>DesktopApp: mount ready
sequenceDiagram
participant User
participant Desktop as Desktop App
participant Wallet as Wallet (EIP-1193)
participant CipherBox as Backend
participant CoreKit as Core Kit
participant Tauri as Tauri Backend
User->>Desktop: Click "Connect Wallet"
Desktop->>Wallet: request accounts / personal_sign
Wallet-->>Desktop: account & signature
Desktop->>CipherBox: GET nonce -> POST signature for JWT
CipherBox-->>Desktop: JWT
Desktop->>CoreKit: loginWithJWT(JWT)
CoreKit-->>Desktop: export TSS key
Desktop->>Tauri: handle_auth_complete(JWT, exported_keys)
Tauri->>Crypto: derive_vault_ipns_keypair(exported_key)
Crypto-->>Tauri: ipns_name
Tauri->>IPFS: fetch & decrypt metadata → mount
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
# Conflicts: # pnpm-lock.yaml
There was a problem hiding this comment.
Actionable comments posted: 5
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/desktop/src-tauri/src/crypto/ed25519.rs (1)
85-97:⚠️ Potential issue | 🟡 Minor
get_public_keyleaves itskey_bytesstack copy unzeroized – inconsistent withsign_ed25519.The same private-key pattern is used here (stack-allocated
[u8; 32]copy), butkey_bytes.zeroize()is never called. The verification doc already notes this as a warning anti-pattern. Risk is lower (read-only derivation), but the pattern should be consistent.🛡️ Proposed fix
pub fn get_public_key(private_key: &[u8]) -> Result<Vec<u8>, Ed25519Error> { if private_key.len() != ED25519_PRIVATE_KEY_SIZE { return Err(Ed25519Error::InvalidPrivateKeySize); } - let key_bytes: [u8; 32] = private_key + let mut key_bytes: [u8; 32] = private_key .try_into() .map_err(|_| Ed25519Error::InvalidPrivateKeySize)?; let signing_key = SigningKey::from_bytes(&key_bytes); + key_bytes.zeroize(); let verifying_key = signing_key.verifying_key(); Ok(verifying_key.to_bytes().to_vec()) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/crypto/ed25519.rs` around lines 85 - 97, get_public_key creates a stack-allocated copy key_bytes of the private key but doesn’t zeroize it like sign_ed25519 does; update get_public_key to call key_bytes.zeroize() after constructing the SigningKey (or after use) so the temporary [u8; 32] is cleared, referencing the existing key_bytes variable and the SigningKey::from_bytes / signing_key.verifying_key usage to determine the safe point to zeroize. Ensure you import/use the same zeroize trait used elsewhere and keep zeroization immediate after the private bytes are no longer needed..planning/STATE.md (1)
143-155:⚠️ Potential issue | 🟡 MinorPending Todos section is missing 2 entries from this PR.
The count says "10 pending todo(s)" but only 8 are listed. The two new planning todos added in this PR —
2026-02-18-remove-pinata-references-from-api.mdand2026-02-18-fix-fuse-rename-on-smb-backend.md— are not reflected in this list.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.planning/STATE.md around lines 143 - 155, Update the "### Pending Todos" section so the displayed count and list include the two missing entries from this PR: add `2026-02-18-remove-pinata-references-from-api.md` and `2026-02-18-fix-fuse-rename-on-smb-backend.md` to the list and change the count text from "10 pending todo(s)" to "12 pending todo(s)"; ensure the new lines follow the existing format (dash, backtick filename, description and area) used by the other entries so the section remains consistent.apps/desktop/src-tauri/src/commands.rs (1)
729-738:⚠️ Potential issue | 🟡 Minor
derive_compressed_public_key_hexis unused dead code and should be removed.The function has no callers in the codebase. The login flow derives the uncompressed public key via
derive_public_key()at line 49 and sends it in the login request at line 55, making the compressed variant unnecessary.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/commands.rs` around lines 729 - 738, Remove the dead function derive_compressed_public_key_hex from commands.rs since it is unused; delete the entire function definition and any now-unused imports it solely relied on (e.g., ecies, hex) and run a build to confirm no remaining references; keep derive_public_key() and the existing login flow intact (they already produce the uncompressed key), and ensure no other code attempts to call derive_compressed_public_key_hex after removal.apps/desktop/src-tauri/src/fuse/mod.rs (1)
448-485:⚠️ Potential issue | 🟠 MajorPreserve
file_meta_ipns_namewhen publishing folder metadata.
build_folder_metadatahard-codesfile_meta_ipns_name: Noneat line 484, dropping FilePointer IPNS names from v2-format files. When the folder metadata is published, unresolved FilePointers (files with empty CID but valid IPNS reference) lose their only way to be resolved, becoming undiscoverable to other clients. Capturefile_meta_ipns_namefrom the inode and propagate it to the FileEntry.Suggested fix
- inode::InodeKind::File { - cid, - encrypted_file_key, - iv, - size, - encryption_mode, - .. - } => { + inode::InodeKind::File { + cid, + encrypted_file_key, + iv, + size, + encryption_mode, + file_meta_ipns_name, + .. + } => { ... metadata_children.push(crate::crypto::folder::FolderChild::File( crate::crypto::folder::FileEntry { ... - file_meta_ipns_name: None, + file_meta_ipns_name: file_meta_ipns_name.clone(), }, )); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/fuse/mod.rs` around lines 448 - 485, build_folder_metadata currently hard-codes FileEntry.file_meta_ipns_name to None, losing FilePointer IPNS names; update the code in the InodeKind::File match arm so it reads the file_meta_ipns_name from the inode and passes it into crate::crypto::folder::FileEntry instead of None. Locate the match for inode::InodeKind::File (the block that constructs crate::crypto::folder::FolderChild::File and uses uuid_from_ino(child_ino), name, cid, file_key_encrypted, file_iv, size, created_at, modified_at, encryption_mode) and replace the hard-coded None with the inode's file_meta_ipns_name (propagating the value you destructure or access from `child`/the inode) so unresolved FilePointers keep their IPNS reference when publishing folder metadata.
🟠 Major comments (17)
apps/desktop/src-tauri/vendor/fuser/simplefs_tests.sh-5-9 (1)
5-9:⚠️ Potential issue | 🟠 MajorINT/EXIT trap silently swallows failures —
${TEST_EXIT_STATUS:-1}requiredThe INT/EXIT trap at line 9 uses bare
$TEST_EXIT_STATUS, while the TERM handler at line 6 correctly uses${TEST_EXIT_STATUS:-1}. BecauseTEST_EXIT_STATUSis only set at lines 36 and 46, any early-exit path — such as the mount check at line 30 (exit 1) or the touch failure at line 37 — fires the EXIT trap whileTEST_EXIT_STATUSis still unset. In bash,exit $unset_varexpands toexitwhich is treated asexit 0, so the CI job reports success even when the test has failed.Every sibling script that shares this variable (
bsd_mount_tests.sh,mount_tests.sh,osx_mount_tests.sh) uses${TEST_EXIT_STATUS:-1}to guard against exactly this.🐛 Proposed fix
-trap 'kill $(jobs -p); exit $TEST_EXIT_STATUS' INT EXIT +trap 'kill $(jobs -p) 2>/dev/null; exit "${TEST_EXIT_STATUS:-1}"' INT EXITThe
2>/dev/nullalso suppresses the spurious error fromkillwhen no background jobs remain (e.g., after the FUSE process has already been waited on at line 43).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/simplefs_tests.sh` around lines 5 - 9, The INT/EXIT trap currently uses a bare $TEST_EXIT_STATUS which can be unset and cause a zero exit; update the trap that runs "kill $(jobs -p); exit $TEST_EXIT_STATUS" to use the same guarded expansion as exit_handler (use ${TEST_EXIT_STATUS:-1}) and redirect the kill error output to avoid noisy errors when there are no jobs (e.g., "kill $(jobs -p) 2>/dev/null; exit ${TEST_EXIT_STATUS:-1}"); modify the trap registration in the script and keep the existing exit_handler function unchanged.apps/desktop/src-tauri/vendor/fuser/pjdfs.Dockerfile-1-24 (1)
1-24:⚠️ Potential issue | 🟠 MajorContainer runs entirely as root — switch to the
fsgqauser created on line 8.The image already creates a non-root user (
fsgqa) but never issues aUSERinstruction, so everyRUN, the final binary, and any container entrypoint execute as UID 0. Trivy flags this as DS-0002.🔒 Proposed fix
ADD . /code/fuser/ RUN cd /code/fuser && cargo build --release --examples $BUILD_FEATURES && cp target/release/examples/simple /bin/fuser + +USER fsgqa🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/pjdfs.Dockerfile` around lines 1 - 24, The Dockerfile creates a non-root user fsgqa but never switches to it, so containers run as root; after building and copying the artifact (reference: the cargo build step in RUN cd /code/fuser && cargo build --release --examples $BUILD_FEATURES && cp target/release/examples/simple /bin/fuser) change ownership of runtime files (e.g. RUN chown fsgqa:fsgqa /bin/fuser and chown -R fsgqa:fsgqa /code if needed) and add a USER fsgqa instruction near the end of the Dockerfile so the final image and any subsequent commands run as the fsgqa user instead of root. Ensure any runtime-required paths are readable/executable by fsgqa.apps/desktop/public/google-callback.html-13-17 (1)
13-17:⚠️ Potential issue | 🟠 MajorGoogle ID token persisted in
localStoragewithout guaranteed cleanup — usesessionStorageor Tauri IPC instead
localStorageis disk-persistent and survives webview restarts. Ifwindow.close()is suppressed or delayed by the webview (which can happen in Tauri on macOS when the window was opened programmatically), the raw Google ID token JWT will sit in storage indefinitely with no TTL and no cleanup code in this file. The consuming side must atomically read-and-delete the key, but there is no enforcement of that contract here.Recommended mitigations (in preference order):
- Tauri IPC — emit the token directly to the opener via
window.__TAURI__.event.emitso no storage is touched at all.sessionStorage— scoped to the browsing context; auto-wiped when the window is destroyed, so a stuckwindow.close()no longer leaves the token on disk.- If
localStoragemust be used, pair it with a short absolute expiry sentinel (e.g. write{ idToken, expiresAt: Date.now() + 30_000 }) so the consumer can reject stale tokens.🛡️ Minimal fix — switch to
sessionStorage- localStorage.setItem('google-auth-result', JSON.stringify({ + sessionStorage.setItem('google-auth-result', JSON.stringify({ idToken: idToken, error: error || (!idToken ? 'No ID token received' : null) }));The consuming code should also be updated to read from
sessionStorageand delete the key immediately after reading.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/public/google-callback.html` around lines 13 - 17, Persisting the Google ID token into localStorage ('google-auth-result' via localStorage.setItem) is unsafe; change the implementation to emit the token directly to the opener using Tauri IPC (window.__TAURI__.event.emit) to avoid storage entirely, and if Tauri IPC is not available fall back to storing in sessionStorage under the same key and ensure the consumer immediately reads-and-deletes the key; if you must keep localStorage as a last resort, wrap the payload in an object with an expiresAt timestamp (Date.now() + 30000) so consumers can reject stale tokens.apps/desktop/public/google-callback.html-9-12 (1)
9-12:⚠️ Potential issue | 🟠 MajorMissing
stateparameter validation — open to CSRF / confused-deputy token injectionOAuth 2.0 (RFC 6749 §10.12) requires the callback to verify the
statevalue round-trips correctly to prevent cross-site request forgery. This callback ignoresstateentirely. Without it, a crafted redirect URL containing an attacker-controlledid_tokenwould be accepted and forwarded to the main app, which would then trust it as a legitimate Google credential.Even in a Tauri desktop app with a custom protocol, the
statecheck defends against confused-deputy scenarios where another local webpage or deep-link handler triggers this callback.🛡️ Proposed fix — validate
statebefore storing the tokenvar hash = window.location.hash.substring(1); var params = new URLSearchParams(hash); var idToken = params.get('id_token'); var error = params.get('error'); + var state = params.get('state'); + var expectedState = sessionStorage.getItem('google-auth-state'); + sessionStorage.removeItem('google-auth-state'); + if (!error && state !== expectedState) { + error = 'state_mismatch'; + idToken = null; + } - localStorage.setItem('google-auth-result', JSON.stringify({ + sessionStorage.setItem('google-auth-result', JSON.stringify({ idToken: idToken, error: error || (!idToken ? 'No ID token received' : null) }));The code that opens the Google authorization URL should generate a random
state, store it insessionStorage['google-auth-state'], and include it in the authorization request.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/public/google-callback.html` around lines 9 - 12, The callback must validate the OAuth2 state before accepting tokens: read the 'state' from the parsed params (alongside id_token and error), compare it to the stored value in sessionStorage['google-auth-state'], and if it does not match or is missing, abort forwarding/storing the idToken and surface/log an error; on success, remove the stored sessionStorage['google-auth-state'] and then continue processing idToken/error as before. Use the existing variables (hash, params, idToken, error) and the sessionStorage key 'google-auth-state' to locate where to add this check.apps/desktop/src-tauri/src/api/client.rs-24-28 (2)
24-28:⚠️ Potential issue | 🟠 MajorSilent fallback to
Client::new()removes all timeout protectionThe reqwest async
ClientBuilderdocs confirm that thetimeout()method "enables a total request timeout … applied from when the request starts connecting until the response body has finished … default is no timeout." This meansClient::new()— the fallback — carries no timeout. IfClientBuilder::build()fails (e.g., if the TLS backend cannot be initialized, or the resolver cannot load the system configuration), the.unwrap_or_else(|_| Client::new())silently produces a timeout-free client. Every subsequent request through that client can hang indefinitely, with no log, no metric, and no indication to the caller that timeout protection is absent.For a desktop app,
expect()at startup is the cleaner choice — a loud crash beats silent infinite hangs. At minimum, the error should be logged before falling back.🔧 Option A — panic loudly (preferred for startup code)
- let client = Client::builder() - .timeout(std::time::Duration::from_secs(120)) - .connect_timeout(std::time::Duration::from_secs(10)) - .build() - .unwrap_or_else(|_| Client::new()); + let client = Client::builder() + .timeout(std::time::Duration::from_secs(120)) + .connect_timeout(std::time::Duration::from_secs(10)) + .build() + .expect("failed to initialize HTTP client (TLS/resolver error)");🔧 Option B — log and fall back (if graceful degradation is required)
- let client = Client::builder() - .timeout(std::time::Duration::from_secs(120)) - .connect_timeout(std::time::Duration::from_secs(10)) - .build() - .unwrap_or_else(|_| Client::new()); + let client = Client::builder() + .timeout(std::time::Duration::from_secs(120)) + .connect_timeout(std::time::Duration::from_secs(10)) + .build() + .unwrap_or_else(|e| { + eprintln!("[ApiClient] HTTP client builder failed: {e}; falling back — no timeouts active"); + Client::new() + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/api/client.rs` around lines 24 - 28, The current Client::builder().timeout(...).connect_timeout(...).build().unwrap_or_else(|_| Client::new()) silently falls back to a timeout-free Client::new() on build() failure; replace the silent fallback with a loud failure by using build().expect(...) (with a clear error message including the build error) so the app crashes at startup, or if graceful degradation is required, change unwrap_or_else to log the error (include the build() error) before returning Client::new(); update the call sites around Client::builder(), build(), unwrap_or_else to use either expect or a logging fallback so no timeout-free client is produced silently.
24-28:⚠️ Potential issue | 🟠 MajorSilent fallback to
Client::new()removes all timeout protection
Client::new()has no timeout by default in reqwest's async client. If the builder fails (e.g., native TLS root store unavailable), the fallback silently produces a timeout-free client, defeating the timeout configuration entirely. The error is swallowed with no logging, leaving zero operator visibility into the degradation.Consider one of:
- Panic/
expectat startup (appropriate for desktop; better to fail loudly than silently hang on every request).- Log the error before falling back, so the failure is surfaced in logs.
🔧 Proposed fix (log + retain timeouts on fallback)
- let client = Client::builder() - .timeout(std::time::Duration::from_secs(120)) - .connect_timeout(std::time::Duration::from_secs(10)) - .build() - .unwrap_or_else(|_| Client::new()); + let client = Client::builder() + .timeout(std::time::Duration::from_secs(120)) + .connect_timeout(std::time::Duration::from_secs(10)) + .build() + .unwrap_or_else(|e| { + eprintln!("[ApiClient] reqwest builder failed ({e}); falling back to Client::new() — no timeouts active"); + Client::new() + });Or prefer
expectif builder failure should not occur in practice:- let client = Client::builder() - .timeout(std::time::Duration::from_secs(120)) - .connect_timeout(std::time::Duration::from_secs(10)) - .build() - .unwrap_or_else(|_| Client::new()); + let client = Client::builder() + .timeout(std::time::Duration::from_secs(120)) + .connect_timeout(std::time::Duration::from_secs(10)) + .build() + .expect("failed to build HTTP client");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/api/client.rs` around lines 24 - 28, The current Client::builder() call swallows build errors via unwrap_or_else(|_| Client::new()), which removes the configured timeout/connection limits and hides the underlying error; replace that silent fallback with either a loud failure (use .expect or unwrap with a clear message on the result of Client::builder().timeout(...).connect_timeout(...).build()) or, if you must continue, change unwrap_or_else to capture the error and log it (e.g., unwrap_or_else(|e| { log::error!("reqwest Client::builder().build() failed: {}", e); Client::new() })) so failures are visible; target the expression using Client::builder(), timeout, connect_timeout, build(), and unwrap_or_else in this file.apps/desktop/src-tauri/vendor/fuser/bsd_mount_tests.sh-5-9 (1)
5-9:⚠️ Potential issue | 🟠 MajorEXIT trap silently masks mount-check failures due to uninitialized
TEST_EXIT_STATUSThe TERM handler (line 6) correctly uses
${TEST_EXIT_STATUS:-1}, but the INT/EXIT trap (line 9) uses bare$TEST_EXIT_STATUS. BecauseTEST_EXIT_STATUSis never initialized at the top of the script, when the mount check on line 26 callsexit 1before any value is set, the EXIT trap fires and evaluatesexit $TEST_EXIT_STATUSasexit(empty expansion) — which bash treats as success (exit 0). A FreeBSD CI run with a broken FUSE mount would report a green build.Fix: initialize
TEST_EXIT_STATUS=1at the top of the script (fail-safe default) and only set it to 0 at line 45 upon success, and apply the same:-1guard in the INT/EXIT trap.🐛 Proposed fix
#!/usr/bin/env bash set -x +TEST_EXIT_STATUS=1 + exit_handler() { exit "${TEST_EXIT_STATUS:-1}" } trap exit_handler TERM -trap 'kill $(jobs -p); exit $TEST_EXIT_STATUS' INT EXIT +trap 'kill $(jobs -p); exit "${TEST_EXIT_STATUS:-1}"' INT EXITAlso applies to: 45-45
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/bsd_mount_tests.sh` around lines 5 - 9, Initialize TEST_EXIT_STATUS=1 at the top of the script to provide a fail-safe default, change the INT/EXIT trap to use the same guarded expansion (${TEST_EXIT_STATUS:-1}) or call the existing exit_handler to avoid empty expansion, and ensure the success path (function/section that currently sets success at line 45) explicitly sets TEST_EXIT_STATUS=0 on success; this keeps exit behavior consistent with exit_handler and prevents silent success when a prior exit 1 occurred.apps/desktop/package.json-14-14 (1)
14-14: 🛠️ Refactor suggestion | 🟠 MajorVerify
@noble/secp256k1version consistency and address guideline deviation.
auth.tscorrectly usessecp256k1.keygen()which is the v3 API (returning{secretKey, publicKey}). However,@cipherbox/cryptodeclares v2.2.3 indevDependencies, and its test files usesecp256k1.utils.randomPrivateKey()(v2 API), creating a version mismatch in the monorepo. Theencrypt.tsvalidation usingProjectivePoint.fromHex()should work with v3, but inconsistency across versions may cause issues during testing or when dependencies are resolved.Additionally, the codebase deviates from the cryptographic guideline: use ethers.js or libsodium.js for ECIES secp256k1 and ECDSA operations.
@noble/secp256k1is neither. While@cipherbox/cryptouses the complianteciesjslibrary for ECIES wrapping, the ephemeral keygen inauth.tsrelies directly on@noble/secp256k1. Consider consolidating ephemeral keypair generation into@cipherbox/cryptoand aligning the monorepo to a single approved library.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/package.json` at line 14, The repo has mixed `@noble/secp256k1` usage (auth.ts uses secp256k1.keygen(), encrypt.ts uses ProjectivePoint.fromHex(), tests use secp256k1.utils.randomPrivateKey()) causing version/API mismatch and a guideline deviation; fix by consolidating ephemeral keypair generation into `@cipherbox/crypto` (add a single exported generateEphemeralKeypair function there using an approved library), update auth.ts to call that function instead of secp256k1.keygen(), update tests to use the new helper instead of secp256k1.utils.randomPrivateKey(), and ensure package.json uses a single, consistent crypto dependency (prefer replacing `@noble/secp256k1` with ethers.js or libsodium.js or ensure `@cipherbox/crypto` wraps eciesjs/approved lib) so ProjectivePoint/fromHex usage is removed or adapted to the chosen approved library.apps/desktop/src-tauri/vendor/fuser/deny.toml-44-48 (1)
44-48:⚠️ Potential issue | 🟠 Major
Apache-2.0missing from license allow-list will breakcargo deny check licensesFive of fuser's seven direct dependencies—
libc,log,serde,smallvec, andzerocopy—declareApache-2.0(inApache-2.0 OR MITdual licensing). WithApache-2.0absent from the allow list,cargo deny check licenseswill fail on these core dependencies.The proposed additions of
BSD-3-ClauseandISCshould be verified against fuser's actual transitive dependency tree, as they are not present in the direct dependencies. Additionally,memchrusesUnlicense, which is also missing from the allow list.🔧 Suggested fix
allow = [ "BSD-2-Clause", -# "BSD-3-Clause", "MIT", + "Apache-2.0", ]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/deny.toml` around lines 44 - 48, The license allow-list in deny.toml currently omits several licenses required by fuser's dependencies; update the allow = [...] array to include "Apache-2.0" (to cover libc/log/serde/smallvec/zerocopy dual-licensed crates), add "ISC" and "BSD-3-Clause" if verified against fuser's transitive dependency tree, and include "Unlicense" for memchr; after updating the allow list in the deny.toml file, run cargo deny check licenses to confirm no other transitive licenses are missing and adjust entries accordingly.apps/desktop/src-tauri/src/sync/mod.rs-236-240 (1)
236-240:⚠️ Potential issue | 🟠 MajorByte-slice
&truncated[..80]will panic on multi-byte UTF-8 error strings.
truncated.len()returns the byte length. If any byte in position 78–80 is part of a multi-byte codepoint (e.g., a localized API error body containing accented characters or CJK),&truncated[..80]panics with"byte index X is not a char boundary". This kills the tokio sync task.🐛 Proposed fix – char-aware truncation
- let truncated = if truncated.len() > 80 { - format!("{}...", &truncated[..80]) - } else { - truncated.to_string() - }; + let truncated = match truncated.char_indices().nth(80) { + Some((i, _)) => format!("{}...", &truncated[..i]), + None => truncated.to_string(), + };Alternatively, if you prefer byte-level truncation, use the stable
floor_char_boundary(Rust ≥ 1.65):- let truncated = if truncated.len() > 80 { - format!("{}...", &truncated[..80]) - } else { - truncated.to_string() - }; + let truncated = if truncated.len() > 80 { + let boundary = truncated.floor_char_boundary(80); + format!("{}...", &truncated[..boundary]) + } else { + truncated.to_string() + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/sync/mod.rs` around lines 236 - 240, The current byte-slice &truncated[..80] can panic on multi-byte UTF-8 boundaries; replace the byte-based truncation with a char-aware truncation: check if truncated.chars().count() > 80, and if so build the prefix using truncated.chars().take(80).collect::<String>() and append "..." (otherwise use truncated.to_string()); update the code around the truncated variable and remove the direct &truncated[..80] slice to avoid char-boundary panics..planning/phases/11.1-macos-desktop-catch-up/11.1-02-PLAN.md-178-197 (1)
178-197:⚠️ Potential issue | 🟠 MajorEnsure
get_dev_keyIPC command and its AppState field arecfg-gated to debug builds.The CLI parsing is correctly wrapped in
#[cfg(debug_assertions)], but the actual implementation exposesget_dev_keyin release builds, unnecessarily widening the release binary's IPC attack surface.Evidence:
commands.rsline 415:get_dev_keylacks#[cfg(debug_assertions)]state.rsline 68:dev_key: RwLock<Option<String>>field lacks#[cfg(debug_assertions)]main.rslines 134–142: The release build branch (#[cfg(not(debug_assertions))]) still registersget_dev_keyon line 141, exposing the endpoint in production binariesWhile the command returns
Nonein release builds (no actual leak), the IPC endpoint and struct field must not exist at all in release, adhering to the principle of least privilege for attack surface.Wrap the command definition, AppState field, and release-build registration with
#[cfg(debug_assertions)].🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.planning/phases/11.1-macos-desktop-catch-up/11.1-02-PLAN.md around lines 178 - 197, Add cfg gating so the debug-only IPC surface is compiled out in release: annotate the Tauri command function get_dev_key in commands.rs with #[cfg(debug_assertions)], annotate the dev_key field on AppState (the RwLock<Option<String>> in state.rs) with the same attribute, and ensure main.rs only registers get_dev_key inside the debug_assertions branch of the invoke_handler (remove/guard any registration in the #[cfg(not(debug_assertions))] branch). This will ensure the get_dev_key symbol, the AppState.dev_key field, and the IPC registration do not exist in release builds..planning/phases/11.1-macos-desktop-catch-up/11.1-05-PLAN.md-147-160 (1)
147-160:⚠️ Potential issue | 🟠 MajorSecurity note:
factorKeytransmitted in plaintext through the backend.The
pollApprovalStatusflow receives the raw factor key from the server (line 147). If the backend is compromised, the attacker gains the TSS device share. The plan forapproveDevice(line 192) similarly sends it as plaintext JSON.Consider noting in the plan that the factor key should be encrypted end-to-end between devices (e.g., using ECIES with the requesting device's ephemeral public key) so the backend only relays an opaque blob. This aligns with the project's E2E encryption stance.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.planning/phases/11.1-macos-desktop-catch-up/11.1-05-PLAN.md around lines 147 - 160, pollApprovalStatus (and the related approveDevice flow) currently accepts/transmits factorKey in plaintext and then uses coreKit!.inputFactorKey/new BN(...) and _UNSAFE_exportTssKey, which exposes the TSS device share if the backend is compromised; change the design to have the approving device encrypt factorKey end-to-end to the requesting device (e.g., use ECIES / ephemeral recipient public key) so the server only relays an opaque ciphertext blob, update pollApprovalStatus and approveDevice to accept/return the encrypted blob instead of raw factorKey, and adjust the caller that invokes coreKit!.inputFactorKey to decrypt locally before calling inputFactorKey.apps/desktop/src-tauri/src/crypto/hkdf.rs-56-73 (1)
56-73:⚠️ Potential issue | 🟠 Major
okm(derived Ed25519 seed) is not zeroized after use.The 32-byte
okmarray holds sensitive key material on the stack. The PR's own security scope includes zeroization of Ed25519 keys (per 11.1-02-PLAN). After constructing theSigningKey,okmshould be cleared.Proposed fix
+use zeroize::Zeroize; + fn derive_ipns_keypair( user_private_key: &[u8; 32], info: &[u8], ) -> Result<(Vec<u8>, Vec<u8>, String), HkdfError> { let hk = Hkdf::<Sha256>::new(Some(HKDF_SALT), user_private_key); let mut okm = [0u8; 32]; hk.expand(info, &mut okm) .map_err(|_| HkdfError::DerivationFailed)?; let signing_key = SigningKey::from_bytes(&okm); + okm.zeroize(); let verifying_key = signing_key.verifying_key();Also, the returned
Vec<u8>private key won't be auto-zeroized on drop — callers should usezeroize::Zeroizing<Vec<u8>>or document the zeroization responsibility.Based on learnings, the PR's 11.1-02 plan explicitly lists "zeroize Ed25519 key" as a security fix.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/crypto/hkdf.rs` around lines 56 - 73, The okm stack buffer holding the derived Ed25519 seed must be zeroized after it is consumed: after calling SigningKey::from_bytes(&okm) (and before returning), explicitly clear/zeroize the okm array to remove sensitive material and handle any errors first (relates to okm, SigningKey::from_bytes, hk.expand, and HkdfError::*). Also do not return a plain Vec<u8> for the private key—either wrap the produced private_key Vec in zeroize::Zeroizing<Vec<u8>> before returning or add a clear API contract/documentation that callers must zeroize the returned private key (references: private_key, public_key, ipns::derive_ipns_name). Ensure zeroization occurs even on error paths.apps/desktop/src-tauri/vendor/fuser/src/mnt/fuse3.rs-68-86 (1)
68-86:⚠️ Potential issue | 🟠 Major
fuse_session_destroyis never called on the successfullibc_umountpath, leaking the session.When
libc_umountsucceeds (theOkbranch of theif let),dropexits without callingfuse_session_destroy. Same for the error-but-not-PermissionDeniedbranch (line 83). Only the Linux +PermissionDeniedpath cleans up fully. Sincefuse_sessionis allocated inMount::newviafuse_session_new, it must be freed before the Mount struct is dropped.Proposed fix
impl Drop for Mount { fn drop(&mut self) { use std::io::ErrorKind::PermissionDenied; if let Err(err) = super::libc_umount(&self.mountpoint) { // Linux always returns EPERM for non-root users. We have to let the // library go through the setuid-root "fusermount -u" to unmount. if err.kind() == PermissionDenied { #[cfg(target_os = "linux")] unsafe { fuse_session_unmount(self.fuse_session); fuse_session_destroy(self.fuse_session); return; } } warn!("umount failed with {err:?}"); } + unsafe { + fuse_session_destroy(self.fuse_session); + } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/src/mnt/fuse3.rs` around lines 68 - 86, The Drop implementation for Mount currently only calls fuse_session_destroy in the Linux+PermissionDenied path, leaking the fuse_session on successful libc_umount and on non-PermissionDenied errors; update Mount::drop to always clean up the session: after calling super::libc_umount(&self.mountpoint) (both in the Ok branch and in the Err branch when the error is not PermissionDenied) invoke the same unsafe cleanup sequence currently used for the PermissionDenied path (unsafe { fuse_session_unmount(self.fuse_session); fuse_session_destroy(self.fuse_session); }), and keep the PermissionDenied branch behavior but avoid an early return so cleanup is uniform for all paths; reference symbols: Mount::drop, libc_umount, fuse_session_unmount, fuse_session_destroy, fuse_session.apps/desktop/src-tauri/src/registry/mod.rs-166-198 (1)
166-198:⚠️ Potential issue | 🟠 Major
panic!in release-modeget_or_create_device_idviolates the "never block login" contract.This function is called from
register_device, which runs inside atokio::spawnfire-and-forget task. Apanic!on line 171 will crash that task. The module-level documentation explicitly states "Registry operations must NEVER block login" and "All errors are caught and logged by the caller," but a panic cannot be caught by the error handling in place and will abort the task instead.Return a
Resultor fallback to an ephemeral ID to handle the keyring entry creation failure gracefully.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/registry/mod.rs` around lines 166 - 198, The code currently panics on keyring entry creation (the unwrap_or_else in get_or_create_device_id), which can abort the background task; change it to handle the error gracefully by logging the failure and falling back to an ephemeral/generated UUID instead of panicking. Specifically, replace the unwrap_or_else that calls panic with a match or if-let on keyring::Entry::new("cipherbox-desktop", "device-id") so that on Err(e) you log a warning and proceed to generate the UUID (the same code used in the default branch) without attempting to set the credential; ensure all pathways return the device id string and never call panic! so register_device’s fire-and-forget task cannot be aborted.apps/desktop/src-tauri/src/fuse/mod.rs-575-610 (1)
575-610:⚠️ Potential issue | 🟠 MajorGate metadata publishing on all uploads completing.
The safety valve publishes after 10s regardless of
pending_uploads, then removes the queue entry. If an upload takes >10s (realistic given the 120s timeout), metadata is published withcid=""and never re-published. The fix correctly prevents publishing untilpending_uploads == 0.🛠️ Suggested fix
- let ready: Vec<u64> = self.publish_queue.iter() - .filter(|(_, entry)| { - let elapsed = now.duration_since(entry.first_dirty); - (entry.pending_uploads == 0 && elapsed >= debounce) - || elapsed >= safety_valve - }) + let ready: Vec<u64> = self.publish_queue.iter() + .filter(|(_, entry)| { + let elapsed = now.duration_since(entry.first_dirty); + if entry.pending_uploads == 0 { + elapsed >= debounce || elapsed >= safety_valve + } else { + false + } + }) .map(|(&ino, _)| ino) .collect();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/fuse/mod.rs` around lines 575 - 610, The flush_publish_queue logic currently allows the safety_valve to publish even when entry.pending_uploads > 0; update the filter inside flush_publish_queue to always require entry.pending_uploads == 0 before publishing (i.e., gate publishing on pending_uploads == 0) and only then check elapsed against debounce/safety_valve, so that entries are removed/published (leading to build_folder_metadata and spawn_metadata_publish) only when no uploads remain; adjust the iterator/filter that builds ready: Vec<u64> using the publish_queue entry fields (pending_uploads and first_dirty) accordingly.apps/desktop/src-tauri/vendor/fuser/src/ll/request.rs-1681-1689 (1)
1681-1689:⚠️ Potential issue | 🟠 Major
assert!in FUSE_WRITE parse will crash the daemon on malformed packets.Since this vendored fuser is patched specifically for SMB socket-based reads (per the PR), partial/truncated reads are a realistic scenario. If
data.len()doesn't matcharg.size, theassert!on line 1687 panics and kills the FUSE daemon. The same applies to FUSE_SETXATTR at line 1706. Both should returnNoneto surface the error gracefully viaRequestError::InsufficientData.Proposed fix: Replace asserts with checked returns
fuse_opcode::FUSE_WRITE => Operation::Write({ let out = Write { header, arg: data.fetch()?, data: data.fetch_all(), }; - assert!(out.data().len() == out.arg.size as usize); + if out.data().len() != out.arg.size as usize { + return None; + } out }),And similarly for SETXATTR:
fuse_opcode::FUSE_SETXATTR => Operation::SetXAttr({ let out = SetXAttr { header, arg: data.fetch()?, name: data.fetch_str()?, value: data.fetch_all(), }; - assert!(out.value.len() == out.arg.size as usize); + if out.value.len() != out.arg.size as usize { + return None; + } out }),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/src/ll/request.rs` around lines 1681 - 1689, The FUSE_WRITE and FUSE_SETXATTR parsing currently use assert! to validate payload lengths which will panic on malformed/truncated packets; instead, after constructing the Write (for fuse_opcode::FUSE_WRITE -> Operation::Write) or Setxattr (for fuse_opcode::FUSE_SETXATTR -> Operation::Setxattr) objects, check whether out.data().len() == out.arg.size as usize and if not return None (or propagate RequestError::InsufficientData) rather than asserting; update the blocks that build Write (header, arg: data.fetch()?, data: data.fetch_all()) and Setxattr similarly to perform a conditional length check and return the error/None to fail parsing gracefully.
- aes_ctr: prevent panic on empty ciphertext / out-of-bounds start_byte - fuse: use PendingContent enum to clear prefetch on download failure - fuse: preserve file_meta_ipns_name in build_folder_metadata - fuse: gate publish queue flush on pending_uploads == 0 - operations: send PendingContent::Failure on prefetch errors - client: replace silent Client::new() fallback with expect() - sync: fix UTF-8 byte-slice panic in sanitize_error truncation - hkdf: zeroize okm after deriving IPNS signing key - ed25519: zeroize key_bytes after constructing SigningKey - registry: replace panic! with graceful keyring fallback - commands: remove dead derive_compressed_public_key_hex - main: remove get_dev_key from release build handler list - google-callback: use sessionStorage instead of localStorage - google-callback: add OAuth state parameter CSRF validation - auth: generate and verify state parameter for Google OAuth Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ioctl: add bounds check on read to prevent out-of-bounds panic - ioctl: fix unwrap() panic and stale size on SET_SIZE - request: replace assert! with checked returns in FUSE_WRITE/SETXATTR - fuse3: add fuse_session_destroy on successful unmount path - deny.toml: add Apache-2.0 and Unlicense to license allow-list - pjdfs.Dockerfile: run container as non-root user - simplefs_tests.sh: fix trap to propagate test exit status - bsd_mount_tests.sh: fix trap to propagate test exit status - rust-toolchain.toml: pin to Rust 1.88 (edition 2024 + time crate) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/desktop/src-tauri/src/fuse/operations.rs (2)
89-95: 🛠️ Refactor suggestion | 🟠 Major
EncryptedFolderMetadatastruct declared three times; decrypt logic duplicated across private helpers and public wrappers.The private
decrypt_metadata_from_ipfs(lines 102–136) anddecrypt_file_metadata_from_ipfs(lines 258–284) are virtually identical to their public counterpartsdecrypt_metadata_from_ipfs_public(lines 2200–2236) anddecrypt_file_metadata_from_ipfs_public(lines 2240–2272). TheEncryptedFolderMetadatastruct is re-declared verbatim inside each of the three scopes.The duplication exists because the private helpers live inside
mod implementation { }and are unreachable from outside it. Lifting the shared parsing logic outside that module eliminates all three copies:♻️ Suggested refactor
+// Outside mod implementation — shared by both private handlers and public wrappers +#[cfg(feature = "fuse")] +#[derive(serde::Deserialize)] +struct EncryptedIpfsPayload { + iv: String, + data: String, +} + +/// Parse encrypted IPFS payload bytes into a sealed (IV || ciphertext) buffer. +#[cfg(feature = "fuse")] +fn parse_sealed_from_ipfs_bytes(encrypted_bytes: &[u8]) -> Result<Vec<u8>, String> { + let enc: EncryptedIpfsPayload = serde_json::from_slice(encrypted_bytes) + .map_err(|e| format!("Failed to parse encrypted metadata JSON: {}", e))?; + let iv_bytes = hex::decode(&enc.iv) + .map_err(|_| "Invalid metadata IV hex".to_string())?; + if iv_bytes.len() != 12 { + return Err(format!("Invalid IV length: {} (expected 12)", iv_bytes.len())); + } + use base64::Engine; + let ciphertext = base64::engine::general_purpose::STANDARD + .decode(&enc.data) + .map_err(|e| format!("Invalid metadata base64: {}", e))?; + let mut sealed = Vec::with_capacity(12 + ciphertext.len()); + sealed.extend_from_slice(&iv_bytes); + sealed.extend_from_slice(&ciphertext); + Ok(sealed) +}Then the four functions collapse to one-liners delegating to
parse_sealed_from_ipfs_bytes+ the appropriate crypto call, and all threeEncryptedFolderMetadata/EncryptedIpfsPayloadlocal-struct declarations are removed.Also applies to: 97-136, 254-284, 2197-2272
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/fuse/operations.rs` around lines 89 - 95, The EncryptedFolderMetadata struct and parsing logic are duplicated across mod implementation and public wrappers; extract a single shared EncryptedFolderMetadata (or EncryptedIpfsPayload) and a helper like parse_sealed_from_ipfs_bytes to perform the serde deserialization/IV+data decoding, then update decrypt_metadata_from_ipfs, decrypt_file_metadata_from_ipfs, decrypt_metadata_from_ipfs_public, and decrypt_file_metadata_from_ipfs_public to call that helper and then their respective crypto routines (AES-GCM decrypt call) so the struct and parsing exist only once and the four functions become thin delegating wrappers.
947-967:⚠️ Potential issue | 🟠 MajorWritable open of a large uncached file fails with EIO in under 3 seconds.
Line 956 falls back to
fetch_and_decrypt_file_content, which internally callsblock_with_timeoutwithNETWORK_TIMEOUT = 3s. TheCONTENT_DOWNLOAD_TIMEOUT(120s) comment explicitly acknowledges that "large files (e.g., 64MB) can take 30-60s from staging IPFS", but that constant is only used in the async prefetch path — not here.The read-only open path correctly avoids blocking the NFS thread by returning immediately and starting a background prefetch; the writable open path has no equivalent: it blocks the NFS thread and times out well before large files finish downloading.
Concrete failure scenario: user opens a 64MB vault file in a text editor without a prior
lsto warm the cache →open(O_RDWR)blocks for 3s, then returns EIO.Mitigations ranked by invasiveness:
- (Minimal fix) Introduce a dedicated
WRITE_OPEN_DOWNLOAD_TIMEOUT(e.g., 30s) and use it here — accepts a longer NFS thread stall for the edit case.- (Better) If content isn't cached, skip pre-population and return an empty temp file; rely on the existing readdir prefetch having warmed the cache. The caller (text editor) would overwrite the content anyway.
- (Best) Mirror the read-only async prefetch pattern: start a background fetch for the content, poll briefly, and return a partially pre-populated handle — same 3s poll budget as
read().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/fuse/operations.rs` around lines 947 - 967, The writable-open path currently calls fetch_and_decrypt_file_content which uses the global NETWORK_TIMEOUT (3s) and causes EIO for large files; fix by adding a new longer timeout constant (e.g., WRITE_OPEN_DOWNLOAD_TIMEOUT ~30s) and use it for this code path when calling fetch_and_decrypt_file_content (or its blocking helper block_with_timeout) so open(O_WRONLY|O_RDWR) allows a longer download window; update the call-site in operations.rs (the branch that checks cid.is_empty() and invokes fetch_and_decrypt_file_content) to pass or apply WRITE_OPEN_DOWNLOAD_TIMEOUT instead of the short NETWORK_TIMEOUT.
🧹 Nitpick comments (15)
apps/desktop/src-tauri/vendor/fuser/pjdfs.Dockerfile (2)
1-1: Consider pinning the base image to a digest for reproducible builds.
ubuntu:22.04is a mutable tag; a tag move or upstream update silently changes the build environment. Pinning to the immutable sha256 digest (e.g.,FROM ubuntu:22.04@sha256:<digest>) eliminates that variability and prevents potential supply-chain drift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/pjdfs.Dockerfile` at line 1, Replace the mutable base image tag in the pjdfs.Dockerfile's FROM instruction with an immutable digest-pinned image; locate the FROM ubuntu:22.04 line and change it to use the matching sha256 digest (e.g., FROM ubuntu:22.04@sha256:<digest>) so builds are reproducible and protected from upstream tag drift—ensure you fetch the correct digest for Ubuntu 22.04 from an official registry and update the Dockerfile accordingly.
5-6: Remove apt list cache to reduce image size.The
apt installlayer currently retains/var/lib/apt/lists/*, which can add tens to hundreds of MB to the image. Appending a cleanup in the sameRUNlayer keeps the intermediate cache out of the final layer.--no-install-recommendsfurther trims unnecessary transitive packages.♻️ Proposed fix
-RUN apt update && apt install -y git build-essential autoconf curl cmake libfuse-dev pkg-config fuse3 bc libtool \ - uuid-dev xfslibs-dev libattr1-dev libacl1-dev libaio-dev attr acl quota bsdmainutils dbench psmisc libfuse3-dev +RUN apt-get update && apt-get install -y --no-install-recommends \ + git build-essential autoconf curl cmake libfuse-dev pkg-config fuse3 bc libtool \ + uuid-dev xfslibs-dev libattr1-dev libacl1-dev libaio-dev attr acl quota bsdmainutils dbench psmisc libfuse3-dev \ + && rm -rf /var/lib/apt/lists/*🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/pjdfs.Dockerfile` around lines 5 - 6, The Dockerfile RUN layer that performs apt update && apt install (the long RUN line installing git build-essential ... libfuse3-dev) leaves apt lists cached and installs recommended packages; modify that same RUN command to add --no-install-recommends to the apt install invocation and remove the apt cache in the same layer by cleaning /var/lib/apt/lists/* (e.g., apt-get clean && rm -rf /var/lib/apt/lists/*) so the image size is reduced and no extra cache persists..planning/STATE.md (1)
154-156: URGENT pending todo represents unresolved root cause — track actively.The FUSE-side fix in this PR (optional
FileEntryfields + v2→v1 fallback parsing) is a defensive workaround. The root cause — the web app writing v2-tagged metadata with v1-style file entries — is explicitly deferred tofix-v2-metadata-version-mismatch.md. Until that todo is resolved, any new vault written by the web app can still trigger the mixed-format condition, and the desktop fallback path will continue to be exercised in production.The two other new todos (
remove-pinata-references-from-api.md,fix-fuse-rename-on-smb-backend.md) are lower-urgency but should also be tracked against the SMB backend switch introduced in this phase, since rename is a core FUSE operation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.planning/STATE.md around lines 154 - 156, The note warns that the PR's FUSE-side defensive workaround (optional FileEntry fields and the v2→v1 fallback parsing logic) only masks the real bug where the web app writes v2-tagged metadata containing v1-style file entries; update the project tracking by creating and actively tracking the high-priority todo document named fix-v2-metadata-version-mismatch.md, and in code references locate symbols implementing the fallback parsing (e.g., FileEntry parsing/serializer functions and the v2→v1 fallback code paths) to add TODO comments linking to that todo file and ensure tests cover generation of v2-tagged metadata from the web app so the root cause can be fixed rather than relying on the FUSE fallback.apps/desktop/src-tauri/rust-toolchain.toml (1)
1-2: Consider pinningtargetsandcomponentsfor reproducible onboarding.The file only specifies
channel. For a Tauri macOS desktop app targeting both Apple Silicon and Intel, explicitly declaring the required targets and dev-tooling components in the toolchain file ensuresrustupinstalls everything needed on a clean clone — directly relevant to the open "Verify on clean clone" test plan item.♻️ Suggested extension
[toolchain] -channel = "1.88" +channel = "1.93" +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] +components = ["rustfmt", "clippy"]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/rust-toolchain.toml` around lines 1 - 2, The toolchain file only pins channel; update the [toolchain] section to also pin the required targets and components so rustup can install them on a clean clone: add the macOS targets (aarch64-apple-darwin and x86_64-apple-darwin) under a targets list and include dev components like rust-src, clippy and rustfmt under a components list (keeping channel = "1.88"); modify the rust-toolchain.toml entry named [toolchain] to include these targets and components so onboarding is reproducible.apps/desktop/src-tauri/vendor/fuser/examples/ioctl.rs (2)
132-132:readdirskips the negative-offset guard present inread.
read(line 101) explicitly rejectsoffset < 0before casting tousize. Here, a negativeoffsetsilently wraps tousize::MAX, causingskip()to exhaust all entries and return an empty listing rather than an error. FUSE doesn't send negativereaddiroffsets in practice, but the inconsistency is worth aligning.♻️ Proposed fix
+ if offset < 0 { + reply.error(EINVAL); + return; + } for (i, entry) in entries.into_iter().enumerate().skip(offset as usize) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/examples/ioctl.rs` at line 132, In the readdir implementation, reject negative offsets before casting instead of silently wrapping via enumerate().skip(offset as usize); add the same negative-offset guard used in read (check if offset < 0 and return the same error/result) and only then cast offset to usize to use in entries.into_iter().enumerate().skip(...), referencing the readdir function and the entries.into_iter().enumerate().skip(offset as usize) expression so behavior is consistent with read.
46-62:fioc_file_attrinitialblocks: 1is inconsistent withsize: 0.The file starts empty (
content: vec![],size: 0), soblocksshould be0to match. Until the firstFIOC_SET_SIZEcall,getattrfor inode 2 returns(size=0, blocks=1), which some tools may interpret as 512 bytes of allocated space.♻️ Proposed fix
let fioc_file_attr = FileAttr { ino: 2, size: 0, - blocks: 1, + blocks: 0,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/examples/ioctl.rs` around lines 46 - 62, The FileAttr for fioc_file_attr incorrectly sets blocks: 1 while size: 0, causing getattr to report allocated blocks for an empty file; update fioc_file_attr to set blocks: 0 (so blocks reflects size 0) — locate the fioc_file_attr initializer and change the blocks field from 1 to 0 (no other fields need adjustment, blksize can remain 512).apps/desktop/src-tauri/vendor/fuser/src/mnt/fuse3.rs (2)
33-33:CString::new(...).unwrap()— propagate the error instead of panicking.While null bytes in Unix paths are practically impossible,
unwrap()is still a panic site. Propagating the error with?(via a shortmap_err) is more consistent with theio::Resultreturn type of the enclosing function.♻️ Proposed fix
- let mnt = CString::new(mnt.as_os_str().as_bytes()).unwrap(); + let mnt = CString::new(mnt.as_os_str().as_bytes()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/src/mnt/fuse3.rs` at line 33, Replace the panic site CString::new(...).unwrap() with error propagation: use CString::new(mnt.as_os_str().as_bytes()).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))? so the conversion error is returned as an io::Result rather than causing a panic; locate the occurrence of CString::new and the mnt variable in the function that returns io::Result and apply this map_err + ? pattern.
17-24:ensure_last_os_erroris duplicated fromfuse2.rs— extract tomod.rs.The function body is byte-for-byte identical to the one in
apps/desktop/src-tauri/vendor/fuser/src/mnt/fuse2.rs. Moving it tomod.rs(or a small shared submodule) removes the duplication and keeps future fixes in one place.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/src/mnt/fuse3.rs` around lines 17 - 24, The ensure_last_os_error function is duplicated in fuse3.rs and fuse2.rs; extract it into the common module (mod.rs) or a small shared submodule and have both fuse2.rs and fuse3.rs import and call that single implementation. Move the function body (ensure_last_os_error) into mod.rs, make it pub(crate) if needed, remove the duplicate definitions from fuse2.rs and fuse3.rs, and add a use/import (or call via crate::mnt::ensure_last_os_error) in both files so they reference the shared implementation.apps/desktop/src-tauri/vendor/fuser/src/ll/request.rs (1)
580-583:RmDirunnecessarily exposespub nameas a direct field, inconsistent with all other operation structs.Every other operation struct (e.g.,
Unlink,Lookup,MkNod) keeps fields private and exposes them only through methods.RmDirhas both apub namefield and a redundantpub fn name()method. Since thename()accessor already exists, the field should be private to keep the API consistent and encapsulated.♻️ Proposed fix
pub struct RmDir<'a> { header: &'a fuse_in_header, - pub name: &'a Path, + name: &'a Path, }Note: the
Displayimplementation at line 1949 also bypasses the accessor (x.nameinstead ofx.name()), which can be updated tox.name()for consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/src/ll/request.rs` around lines 580 - 583, The RmDir struct exposes its name field publicly while also providing a pub fn name() accessor, breaking encapsulation and consistency with other request structs; change the field declaration in RmDir from pub name: &'a Path to a private name: &'a Path, keep the existing pub fn name(&self) -> &Path as the public accessor, and update any direct field uses (notably the Display implementation which currently uses x.name) to call x.name() instead so all access goes through the accessor.apps/desktop/src-tauri/src/registry/mod.rs (1)
66-67:now_ms()called twice —created_atandlast_seen_atmay differ by a few microseconds.Capture the value once and reuse it:
🔧 Proposed fix
+ let now = now_ms(); let device_entry = DeviceEntry { ... - created_at: now_ms(), - last_seen_at: now_ms(), + created_at: now, + last_seen_at: now,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/registry/mod.rs` around lines 66 - 67, The timestamps for created_at and last_seen_at are produced by two separate now_ms() calls which can differ; capture a single timestamp once (e.g., let ts = now_ms()) and reuse it for both fields when constructing the record so created_at and last_seen_at are identical; update the code around the struct/record initialization that sets created_at and last_seen_at to use that single variable instead of calling now_ms() twice.apps/desktop/src-tauri/src/commands.rs (1)
415-418:get_dev_keyis missing#[cfg(debug_assertions)]— it compiles into release builds as dead code.
handle_test_login_complete(line 428) is correctly gated.get_dev_keyis not, so it compiles in release but is only registered in the debuginvoke_handler. Add the cfg gate for consistency and to avoid returning adev_keyvalue that is alwaysNonein release.+#[cfg(debug_assertions)] #[tauri::command] pub async fn get_dev_key(state: State<'_, AppState>) -> Result<Option<String>, String> {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/commands.rs` around lines 415 - 418, get_dev_key is missing the debug-only gate and therefore compiles into release as dead code; add the same #[cfg(debug_assertions)] attribute used for handle_test_login_complete above the get_dev_key function so it only exists in debug builds and matches the debug-only registration in the invoke_handler, ensuring dev_key isn't present (or returned as None) in release builds.apps/desktop/src-tauri/src/crypto/hkdf.rs (1)
40-41:InvalidKeySizevariant is dead code — input type&[u8; 32]statically guarantees the correct size.Since all three public functions accept
&[u8; 32],HkdfError::InvalidKeySizecan never be constructed. Either remove it or change the internal helper to accept&[u8]to cover future callers that pass a slice.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/crypto/hkdf.rs` around lines 40 - 41, HkdfError::InvalidKeySize is dead code because all three public functions take &[u8; 32] so that variant can never be produced; either remove the InvalidKeySize enum variant and all references to it from HkdfError, or change the internal helper that performs the extract/expand (referenced as the internal HKDF helper used by the public functions) to accept a flexible &[u8] key parameter instead of &[u8; 32] so the variant becomes meaningful—update the helper’s signature and any callers, or delete the HkdfError::InvalidKeySize variant and its uses accordingly.apps/desktop/src-tauri/vendor/fuser/bsd_mount_tests.sh (1)
19-21: Unquoted path variables are susceptible to word-splitting.
$DIRand$3should be quoted. A temp directory with spaces (possible on some CI environments) would break thecargo runinvocation.🔧 Proposed fix
- cargo build --example hello $1 > /dev/null 2>&1 - cargo run --example hello $1 -- $DIR $3 & + cargo build --example hello "$1" > /dev/null 2>&1 + cargo run --example hello "$1" -- "$DIR" $3 &Also on line 28, quote the substitution inside
[[ ]]:- if [[ $(cat ${DIR}/hello.txt) = "Hello World!" ]]; then + if [[ "$(cat "${DIR}/hello.txt")" = "Hello World!" ]]; then🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/vendor/fuser/bsd_mount_tests.sh` around lines 19 - 21, The cargo run invocation uses unquoted path variables ($DIR and $3) which can break on paths with spaces; update the command in the example runner (the cargo run --example hello invocation) to quote both $DIR and $3 so the shell treats them as single arguments, and ensure any related variable expansions used in the surrounding logic (e.g., the conditional test around line 28 that uses [[ ... ]]) quotes the substitution inside the [[ ]] expression as well so word-splitting and globbing cannot occur.apps/desktop/src-tauri/src/fuse/operations.rs (2)
1981-2003: Hoist repeateduse unicode_normalization::UnicodeNormalization;imports to function scope.The same
usestatement appears in two separate blocks within therenamefunction. It can be declared once at the top of the function.♻️ Minor cleanup
+ use unicode_normalization::UnicodeNormalization; + // Remove source from old parent's name index (NFC-normalized) { - use unicode_normalization::UnicodeNormalization; let nfc_key: String = name_str.nfc().collect(); self.inodes.name_to_ino.remove(&(parent, nfc_key)); } // ... // Update the name lookup index for the new location (NFC-normalized) { - use unicode_normalization::UnicodeNormalization; let nfc_key: String = newname_str.nfc().collect();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/fuse/operations.rs` around lines 1981 - 2003, The repeated local imports of UnicodeNormalization inside the rename function are redundant; remove the two inner "use unicode_normalization::UnicodeNormalization;" lines and add a single "use unicode_normalization::UnicodeNormalization;" declaration at the top of the rename function body so both uses (the NFC normalization of name_str and newname_str when building nfc_key) share the same import and reduce duplication in the function where name_str, newname_str, parent, newparent, and the inodes.name_to_ino updates occur.
199-252:resolve_file_pointers_blockingresolves FilePointers sequentially — startup latency scales O(N).Each FilePointer triggers a separate
block_with_timeoutcall (3s timeout), and they're processed one-at-a-time. A folder with 20 files could add up to ~60s of sequential blocking before the mount is usable, depending on network latency.Since
rtis available, all resolutions can be dispatched concurrently with a singlert.block_on(futures::future::join_all(...)):♻️ Suggested refactor — parallel resolution
- for (ino, ipns_name) in unresolved { - let resolve_result = block_with_timeout(&rt, async { - let resp = crate::api::ipns::resolve_ipns(&api, ipns_name).await?; - let encrypted_bytes = crate::api::ipfs::fetch_content(&api, &resp.cid).await?; - Ok::<Vec<u8>, String>(encrypted_bytes) - }); - - match resolve_result { - Ok(encrypted_bytes) => { /* ... */ } - Err(e) => { log::warn!(...) } - } - } + let tasks: Vec<_> = unresolved.iter().map(|(ino, ipns_name)| { + let api = api.clone(); + let ipns_name = ipns_name.clone(); + let ino = *ino; + async move { + let result = tokio::time::timeout(NETWORK_TIMEOUT, async { + let resp = crate::api::ipns::resolve_ipns(&api, &ipns_name).await?; + crate::api::ipfs::fetch_content(&api, &resp.cid).await + }).await; + (ino, ipns_name, result) + } + }).collect(); + + let results = rt.block_on(futures::future::join_all(tasks)); + for (ino, ipns_name, result) in results { + match result { + Ok(Ok(encrypted_bytes)) => { /* decrypt and resolve ... */ } + Ok(Err(e)) | Err(_) => { log::warn!("FilePointer resolve failed for ino {} ({}): ...", ino, ipns_name, e) } + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src-tauri/src/fuse/operations.rs` around lines 199 - 252, resolve_file_pointers_blocking currently calls block_with_timeout sequentially for each (ino, ipns_name), causing O(N) startup latency; instead spawn all resolve+fetch tasks and await them concurrently via rt.block_on(futures::future::join_all(...)). Convert folder_key to folder_key_arr as before, then build a Vec of async closures that call crate::api::ipns::resolve_ipns, crate::api::ipfs::fetch_content, and decrypt_file_metadata_from_ipfs (or return the error), call rt.block_on(join_all(tasks)), and iterate results to call fs.inodes.resolve_file_pointer on successes and log per-item errors (preserving the current log messages). Ensure block_with_timeout is removed per-item and any timeout is applied around the whole join_all or per-task as desired.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.planning/STATE.md:
- Around line 208-209: Remove the stale conflicting "Next: Phase 13 (File
Versioning) or Phase 11 (Cross-Platform Desktop)" line from .planning/STATE.md
so only the current "Next: Phase 11.1 complete. Next phase TBD." remains; locate
the two "Next:" lines (the exact string "Next: Phase 13 (File Versioning) or
Phase 11 (Cross-Platform Desktop)" and "Next: Phase 11.1 complete. Next phase
TBD.") and delete the first to avoid contradictory next-step information.
In `@apps/desktop/src-tauri/rust-toolchain.toml`:
- Line 2: The toolchain is pinned to an old Rust channel value ("channel =
\"1.88\"") and should be updated to a more recent release; change the channel
string in rust-toolchain.toml from "1.88" to a newer supported value (e.g.,
"1.93" or at least "1.83"), or set it to "stable" if exact reproducibility is
not required, then commit the updated rust-toolchain.toml so the project builds
with the newer toolchain.
In `@apps/desktop/src-tauri/src/commands.rs`:
- Around line 94-103: The function complete_auth_setup currently accepts
private_key_bytes and public_key_bytes as plain Vec<u8>, leaving sensitive key
material unzeroed; change their types to zeroize::Zeroizing<Vec<u8>> in
complete_auth_setup and update uses (e.g., when constructing CipherBoxFS and
when creating reg_private_key for the registry spawn) to avoid cloning raw Vecs
— pass or clone the Zeroizing-wrapped values so drops zero the memory, and
update the callers handle_auth_complete and handle_test_login_complete to wrap
decoded bytes with Zeroizing::new(...) before calling complete_auth_setup.
In `@apps/desktop/src-tauri/src/crypto/hkdf.rs`:
- Around line 56-75: The function in hkdf.rs currently returns a plain Vec<u8>
for the derived private key (private_key = signing_key.to_bytes().to_vec()),
leaving secret bytes unzeroized on the heap; update the function signature to
return Zeroizing<Vec<u8>> for the private key (i.e., Result<(Zeroizing<Vec<u8>>,
Vec<u8>, String), HkdfError>), construct the private_key as
Zeroizing::new(signing_key.to_bytes().to_vec()), import zeroize::Zeroizing, and
then update all callers (e.g., initialize_vault and register_device) to accept
and propagate Zeroizing<Vec<u8>> instead of plain Vec<u8> so the secret buffer
is automatically zeroized on drop and no unzeroized clones remain.
In `@apps/desktop/src-tauri/src/fuse/mod.rs`:
- Around line 662-704: The current drain_refresh_completions implementation
blocks the FUSE thread by calling block_with_timeout/rt.block_on for every
unresolved FilePointer; instead, create a file_pointer_tx/file_pointer_rx
channel pair (analogous to refresh_tx/refresh_rx), and in
drain_refresh_completions replace the synchronous block_with_timeout call with
spawning background tasks via self.rt.spawn that perform
crate::api::ipns::resolve_ipns and crate::api::ipfs::fetch_content and then send
results (ino, enc_bytes or error) on file_pointer_tx; add a new
drain_file_pointer_results() invoked at the start of FUSE callbacks that reads
from file_pointer_rx and calls self.inodes.resolve_file_pointer(...) (or logs
errors); use a prefetching set keyed by IPNS name to avoid duplicate spawns when
iterating get_unresolved_file_pointers(), and keep resolve/decrypt logic
(operations::decrypt_file_metadata_from_ipfs_public) in the background task
before sending final resolved metadata to the channel.
- Around line 579-616: In flush_publish_queue, the safety-valve check must run
even when entry.pending_uploads > 0; change the filter so it publishes if either
elapsed >= safety_valve (regardless of pending_uploads) or (pending_uploads == 0
and elapsed >= debounce). Specifically, in the iterator over self.publish_queue
use entry.first_dirty and now to compute elapsed, then allow readiness when
elapsed >= safety_valve OR (entry.pending_uploads == 0 && elapsed >= debounce),
instead of returning early on pending_uploads > 0; keep references to debounce,
safety_valve, entry.pending_uploads, entry.first_dirty and the ready collection
logic in flush_publish_queue.
In `@apps/desktop/src-tauri/src/fuse/operations.rs`:
- Around line 773-779: Replace the four unconditional eprintln! debug prints in
the prefetch paths with structured logging: in the readdir prefetch block (the
match arm that prints ">>> prefetch(readdir)"), the open prefetch block (around
open()), the read() prefetch spawn (the spawned prefetch in read()), and the
read polling loop, change eprintln! to log::debug! and remove the ">>> " prefix
from the message; keep the same formatted values (e.g., plaintext.len() and
&cid_clone[..cid_clone.len().min(12)]) and message text otherwise so logs are
emitted via the log framework instead of writing directly to stderr. Ensure the
log crate is in scope where these prints are replaced.
- Around line 311-338: Wrap the heap key Vec returned by
crate::crypto::ecies::unwrap_key in zeroize::Zeroizing (e.g., let file_key =
Zeroizing::new(...)) so it is zeroed on every exit path, then obtain the
fixed-size file_key_arr from file_key.as_slice().try_into(); also ensure the
stack array is zeroed by using Zeroizing<[u8;32]> or calling zeroize::Zeroize on
file_key_arr after use (instead of relying on
crate::crypto::utils::clear_bytes). Remove the current explicit clear_bytes(&mut
file_key) and apply the same change in both fetch_and_decrypt_file_content and
fetch_and_decrypt_content_async (and the similar block at lines 354-378) so
early-return error paths zeroize secrets.
In `@apps/desktop/src-tauri/src/main.rs`:
- Around line 89-91: Replace the tokio async RwLock for the AppState dev_key
with a synchronous primitive (prefer std::sync::RwLock<Option<String>> or a
plain Option<String>) because setup runs on Tauri's Tokio runtime and mixing
blocking_read() / .await will panic; update all access sites: in main.rs where
you check state.dev_key.blocking_read().is_some() switch to
state.dev_key.read().unwrap().is_some(), in get_dev_key() return
state.dev_key.read().unwrap().clone(), and in zeroize_keys() replace the async
.write().await usage with a synchronous write (e.g. .write().unwrap()) or set
the Option directly; ensure all uses reference the dev_key field on AppState and
remove any tokio::sync::RwLock imports.
In `@apps/desktop/src-tauri/src/registry/mod.rs`:
- Around line 154-162: The UUID v4 formatting builds the third group incorrectly
because `bytes[6] & 0x0f` is printed with `{:02x}`, producing two hex digits and
making the third group 5 chars; update the formatting for that expression to use
a single-digit formatter (`{:01x}`) in each UUID generation site so the third
group becomes `"4" + one-nibble + bytes[7]` (4 chars total); apply this exact
change for the `uuid` format calls where `bytes[6] & 0x0f` appears (the three
occurrences in mod.rs) so the produced UUIDs are 36 characters and conform to v4
layout.
- Around line 150-208: The UUID-v4 formatting logic is duplicated in
get_or_create_device_id (three places); extract that logic into a new helper fn
generate_uuid_v4() -> String which calls
crypto::utils::generate_random_bytes(16) and returns the formatted UUID string,
then replace each inline format block in get_or_create_device_id with calls to
generate_uuid_v4(); keep existing behavior around keyring::Entry handling,
entry.get_password()/set_password()/delete_credential() and logging unchanged.
In `@apps/desktop/src-tauri/vendor/fuser/pjdfs.Dockerfile`:
- Line 15: Replace the Dockerfile's use of ADD for local file copies with COPY:
change the instruction "ADD rust-toolchain /code/fuser/rust-toolchain" to use
COPY, and similarly replace any other local-file ADD occurrences (e.g., the ADD
on the other noted occurrence) with COPY to avoid implicit side-effects like tar
extraction or remote fetching; update the Dockerfile so all local file/directory
copies use the COPY instruction instead of ADD.
In `@apps/desktop/src-tauri/vendor/fuser/src/ll/request.rs`:
- Around line 1297-1300: IoCtl::in_data currently slices self.data using
self.arg.in_size without bounds checks, which can panic if the kernel header is
wrong; change in_data to validate bounds and return a Result<&[u8], SomeError>
(or at least clamp to the available length and document it) instead of
panicking: check that (self.arg.in_size as usize) <= self.data.len(), return an
explicit error (e.g., IoctlError::InvalidSize) when it exceeds, and update all
callers of IoCtl::in_data to handle the Result (or the clamped slice)
accordingly so no unchecked slicing occurs; reference the IoCtl struct, the
in_data method, self.data, and self.arg.in_size when making the changes.
- Around line 1607-1613: system_time_from_time computes the wrong SystemTime for
negative secs when nsecs > 0; change the negative branch in
system_time_from_time so the nanoseconds are handled as an additive offset
rather than making the total larger (convert timespec to a signed nanosecond
total or adjust by borrowing one second): when secs < 0 and nsecs == 0 keep
UNIX_EPOCH - Duration::new((-secs) as u64, 0), but when secs < 0 and nsecs > 0
use UNIX_EPOCH - Duration::new(((-secs) as u64) - 1, 1_000_000_000 - nsecs) so
that secs = -1, nsecs = 500_000_000 yields EPOCH − 0.5s.
- Around line 2204-2213: The big-endian constant MKNOD_REQUEST currently has a
raw array literal assigned to the type AlignedData<[u8; 56]> causing a type
mismatch; change the constant to construct the tuple struct by wrapping the
array in AlignedData(...) (same pattern used by INIT_REQUEST and other
big-endian constants) so the value is AlignedData([0x00, 0x00, ... 0x00])
instead of a bare array literal.
In `@apps/desktop/src-tauri/vendor/fuser/src/mnt/fuse3.rs`:
- Line 46: Replace direct calls to io::Error::last_os_error() with the helper
ensure_last_os_error() to normalize errors when C functions return null/negative
but errno is 0; specifically change the return at the branch that currently does
return Err(io::Error::last_os_error()) (and the similar occurrence later) to
return Err(ensure_last_os_error()), keeping the same control flow and using the
existing ensure_last_os_error() function to avoid misleading "Success" messages.
- Around line 68-88: The Drop impl for Mount currently calls
fuse_session_destroy but skips fuse_session_unmount when super::libc_umount
returns an Err (non-PermissionDenied), which can leave a stale kernel mount;
modify Mount::drop so that in the error path (after logging warn!("umount failed
with {err:?}")) you call unsafe { fuse_session_unmount(self.fuse_session); }
before calling fuse_session_destroy(self.fuse_session) (keeping the existing
early-return behavior for the PermissionDenied/Linux branch that already
unmounts and destroys); this ensures the canonical teardown sequence
fuse_session_unmount -> fuse_session_destroy runs for all non-permission
failures.
---
Outside diff comments:
In `@apps/desktop/src-tauri/src/fuse/operations.rs`:
- Around line 89-95: The EncryptedFolderMetadata struct and parsing logic are
duplicated across mod implementation and public wrappers; extract a single
shared EncryptedFolderMetadata (or EncryptedIpfsPayload) and a helper like
parse_sealed_from_ipfs_bytes to perform the serde deserialization/IV+data
decoding, then update decrypt_metadata_from_ipfs,
decrypt_file_metadata_from_ipfs, decrypt_metadata_from_ipfs_public, and
decrypt_file_metadata_from_ipfs_public to call that helper and then their
respective crypto routines (AES-GCM decrypt call) so the struct and parsing
exist only once and the four functions become thin delegating wrappers.
- Around line 947-967: The writable-open path currently calls
fetch_and_decrypt_file_content which uses the global NETWORK_TIMEOUT (3s) and
causes EIO for large files; fix by adding a new longer timeout constant (e.g.,
WRITE_OPEN_DOWNLOAD_TIMEOUT ~30s) and use it for this code path when calling
fetch_and_decrypt_file_content (or its blocking helper block_with_timeout) so
open(O_WRONLY|O_RDWR) allows a longer download window; update the call-site in
operations.rs (the branch that checks cid.is_empty() and invokes
fetch_and_decrypt_file_content) to pass or apply WRITE_OPEN_DOWNLOAD_TIMEOUT
instead of the short NETWORK_TIMEOUT.
---
Duplicate comments:
In `@apps/desktop/src-tauri/src/crypto/aes_ctr.rs`:
- Around line 91-104: The panic on empty ciphertext is addressed by adding an
early guard: in the function using ciphertext, start_byte and end_byte (check
for AesCtrError::InvalidRange), keep the conditional that returns Ok(Vec::new())
when ciphertext.is_empty() || start_byte >= ciphertext.len(), then compute
clamped_end with ciphertext.len().saturating_sub(1) and return Ok(Vec::new()) if
clamped_end < start_byte; ensure these exact checks and the use of
saturating_sub remain in place to avoid underflow and out-of-range indexing.
In `@apps/desktop/src-tauri/src/fuse/operations.rs`:
- Around line 741-799: Prefetch logic is correct and doesn't need changes: keep
the proactive readdir prefetch in the block that checks offset == 0, retain the
dedup guard using self.prefetching.insert(...) and checks, continue spawning the
background task with rt.spawn calling fetch_and_decrypt_content_async, and
ensure both error and timeout branches still send
crate::fuse::PendingContent::Failure (and success sends PendingContent::Success)
so no cleanup/fix is required.
In `@apps/desktop/src-tauri/vendor/fuser/examples/ioctl.rs`:
- Around line 165-174: The previous ioctl handler for FIOC_SET_SIZE used an
unsafe unwrap causing panics and left file attributes stale; update the
FIOC_SET_SIZE arm in the ioctl handling code so it safely converts in_data via a
fallible pattern (e.g., use let Ok(bytes) = in_data.try_into() else {
reply.error(EINVAL); return; }) instead of try_into().unwrap(), then compute
new_size from usize::from_ne_bytes(bytes), resize self.content to vec![0_u8;
new_size], and update self.fioc_file_attr.size and self.fioc_file_attr.blocks
((new_size as u64 + 511) / 512) before calling reply.ioctl(0, &[]).
- Around line 100-111: No change required: the read handling correctly guards
against negative offsets and EOF and clamps the returned slice by using the
checks on offset (< 0 and >= self.content.len()), computing start/end with
offset and _size, and calling reply.data(&self.content[start..end]); leave the
logic around ino, offset, _size, self.content, reply.data, and
reply.error(ENOENT) as-is.
---
Nitpick comments:
In @.planning/STATE.md:
- Around line 154-156: The note warns that the PR's FUSE-side defensive
workaround (optional FileEntry fields and the v2→v1 fallback parsing logic) only
masks the real bug where the web app writes v2-tagged metadata containing
v1-style file entries; update the project tracking by creating and actively
tracking the high-priority todo document named
fix-v2-metadata-version-mismatch.md, and in code references locate symbols
implementing the fallback parsing (e.g., FileEntry parsing/serializer functions
and the v2→v1 fallback code paths) to add TODO comments linking to that todo
file and ensure tests cover generation of v2-tagged metadata from the web app so
the root cause can be fixed rather than relying on the FUSE fallback.
In `@apps/desktop/src-tauri/rust-toolchain.toml`:
- Around line 1-2: The toolchain file only pins channel; update the [toolchain]
section to also pin the required targets and components so rustup can install
them on a clean clone: add the macOS targets (aarch64-apple-darwin and
x86_64-apple-darwin) under a targets list and include dev components like
rust-src, clippy and rustfmt under a components list (keeping channel = "1.88");
modify the rust-toolchain.toml entry named [toolchain] to include these targets
and components so onboarding is reproducible.
In `@apps/desktop/src-tauri/src/commands.rs`:
- Around line 415-418: get_dev_key is missing the debug-only gate and therefore
compiles into release as dead code; add the same #[cfg(debug_assertions)]
attribute used for handle_test_login_complete above the get_dev_key function so
it only exists in debug builds and matches the debug-only registration in the
invoke_handler, ensuring dev_key isn't present (or returned as None) in release
builds.
In `@apps/desktop/src-tauri/src/crypto/hkdf.rs`:
- Around line 40-41: HkdfError::InvalidKeySize is dead code because all three
public functions take &[u8; 32] so that variant can never be produced; either
remove the InvalidKeySize enum variant and all references to it from HkdfError,
or change the internal helper that performs the extract/expand (referenced as
the internal HKDF helper used by the public functions) to accept a flexible
&[u8] key parameter instead of &[u8; 32] so the variant becomes
meaningful—update the helper’s signature and any callers, or delete the
HkdfError::InvalidKeySize variant and its uses accordingly.
In `@apps/desktop/src-tauri/src/fuse/operations.rs`:
- Around line 1981-2003: The repeated local imports of UnicodeNormalization
inside the rename function are redundant; remove the two inner "use
unicode_normalization::UnicodeNormalization;" lines and add a single "use
unicode_normalization::UnicodeNormalization;" declaration at the top of the
rename function body so both uses (the NFC normalization of name_str and
newname_str when building nfc_key) share the same import and reduce duplication
in the function where name_str, newname_str, parent, newparent, and the
inodes.name_to_ino updates occur.
- Around line 199-252: resolve_file_pointers_blocking currently calls
block_with_timeout sequentially for each (ino, ipns_name), causing O(N) startup
latency; instead spawn all resolve+fetch tasks and await them concurrently via
rt.block_on(futures::future::join_all(...)). Convert folder_key to
folder_key_arr as before, then build a Vec of async closures that call
crate::api::ipns::resolve_ipns, crate::api::ipfs::fetch_content, and
decrypt_file_metadata_from_ipfs (or return the error), call
rt.block_on(join_all(tasks)), and iterate results to call
fs.inodes.resolve_file_pointer on successes and log per-item errors (preserving
the current log messages). Ensure block_with_timeout is removed per-item and any
timeout is applied around the whole join_all or per-task as desired.
In `@apps/desktop/src-tauri/src/registry/mod.rs`:
- Around line 66-67: The timestamps for created_at and last_seen_at are produced
by two separate now_ms() calls which can differ; capture a single timestamp once
(e.g., let ts = now_ms()) and reuse it for both fields when constructing the
record so created_at and last_seen_at are identical; update the code around the
struct/record initialization that sets created_at and last_seen_at to use that
single variable instead of calling now_ms() twice.
In `@apps/desktop/src-tauri/vendor/fuser/bsd_mount_tests.sh`:
- Around line 19-21: The cargo run invocation uses unquoted path variables ($DIR
and $3) which can break on paths with spaces; update the command in the example
runner (the cargo run --example hello invocation) to quote both $DIR and $3 so
the shell treats them as single arguments, and ensure any related variable
expansions used in the surrounding logic (e.g., the conditional test around line
28 that uses [[ ... ]]) quotes the substitution inside the [[ ]] expression as
well so word-splitting and globbing cannot occur.
In `@apps/desktop/src-tauri/vendor/fuser/examples/ioctl.rs`:
- Line 132: In the readdir implementation, reject negative offsets before
casting instead of silently wrapping via enumerate().skip(offset as usize); add
the same negative-offset guard used in read (check if offset < 0 and return the
same error/result) and only then cast offset to usize to use in
entries.into_iter().enumerate().skip(...), referencing the readdir function and
the entries.into_iter().enumerate().skip(offset as usize) expression so behavior
is consistent with read.
- Around line 46-62: The FileAttr for fioc_file_attr incorrectly sets blocks: 1
while size: 0, causing getattr to report allocated blocks for an empty file;
update fioc_file_attr to set blocks: 0 (so blocks reflects size 0) — locate the
fioc_file_attr initializer and change the blocks field from 1 to 0 (no other
fields need adjustment, blksize can remain 512).
In `@apps/desktop/src-tauri/vendor/fuser/pjdfs.Dockerfile`:
- Line 1: Replace the mutable base image tag in the pjdfs.Dockerfile's FROM
instruction with an immutable digest-pinned image; locate the FROM ubuntu:22.04
line and change it to use the matching sha256 digest (e.g., FROM
ubuntu:22.04@sha256:<digest>) so builds are reproducible and protected from
upstream tag drift—ensure you fetch the correct digest for Ubuntu 22.04 from an
official registry and update the Dockerfile accordingly.
- Around line 5-6: The Dockerfile RUN layer that performs apt update && apt
install (the long RUN line installing git build-essential ... libfuse3-dev)
leaves apt lists cached and installs recommended packages; modify that same RUN
command to add --no-install-recommends to the apt install invocation and remove
the apt cache in the same layer by cleaning /var/lib/apt/lists/* (e.g., apt-get
clean && rm -rf /var/lib/apt/lists/*) so the image size is reduced and no extra
cache persists.
In `@apps/desktop/src-tauri/vendor/fuser/src/ll/request.rs`:
- Around line 580-583: The RmDir struct exposes its name field publicly while
also providing a pub fn name() accessor, breaking encapsulation and consistency
with other request structs; change the field declaration in RmDir from pub name:
&'a Path to a private name: &'a Path, keep the existing pub fn name(&self) ->
&Path as the public accessor, and update any direct field uses (notably the
Display implementation which currently uses x.name) to call x.name() instead so
all access goes through the accessor.
In `@apps/desktop/src-tauri/vendor/fuser/src/mnt/fuse3.rs`:
- Line 33: Replace the panic site CString::new(...).unwrap() with error
propagation: use CString::new(mnt.as_os_str().as_bytes()).map_err(|e|
std::io::Error::new(std::io::ErrorKind::InvalidInput, e))? so the conversion
error is returned as an io::Result rather than causing a panic; locate the
occurrence of CString::new and the mnt variable in the function that returns
io::Result and apply this map_err + ? pattern.
- Around line 17-24: The ensure_last_os_error function is duplicated in fuse3.rs
and fuse2.rs; extract it into the common module (mod.rs) or a small shared
submodule and have both fuse2.rs and fuse3.rs import and call that single
implementation. Move the function body (ensure_last_os_error) into mod.rs, make
it pub(crate) if needed, remove the duplicate definitions from fuse2.rs and
fuse3.rs, and add a use/import (or call via crate::mnt::ensure_last_os_error) in
both files so they reference the shared implementation.
- Wrap private key material in Zeroizing<Vec<u8>> (hkdf returns, commands param,
operations file_key) ensuring zero-on-drop for all exit paths
- Fix safety valve in flush_publish_queue: was unreachable because
pending_uploads > 0 returned false before safety_valve check
- Extract generate_uuid_v4() helper and fix format bug ({:02x} → {:01x}
for masked nibble producing 5-char group instead of 4)
- Replace eprintln!(">>> ...") debug prints with log::debug!
- Add TODO for blocking I/O in drain_refresh_completions
- Remove stale duplicate STATE.md line
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ADD → COPY in Dockerfile (Hadolint DL3020) - IoCtl bounds check with .min() to prevent OOB slice - Fix system_time_from_time for negative secs with nsecs > 0 - Fix big-endian MKNOD_REQUEST missing AlignedData wrapper - Use ensure_last_os_error() consistently in fuse3 mount - Add fuse_session_unmount fallback on non-EPERM umount failure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests