Conversation
pmaxhogan
force-pushed
the
feat/macos-apfs-wiring
branch
from
July 29, 2026 18:19
f92d46c to
37ac481
Compare
pmaxhogan
force-pushed
the
feat/macos-fda-onboarding
branch
from
July 29, 2026 18:20
51c69b4 to
ca05ccb
Compare
5 tasks
pmaxhogan
force-pushed
the
feat/macos-apfs-wiring
branch
from
July 29, 2026 18:30
37ac481 to
d19aaa9
Compare
pmaxhogan
force-pushed
the
feat/macos-fda-onboarding
branch
from
July 29, 2026 18:32
ca05ccb to
c7177a5
Compare
pmaxhogan
added a commit
that referenced
this pull request
Jul 29, 2026
## Summary Refreshes user-facing and design docs for the 2.5.0 feature set, verified against merged code (not PR descriptions) rather than restated from them. - **README + comparison table**: sharpens the macOS section - Full Disk Access is now documented as a separate permission from Gatekeeper, with an explicit statement that a locked-file snapshot is not a substitute for it and that Driven does not use the patched `-o noowners` (CVE-2020-9771) bypass. Adds footnote 41 to the "Locked / open-file backup" comparison row scoping the claim precisely to what's actually true today. - **design/DESIGN.md**: fixes two now-false "the maintainer has no Apple hardware" statements - Apple hardware is available and macOS-specific work this cycle (locked/permission-denied classification, the APFS broker, the template tray icon) was verified on it. Code signing (Apple Developer ID) is called out as the actual remaining blocker, separate from hardware access. - **design/ROADMAP.md + STRESS_HARNESS.md**: fixes several stale "skipped until M4 lands" / `if: false`-gated CI descriptions - the real-Drive gate flipped long ago (`chaos-real-drive` now runs on `v*` tag pushes and degrades to a clean skip without secrets, per `.github/workflows/chaos.yml` s not on a nightly schedule as previously documented, and `chaos-soak` is a local-only task, not a CI cron). Updates the "Beyond V1" list to mark the macOS VSS-equivalent and multi-backend items with their real current status. Adds brief post-GA status notes to both files so they read as the historical build logs they now are. - **design/IMPLEMENTATION.md**: adds a status note pointing to `design/E2E_REAL.md` for current e2e credential state, since this file's M4 OAuth preflight section is historical. - **Landing page** (`site-landing/index.html`): narrows the code-signing note to say what's actually verified (macOS-specific work tested on real Apple Silicon hardware), not a broader "builds are tested" claim. - **Draft-only sections**: README and the landing page each get an HTML comment block listing the still-unmerged features (S3 backend, local/ removable-folder backend, `driven-remote` crate, FDA onboarding UI, scheduled integrity scrub, restore drill, rclone config importer) so they can be uncommented individually as each PR lands, rather than guessed at. - CHANGELOG.md and version numbers were not touched (release-please owns them). ## Key correction - please read The task brief asked me to document that "locked-file backup via APFS snapshots works" on macOS. **It does not, yet.** `driven-apfs` (the broker crate from #196) is referenced only by the workspace `Cargo.toml` and its own files - nothing in `driven-core` or `driven-vss` calls into it. `crates/driven-vss/src/provider.rs:276-279` (`#[cfg(not(windows))] fn map_for_volume`) still unconditionally returns `SnapshotOutcome::Unavailable`, so every locked file on macOS still routes to a plain skip, exactly as before this cycle. This matches the brief's own "still in flight" list, which separately named "#201/#205 (macOS locked-file wiring + Full Disk Access onboarding UI)" as unmerged - the two halves of the brief were in tension, and I resolved it toward what the code (and the in-flight list) actually says. Docs here describe what shipped precisely: accurate locked-vs-denied *classification* (#195, real and working), not locked-file *backup* (still unwired). ## Test plan - [x] `git diff --check` - clean, no whitespace errors - [x] Swept the staged diff for non-ASCII on added lines; only pre-existing per-file conventions remain (README's superscript footnote numbers, design docs' `§` section markers) - no em-dashes or en-dashes anywhere in the diff - [x] `git ls-files --eol` on every touched file - all LF - [x] Docs-only change; no cargo/pnpm gates apply (nothing under `crates/`, `src-tauri/`, or `ui/` touched) - [ ] Human review of the "Key correction" section above and the draft-only blocks before merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Qu8GxMwkuxF7JBzwRjtcw7 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
pmaxhogan
force-pushed
the
feat/macos-fda-onboarding
branch
from
July 29, 2026 18:49
c7177a5 to
4518a88
Compare
pmaxhogan
force-pushed
the
feat/macos-apfs-wiring
branch
2 times, most recently
from
July 29, 2026 19:11
f431658 to
beb9dc4
Compare
pmaxhogan
force-pushed
the
feat/macos-fda-onboarding
branch
from
July 29, 2026 19:11
4518a88 to
fe3ea32
Compare
pmaxhogan
force-pushed
the
feat/macos-apfs-wiring
branch
from
July 29, 2026 19:16
beb9dc4 to
2219feb
Compare
pmaxhogan
force-pushed
the
feat/macos-fda-onboarding
branch
from
July 29, 2026 19:16
fe3ea32 to
106547f
Compare
pmaxhogan
force-pushed
the
feat/macos-apfs-wiring
branch
from
July 29, 2026 19:38
2219feb to
ce89cd4
Compare
…denied A TCC denial is invisible from the user's side: the backup "succeeds", some files just never reach Drive. Add a root-mounted dismissible banner that latches on the first local.permission_denied activity row, counts DISTINCT files (a denial is permanent, so the same file is re-reported every cycle, unbounded), and offers a one-click deep link to the Full Disk Access pane. The deep link needs an explicit opener capability scope: opener:default only permits mailto/tel/http/https, so a custom scheme would throw at runtime while every mocked test still passed. Document in README and DESIGN s5.3.3 that APFS snapshots do NOT bypass TCC, and that an unsigned binary's TCC grant can silently invalidate on update because macOS binds the grant to the code signature.
pmaxhogan
force-pushed
the
feat/macos-fda-onboarding
branch
from
July 29, 2026 19:39
106547f to
650ebab
Compare
pmaxhogan
added a commit
that referenced
this pull request
Jul 29, 2026
…ker (#201) Wires the `ApfsBrokeredProvider` from #196 into the app the way `BrokeredVssProvider` is wired on Windows, and ships the `driven-apfs-helper` broker as a Tauri sidecar on both darwin targets. **Stacked on #196** (base is `feat/macos-apfs-snapshot`, not `main`). It does NOT depend on #195. Rebase onto `main` once #196 squash-merges. ## Provider selection `assembly::build_vss` now has three arms instead of two - Windows VSS, macOS APFS, Linux `None`: - The macOS arm builds an `ApfsBrokeredProvider` over a single app-owned `ApfsHelperManager` (new, `src-tauri/src/apfs_helper.rs`), and the SAME `Arc` is coerced to `Arc<dyn HelperLauncher>` for every account's provider. One broker, one socket, one administrator prompt per session - the same sharing contract the Windows manager documents. - `build_apfs_helper_manager` mirrors `build_vss_helper_manager`: built at boot REGARDLESS of the setting, so flipping the toggle on takes effect with no app restart. A disabled manager short-circuits to `HelperLaunchStatus::Disabled` before ever consulting the launcher, so nothing prompts at silent startup. - `AppState` gains `apfs_helper` storage plus the quit-sweep `shutdown_apfs_helper()`, so no root process and no mounted snapshot outlives the session. `ApfsHelperManager` is deliberately thinner than `VssHelperManager`: the launch state machine (at-most-once osascript consent, background thread, memoised decline) already lives in `driven_apfs::OsascriptLauncher`, so the manager only adds the setting gate and session identity. A re-enable installs a *fresh* launcher, which is exactly how the crate documents clearing a memoised decline. ## Settings New `macos` group (SPEC s22), `macos.apfs_snapshot`, default `false`: - Additive and `#[serde(default)]` on both the DTO and the `storage::Macos` on-disk struct, so a settings DB written before this feature loads unchanged. No migration needed (the `windows` group is never seeded either - `load_group` returns `None` and the default fills in). There is a test for exactly this: `macos_group_absent_from_an_older_db_reads_as_off`. - Gated to macOS the same way `windows` is gated to Windows: `get_settings` returns the group as `None` off macOS, and that nullability IS the UI's platform check. No userAgent sniff. - **Wired through `reconfigure`**: the toggle drives the provider's `VssMode` (on -> `Auto`, off -> `Never`), so a change is applied between cycles by the existing `reconfigure` -> `VssProvider::set_mode` path. The toggle also (dis)arms the shared manager after persistence and fires the attended prompt eagerly, mirroring `windows.vss_helper`. The two mechanisms are belt-and-braces; either alone would disable the fallback. `get_apfs_helper_status` mirrors `get_vss_helper_status` so the toggle can show pending -> ready/declined. Without it a declined administrator prompt would fail silently, which is why it is in this PR rather than deferred. ## Sidecar build + ship Mirrors the Windows helper exactly: - New `src-tauri/tauri.apfs-helper.conf.json` carrying `externalBin: ["binaries/driven-apfs-helper"]`, merged only on darwin via the matrix `helperConfig` in both `release.yml` and `dev-channel.yml`. Same split-config trick as `tauri.helper.conf.json`: each OS's bundle references only its own sidecar, and the cargo-only CI gates (which never pass `--config`) stay free of both. - A `Build APFS helper sidecar (macOS)` step in both workflows prebuilds `-p driven-apfs --bin driven-apfs-helper` and stages it target-triple-suffixed into `src-tauri/binaries/` (`tauri build` does not build the helper bin itself). Source path is the workspace-root `target/`, verified empirically. - Verified that Tauri's `copy_binaries_to_bundle` places an externalBin in `Contents/MacOS/` - the same directory as the app binary - so the Windows `current_exe().parent()` resolution works unchanged on macOS. - `.gitignore`'s `/src-tauri/binaries` comment updated to cover both helpers. ## Gates - `SQLX_OFFLINE=true cargo test --workspace` - green - `cargo clippy --workspace --all-targets -- -D warnings` - green - `cargo fmt --all -- --check` - green - `pnpm --dir ui lint && test:unit && build` - green (531 UI tests). `format:check` also green. - `actionlint` clean on both edited workflows (CONTRIBUTING requirement). - New coverage: 11 Rust tests (6 manager gate tests + 5 settings tests) and 5 UI component tests. `Settings.vue` coverage went 94.6 -> 95.23 %stmts, 88.88 -> 90.05 %branch, so the coverage gate has no new uncovered branches. ## Things the reviewer should know - **This PR contains a 2-hunk `cargo fmt` fix to `crates/driven-apfs/tests/broker_integration.rs`, which is #196's file, not mine.** `cargo fmt --all -- --check` fails on #196 as it currently stands. I did not otherwise touch that crate. Drop those hunks if #196 fixes it first. - **No local dev path for the sidecar exists, on either platform.** `cargo tauri dev` never passes `--config`, so it never resolves `externalBin`; the Windows helper has the same gap. That means the enable-toggle's end-to-end launch (osascript prompt -> broker -> mount) is NOT exercisable from a dev build, and I have not run it. What I did verify on macOS 26.6 hardware: `tmutil localsnapshot` succeeds with no Time Machine destination configured (so the feature does not require TM to be set up), and the helper binary builds and links on darwin. - **macOS bundles are not signed or notarized today** (no `APPLE_*` secrets in `release.yml`). Adding an externalBin to an unsigned `.app` builds fine, but a nested sidecar Mach-O will need its own signature the moment notarization is turned on. - `lockedFileBackupDegraded` is computed and exposed but not yet rendered as a banner; the Windows side has one. Deliberate - the FDA/denial surface is #PR2's job and a second degraded banner there would compete with it. --- **Follow-up commit `f92d46c` (post-review self-catch):** reading the helper status must never trigger the administrator prompt. `OsascriptLauncher::launch_status()` is not a pure read - its `NotAttempted -> InFlight` transition is exactly what spawns the consent thread. The status accessors are polled every time the Settings Rules tab opens, so delegating to it blindly meant that merely opening Settings would pop a password prompt the user never asked for. The manager now tracks whether a launch was genuinely triggered (by the provider's locked-file path or the enable-toggle) and answers "not yet tried" without touching the launcher until then. The same commit fixes `helper_launchable()` to count not-yet-attempted as launchable, matching the Windows twin - it previously reported `locked_file_backup_degraded: true` before any launch attempt even with the setting on and the sidecar present, contradicting its own doc comment. Regression test: `reading_status_never_triggers_a_consent_prompt` reads every status accessor three times and asserts the launcher never goes in-flight. --- **Note on CI:** `ci.yml`, `coverage.yml` and friends are declared `on: pull_request: branches: [main]`, so a PR whose base is a feature branch gets NO checks beyond the PR-title gate. That is why every gate in this description was run locally on this machine and reported verbatim rather than pointed at a green CI run. Retargeting this PR to `main` after the parent PRs merge will trigger the real matrix. --- # Rebased onto `main` after #196 merged (`a5f105e`) Re-cut from `origin/main` and re-applied rather than rebased through the squash-merge, so the diff is only my own work. The `cargo fmt` hunk in `crates/driven-apfs/tests/broker_integration.rs` that this PR previously carried is **gone** - it landed upstream, so that file is no longer touched here. **Compatibility with #196's security review: verified, no changes needed.** I never called the removed `Control::DeleteSnapshot` / `client.delete_snapshot`; the APIs this wiring uses are all intact (`client` still `#[cfg(target_os = "macos")]`, `HelperClient::shutdown()` present, `ApfsBrokeredProvider::new(Arc<dyn HelperLauncher>, VssMode)` unchanged, `launch.rs` untouched). One unrelated fix was needed: main's proxy-credential-redaction PR added a second `SettingsDto` initializer that needed the new `macos` field. ## New: a bug that would have made this feature 100% non-functional **The session socket path did not fit in `sockaddr_un`.** Darwin's `sun_path` is 104 bytes, and macOS points `TMPDIR` at a per-user `/var/folders/<xx>/<~20 chars>/T/`. My `runtime_dir()` used `std::env::temp_dir()`, producing: ``` /var/folders/s8/r0zq_8jj4nn8kzjdt8p3453c0000gn/T/driven-apfs/driven-apfs-<32 hex>.sock = 110 bytes (limit 104, so 6 over) ``` I found this by running the real broker binary, which died with `path must be shorter than SUN_LEN` before serving a single request. This would have happened on **every** Mac. No test caught it because nothing in the suite binds a socket - my tests only checked the path's *shape*. Fixed by moving the runtime dir to `/tmp/driven-apfs-<uid>` (mode `0700`), which brings the full path to 70 bytes. `/tmp` is world-writable but sticky and the per-uid dir is `0700`, so another user can neither read the socket nor replace the dir; if they pre-create it, it is owned by them and the broker's own "socket parent must be owned by the peer uid with mode 0700" check refuses to serve - fails closed. Two regression tests added (`socket_path_fits_in_sockaddr_un`, `runtime_dir_avoids_the_long_per_user_tmpdir`). ## The end-to-end gap is now closed - the broker chain WORKS Using the merged broker binary, a root-owned bundle dir, and a co-installed client harness, the full chain ran on this macOS 26.6 machine: ``` == 1. create an APFS local snapshot (UNPRIVILEGED, tmutil) == created snapshot date = 2026-07-29-132651 == 2. connect to the root broker (client verifies peer is uid 0) == connected, peer authenticated as root == 3. broker-mount the snapshot for /System/Volumes/Data == mounted at /private/var/run/driven-apfs-mounts-96237/m0 == 4. read the probe file THROUGH the snapshot (un-elevated) == mapped -> /private/var/run/driven-apfs-mounts-96237/m0/Users/pmaxh/driven-apfs-e2e-probe.txt READ OK: 54 bytes, first line = "PROBE CONTENT written at Wed Jul 29 13:26:50 CDT 2026" == 5. unmount everything == unmounted == 6. delete the snapshot (UNPRIVILEGED post-#196) == deleted ``` Zero leftover snapshots, zero leftover mounts afterwards. The firmlink mapping resolved `~/…` to `/System/Volumes/Data` correctly, and #196's new root-owned audit log was written as designed. ## BLOCKER for shipping: the install layout defeats the broker's own guard #196's broker refuses to serve if the helper's directory is writable by the client uid. Measured on this machine: | App | `Contents/MacOS` owner | Broker would serve? | |---|---|---| | AltTab, Audacity, Chrome, Claude, Discord, Docker, GIMP (all DMG drag-installs, in `/Applications`) | `pmaxh` (me) | **NO** | | Cloudflare WARP (pkg), GarageBand (App Store), system apps | `root:wheel` | yes | I confirmed the refusal by running the real broker from a user-owned dir: ``` driven-apfs-helper: refusing to serve: the helper's directory is writable by the client user, so co-installation cannot authenticate a peer ``` **Driven ships a `.dmg` and the README tells users to drag it to `/Applications`, which produces a user-owned bundle. So on a stock install this feature is inert**: the user toggles it on, approves the administrator prompt, and the broker exits at startup. Note the guard's own comment anticipates `~/Applications` / `~/Downloads`; the empirical reality is broader - plain `/Applications` drag-installs are user-owned too. Per the brief I am **not** working around this - it is a deliberate security property, and the honest fix is packaging (a `.pkg`, or a post-install step that leaves the bundle root-owned), which is out of scope here. It is documented in DESIGN s5.3.2 and, user-facing, in the README (PR #205). One related rough edge, not fixed here: `osascript` returns success as soon as the detached broker is spawned, so the launcher records `Ready` even when the broker then exits immediately. The user gets a password prompt and a "working" status while nothing mounts. Worth a follow-up (probe the socket before declaring `Ready`). ## Gates re-run after the rebase (all local - see CI note below) `cargo test --workspace` 41 suites / exit 0 - `cargo clippy --workspace --all-targets -D warnings` clean - `cargo fmt --all --check` clean - UI lint 0 errors, 535 tests, build + `format:check` clean. 14 apfs-specific Rust tests pass. --- # Correction: the helper-dir refusal is advisory now (#211), and the docs above are superseded **Everything above that says this feature is "inert on a stock install" is no longer true** and has been rewritten in DESIGN s5.3.2. #211 downgraded the broker's co-installation helper-dir check from fatal to advisory, so **locked-file backup works on a normal drag-installed `.app`**. I am leaving the earlier text in place for the audit trail rather than silently editing it; DESIGN and the README (in #205) carry the accurate version. The accurate story, not overstated in either direction: - The co-installation check ("the caller's executable sits next to mine") is only strong if the peer cannot write to the helper's directory. On a drag-installed bundle they can, and that is the normal case, not the exotic one - measured across 7 real apps, including in `/Applications`. - So the broker now **serves** and writes a `DEGRADED:` line to its root-owned audit log naming the directory and the uid. - The residual risk is small because the attacker is already the same uid (`getpeereid` is not bypassable by planting a binary), and what defeating co-installation buys them is a read-only, `nosuid`, `nodev`, ownership-preserving mount of an allow-listed volume at a broker-chosen mountpoint - a point-in-time copy of files that uid could already read, with TCC still applying to their own process. - The load-bearing checks (peer uid, volume allow-list, snapshot-name validation, mount options) are untouched. - A `.pkg`-style root-owned install restores the check to full strength and silences the `DEGRADED:` line. It is an improvement, not a prerequisite. My empirical measurements stand and are what motivated the downgrade; only the consequence changed. ## Also fixed here: `Ready` no longer means "a process was spawned" This was the rough edge I flagged, and it is small enough to fix rather than park (~45 lines plus tests, contained entirely in `src-tauri/src/apfs_helper.rs`). `osascript` exits 0 as soon as the consent prompt resolves and the shell backgrounds the broker, so the launcher recorded `Ready` from the **spawn alone**. A broker that then died immediately - failed pre-flight, bad sidecar, unbindable socket path - left the user with an administrator prompt followed by a healthy-looking status while nothing ever mounted. That is precisely the silent shape my `sun_path` overflow produced, so this is the difference between a bug found in minutes and one found never. The manager now probes the socket before passing `Ready` upward: - **Connect-and-drop liveness, not existence.** A dead broker leaves its socket *file* on disk; connecting to that stale file fails `ECONNREFUSED`. A test asserts exactly this distinction by binding a real `UnixListener`, probing it live, dropping it, and probing the leftover file. - **An unbacked `Ready` reports `Pending`** (a transient skip, retried next cycle) for a 15s grace window covering the bind, **then `Disabled`** - which makes `helper_launchable` false and surfaces as degraded in the UI, rather than an eternal `Pending` nobody can diagnose. A warn line names the socket. - **Probing stops at the first success**, so a healthy session costs a couple of connects, not one per status poll. - It is not authentication and does not pretend to be - the client's own root-peer check does that. Three new tests (`socket_probe_sees_a_live_listener_and_rejects_a_stale_file`, `a_ready_launcher_without_a_live_socket_is_not_reported_ready`, `an_unbacked_ready_becomes_disabled_once_the_grace_window_expires`), bringing the apfs-specific Rust tests to 17. One thing deliberately NOT asserted, and the comment in the test says so: the end-to-end `Disabled -> helper_launchable false -> degraded` mapping. Reaching it needs `launch_attempted` set, which makes the accessors call the real launcher, whose `NotAttempted` transition spawns a genuine osascript consent prompt. A test that popped a password dialog would be worse than the coverage is worth; `reading_status_never_triggers_a_consent_prompt` guards that boundary instead. ## Gates after these changes `cargo test --workspace` 41 suites / exit 0 - clippy `-D warnings` clean - `cargo fmt --all --check` clean - UI lint 0 errors, build + `format:check` clean. The previous commit's full CI matrix (17 checks incl. all three `cargo test + clippy` OSes, all three `tauri compile`, Coverage, Chaos, CodeQL) passed green; this push re-runs it. --- # Rebased onto current `main` (`2df3540`), and reconciled with #209's docs refresh `a5f105e` is an ancestor of `main`, so this was a plain `git rebase origin/main`, not another re-cut. Two conflicts, both trivial: an import-list collision in `ui/src/ipc/commands.ts` (kept both sides) and `Cargo.lock` (regenerated from main's). **#209 landed a docs refresh while this was in flight, and it describes this feature as not existing.** Since #201 is what makes those statements false, correcting them belongs here, not in a follow-up: - **Note 41** said "a macOS APFS-snapshot bypass ... exists as a broker crate but is not yet wired into the backup path, so it does not do anything for a user yet". Rewritten: macOS now has the equivalent behind the opt-in Settings > Rules toggle, off by default, and it does nothing for an FDA denial. I also fixed a contradiction I briefly introduced there ("neither macOS nor Linux backs a file up through a lock today" immediately followed by "macOS additionally has ..."); the note now leads with what macOS does and ends with Linux having no equivalent. - **The feature bullet** ("an APFS-snapshot bypass ... is in development for macOS but not yet available") now describes the shipped opt-in. - **The FDA section's cross-reference** ("a macOS APFS-snapshot equivalent is in development, see the table above") now points at the real section below it. New README section **"macOS locked-file backup (APFS snapshot, opt-in)"**, with the two-row table splitting `local.file_locked` (fix: the toggle) from `local.permission_denied` (fix: FDA, nothing else), a note that it works without Time Machine configured (verified on hardware), and the drag-install advisory blockquote in its corrected, non-alarmist form. I deliberately did NOT re-add my own "not a substitute for Full Disk Access" paragraph: #209 already added an equivalent one, better placed, including the CVE-2020-9771 history. Duplicating it would have been noise. ## Note on the local test run `cargo test --workspace` cannot complete on this machine any more, and it is not this branch's doing: #200's new `driven-backend` crate has two keychain tests (`account_creds_prefer_the_keychain_record_and_fall_back_to_the_env_seam`, `build_store_returns_a_drive_store_once_a_refresh_token_is_stored`) that block on an invisible macOS `SecurityAgent` prompt (I confirmed the process is running while they hang). They pass on CI, and this branch does not touch `driven-backend` - `git diff origin/main --name-only` returns zero files under it. So the local run reported here is `cargo test --workspace --exclude driven-backend`: **43 suites, exit 0**, including all 13 `apfs_helper` tests and the 4 settings tests. `driven-backend` is covered by CI's macos/ubuntu/windows matrix on this PR. Other gates after the rebase: clippy `-D warnings` clean, `cargo fmt --all --check` clean, UI lint 0 errors, 545 tests, build + `format:check` clean. --- # Rebased onto `main` with #211, #207 and #209 (`65010ac`) Plain `git rebase origin/main`, no conflicts. ## Re-ran the full end-to-end against the merged #211 broker, from a USER-OWNED directory This is the case that previously refused to serve, i.e. exactly what a DMG drag-install produces. It now works end to end: ``` === helper dir (user-owned, as a drag-install produces) === pmaxh:wheel mode=755 .../e2e/userowned broker up (it now SERVES from a user-owned dir) 1. create an APFS local snapshot (UNPRIVILEGED, tmutil) -> 2026-07-29-143527 2. connect to the root broker -> peer authenticated as root 3. broker-mount the snapshot for /System/Volumes/Data -> /private/var/run/driven-apfs-mounts-37329/m0 4. read the probe file THROUGH the snapshot -> READ OK: 54 bytes, content correct 5. unmount everything -> unmounted 6. delete the snapshot (unprivileged) -> deleted ``` Zero leftover snapshots, zero leftover mounts. So the downgrade achieves what it set out to: the feature is live for ordinary installs, and my DESIGN wording matches the merged implementation's audit string verbatim (`DEGRADED: helper directory {} is writable by uid {}`). One caveat on my own evidence: the script's audit-log dump came back empty on this run because the per-session mount root is torn down before it reads it, so I did **not** independently observe the `DEGRADED:` line in this particular run - I am relying on the merged code path for that, not on my own observation. Everything else above I watched happen. ## The earlier ubuntu CI failure was not this branch The previous run showed `cargo test + clippy (ubuntu-latest)` failing on `driven_backend::tests::build_store_needs_reauth_for_a_drive_account_with_an_empty_keychain` with `org.freedesktop.secrets was not provided by any .service files`. That branch was based on `2df3540`, which predates #207 - the test had no mock guard yet. On the current base it carries `let Some(_g) = keychain() else { return };` and passes. Everything else in that run was green, including all three `tauri compile` jobs and `cargo test + clippy` on macOS and Windows. ## Local gates now run WITHOUT the `--exclude driven-backend` workaround With #207 in the base I re-ran the real thing and watched for the prompt while it ran: - `SQLX_OFFLINE=true cargo test --workspace` - **48 suites, exit 0, no exclusions** - Both formerly-hanging keychain tests now pass against the in-memory store (`build_store_needs_reauth_for_a_drive_account_with_an_empty_keychain ... ok`, `account_creds_prefer_the_keychain_record_and_fall_back_to_the_env_seam ... ok`) - `pgrep SecurityAgent` was empty throughout - **no keychain prompt was raised** Also green: clippy `-D warnings`, `cargo fmt --all --check`, UI lint 0 errors, 552 UI tests, build, `format:check`.
pmaxhogan
added a commit
that referenced
this pull request
Jul 29, 2026
…denied (#216) Reopened: the original #205 was auto-closed when its base branch `feat/macos-apfs-wiring` was deleted on #201's merge. Same content, rebased onto `main` so it now gets the full CI matrix (a stacked PR only ever ran the title check). Completes the macOS locked-file story started by #195 (classification), #196 (the APFS snapshot broker) and #201 (wiring). Those handle files a *lock* blocks; this handles the other half - files macOS PRIVACY blocks. ## What it does When a cycle skips files with `local.permission_denied`, the UI surfaces a dismissible hint explaining that macOS privacy protection (TCC) is blocking them, with a button that opens System Settings directly at the Full Disk Access pane. The deep link is `x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles`, verified four ways on macOS 26.6 rather than assumed: 1. Opening it lands on a window titled exactly `Full Disk Access`. 2. Control: the same URL with a bogus anchor lands on plain `Privacy & Security`, so the anchor is load-bearing rather than a false positive. 3. It still navigates correctly when System Settings is already open on another pane. 4. Because `openUrl` goes through the plugin's scope rather than `open(1)`, the glob was checked directly: it matches the target and rejects `file:///etc/passwd`, `https://evil.example` and `x-apple.systemprefsX:y`. ## Two bugs caught that tests could not have - **`opener:default` would have silently blocked the link.** Its scope permits only `mailto:`/`tel:`/`http`/`https`; a custom scheme throws at runtime while every mocked test passes. Fixed with a scoped `opener:allow-open-url` entry, confirmed present in the generated `capabilities.json`. - **Reading helper status fired the admin password prompt.** `launch_status()` is not a pure read - its `NotAttempted -> InFlight` transition is what spawns consent. Status accessors are polled whenever the Settings tab opens, so merely opening Settings would have asked for a password. Fixed with a regression test. ## The accuracy point this PR exists to get right A snapshot cannot substitute for Full Disk Access, and no doc here implies otherwise. The `-o noowners` bypass was CVE-2020-9771, is patched, and is now an EDR-flagged signature - Driven never uses it. So the README carries a table splitting `local.file_locked` (fix: the APFS toggle) from `local.permission_denied` (fix: FDA, and nothing else will do). Also documented: because Driven ships unsigned, a granted FDA can silently lapse when the binary is rebuilt, since macOS ties the grant to code identity. ## Reconciled with #209 #209's docs refresh landed mid-flight saying "there is currently no in-app guided flow - planned for a future release". Rather than adding a second FDA section, this edits #209's in place, adds the grant steps and the unsigned-binary caveat it lacked, keeps its "not a substitute for FDA" paragraph, and flips its `DRAFT - do not uncomment` roadmap bullet live. Banner dismissal is per-session rather than persisted: `localStorage` appears nowhere in `ui/src`, and the condition is genuinely unresolved until FDA is granted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Full Disk Access onboarding surface for macOS TCC denials.
Stacked on #201 (base is
feat/macos-apfs-wiring), which is itself stacked on #196. Rebase ontomainonce those land.It also depends on #195 at runtime, but shares no lines with it. #195 is what makes the executor emit
local.permission_denied; this PR is the UI that reacts to that code. The banner keys off the code as a plain string inentry.eventType, so nothing here needs #195 to compile or to pass tests - but the banner stays inert until #195 merges. I deliberately did NOT add theerrors.local.permission_deniedi18n entry, because #195 adds exactly that key and duplicating it would guarantee a conflict. All new strings live under a newfdaBannernamespace.Why a banner at all
A TCC denial is invisible from the user's side: the backup "succeeds", certain files just never appear in Drive. The activity row alone does not tell anyone what to do about it. So the UI has to name the problem and point at the fix.
Detection
A root-mounted
FdaBannersubscribes toactivity:new(theToastHostpattern, so a denial during a background cycle is not missed because the user was on another tab) and latches on the first row whoseeventTypeislocal.permission_denied.No per-cycle bookkeeping: a denial is PERMANENT until the user acts, unlike a lock, so "has this ever happened" is the right question. Gating on
backup_donewould have been wrong twice over - failed ops suppress that row, and it is emitted per source rather than per cycle.Dedupe
Since a denial is permanent, the same file emits one warn row EVERY cycle, forever. The banner counts DISTINCT files rather than rows, so a file denied across fifty cycles is reported once. Kept entirely in the display layer - the executor's emission and the activity store are untouched, so the raw rows stay available for diagnostics.
The deep link, and the trap under it
Button opens the FDA pane directly with:
Verified on this macOS 26.6 (25G72, arm64) machine, three ways:
Full Disk Access.?Privacy_BogusAnchorXYZ) lands onPrivacy & Securityinstead - so the anchor is genuinely load-bearing and the result is not a false positive.Full Disk Accessrather than no-opping.opener:defaultwould have blocked this. Its scope only permitsmailto:,tel:,http://,https://, soopenUrlwith a custom scheme throws at runtime while every mocked test still passes.src-tauri/capabilities/default.jsonnow carries an explicit scoped entry:{ "identifier": "opener:allow-open-url", "allow": [{ "url": "x-apple.systempreferences:*" }] }Verified end to end: the entry is present in the generated
src-tauri/gen/schemas/capabilities.json, andglob::Pattern::new("x-apple.systempreferences:*")(the same glob crate the opener plugin matches with) returnstruefor the exact target URL andfalseforfile:///etc/passwd,https://evil.example, andx-apple.systemprefsX:y- so the scope admits the pane and nothing else.The unsigned-binary caveat (real, recurring)
macOS binds a TCC grant to the binary's code signature (cdhash), not its path. Driven's V1 builds are unsigned, so every update is a different program as far as TCC is concerned, and a working grant can silently stop applying - the app still shows in the Full Disk Access list with its switch on while being denied. This is stated in one line in the banner copy and explained with the remove-and-re-add fix in the README.
Docs
local.file_locked(fix: APFS snapshot toggle) fromlocal.permission_denied(fix: FDA, nothing else).Accuracy point held throughout: APFS snapshots do NOT bypass TCC. A snapshot mount preserves the original's ownership and is itself TCC-gated, and the
-o noownersbypass was CVE-2020-9771 - patched, and now an EDR-flagged signature Driven deliberately does not emit. Nothing in these docs implies snapshots substitute for FDA; the README table makes the split explicit.Judgment calls
localStorage/sessionStorageappear zero times inui/src, and the only dismissible-banner precedent (stores/updater.ts) is an in-memory Pinia ref, so this matches it. It is also the better semantic: the condition is unresolved until FDA is granted, so permanently silencing it would hide an ongoing data-coverage gap. Once granted, the denials stop and the banner stops on its own. A persisted flag would have meant a new settings-schema field, and this stack already adds one.local.permission_deniedcan only be emitted on macOS, so the banner is inherently mac-only - the same way the VSS banner derives purely from backend state rather than sniffing the userAgent.query_activitybackfill would show it a little sooner and is an easy follow-up, but it is not needed for correctness.Gates
pnpm --dir ui lint,test:unit,build,format:check- all greenSQLX_OFFLINE=true cargo test --workspace,cargo clippy --workspace --all-targets -- -D warnings,cargo fmt --all -- --check- all greenFdaBanner.vuehas a vitest mount test (coverage gate), covering hidden-by-default, show-on-denial, ignore-other-codes, dedupe, multi-file count, the exact deep-link URL string, dismissal stickiness, and the unsigned-binary note.Not verified
The in-app path is not exercised end to end.
cargo tauri devnever passes--config, so there is no dev bundle with the sidecar, and the UI tests mock@tauri-apps/plugin-opener- they prove the button callsopenUrlwith the right string, not that the call succeeds inside a running Driven. What IS verified is each link in that chain independently: the URL against real System Settings with a control, the capability entry in the generated ACL, and the glob match against the shipped pattern.Note on CI:
ci.yml,coverage.ymland friends are declaredon: pull_request: branches: [main], so a PR whose base is a feature branch gets NO checks beyond the PR-title gate. That is why every gate in this description was run locally on this machine and reported verbatim rather than pointed at a green CI run. Retargeting this PR tomainafter the parent PRs merge will trigger the real matrix.Update: #195 merged to
main(08d2864) while this was being written. That is the PR that makes the executor emitlocal.permission_denied, so once this stack is rebased ontomainthe banner is live rather than inert. Nothing here needs changing - this PR still shares no lines with #195, and deliberately does not define theerrors.local.permission_deniedi18n key that #195 already added.Cross-checked the predicate against merged
main, since a wrong literal would make this banner silent dead code that no gate in this repo would catch:crates/driven-core/src/types.rsonmain:ErrorCode::LocalPermissionDenied => "local.permission_denied"(and the matchingfrom_codearm).crates/driven-core/tests/e2e_fake.rsonmain(fix(core): classify macOS locked and permission-denied opens into the skip-and-report path #195's own e2e): queriesevent_types: vec!["local.permission_denied".to_string()]and asserts exactly one warn row for the one unreadable file.So the activity row's
event_typeis exactly the string this banner matches on.Merge-conflict warning for whoever resolves this one:
ui/src/__tests__/app-shell.test.tscontains a single-line change,expect(listenMock).toHaveBeenCalledTimes(8)->(9), because mountingFdaBannerat the app root adds onelistenregistration. That number is an arithmetic invariant, not a preference. If another PR also adds a root-level subscription and touches the same line, the resolution must be the SUM of both additions, not either side's value. Picking one side would silently lose a subscription's worth of coverage while still passing.Rebased onto the re-cut #201 (which is now based on
main)Re-cut and re-applied rather than rebased through #196's squash-merge. Retarget this to
mainonce #201 lands.#195 is on
mainnow, so thelocal.permission_deniedrows this banner keys off are really emitted - the banner is live, not inert. Still shares no lines with #195 and still does not define theerrors.local.permission_deniedkey that #195 owns.The
app-shell.test.tslistener count is still correct arithmetic after the rebase:mainis at 8, this PR is at 9 (+1 forFdaBanner). Main's exclusion-preview listener fix (#206) did not add a root-levelApp.vuesubscription, so no adjustment was needed. The merge-conflict warning above still applies.Added: a user-facing note that this feature is inert on a stock install
While verifying #201's sidecar work I found that #196's broker refuses to serve when the helper's directory is writable by the client uid - and every DMG drag-install produces exactly that, including into
/Applications(measured across 7 real apps on macOS 26.6; only pkg/App-Store installs areroot:wheel). Confirmed by running the real broker binary from a user-owned directory and getting its refusal.Since the README is where a user would look, the "What locked-file backup covers on macOS" section now carries a blockquote saying plainly that the APFS snapshot setting does not work from a drag-installed
.app, why (the helper cannot authenticate a caller who could plant a binary beside it), and that fixing it needs a.pkg-style install that leaves the bundle root-owned. It also says this is a deliberate safety property, not a bug - so nobody "fixes" it by weakening the check.This does not affect the FDA banner itself, which is about TCC denials and works regardless of install layout.
Gates re-run after the rebase
UI: lint 0 errors, 549 tests / 44 files, build clean,
format:checkclean. Rust:cargo test --workspace41 suites / exit 0, clippy-D warningsclean,cargo fmt --all --checkclean.Correction: the README note about drag-installs was wrong and has been rewritten
The blockquote this PR previously added said locked-file backup is inert on a drag-installed
.appand framed the helper's refusal as a deliberate safety property. #211 downgraded that check from fatal to advisory, so that is no longer true. I have replaced the blockquote rather than deleted it, because the situation is still worth one honest paragraph for users - just not the one I wrote.The replacement says, in user-facing language:
.dmgmakes you the owner of its contents (true even into/Applications; only.pkg/App Store installs land root-owned). So the check is advisory there and the helper records aDEGRADEDline in its own log..pkginstall would restore full strength - an improvement, not a prerequisite.No scare framing, and no claim that it does not matter at all.
None of this affects the FDA banner itself, which is about TCC denials and behaves identically regardless of install layout.
Gates after the rebase and rewrite
UI: lint 0 errors, 549 tests / 44 files, build clean,
format:checkclean. Rust:cargo test --workspace41 suites / exit 0, clippy-D warningsclean,cargo fmt --all --checkclean. Theapp-shell.test.tslistener count is stillmain+1 (8 -> 9); the merge-conflict note above still applies.Rebased, and reconciled with #209's docs refresh
#209 landed a README section, "macOS Full Disk Access (separate from Gatekeeper)", that overlaps what this PR was adding. Rather than carry a near-duplicate section, I dropped my version and edited #209's in place. The net diff is now much smaller and the README has one FDA section instead of two.
What changed in #209's section:
Flipped the roadmap bullet. #209 left a
DRAFT - do not uncomment until the corresponding PR mergesblock containing "Guided Full Disk Access onboarding for macOS ... (#205, unmerged.)" - which is this PR. It is now a live feature bullet and the draft entry is gone, so the README is accurate the moment this merges. The remaining draft bullets (S3, local destination, scrub, restore drill, rclone importer) are untouched, and the comment wrapper stays for them.Gates
UI: lint 0 errors, 559 tests / 45 files, build clean,
format:checkclean. Rust:cargo test --workspace --exclude driven-backend43 suites / exit 0, clippy-D warningsclean,cargo fmt --all --checkclean.The exclusion is not this branch's doing - see the note on #201: #200's new
driven-backendcrate has two keychain tests that block on an invisible macOSSecurityAgentprompt on this machine. They pass in CI, and neither of these PRs touches that crate.Rebased onto the re-cut #201, which now sits on
mainwith #211, #207 and #209Still needs retargeting to
mainonce #201 lands.Gates re-run, now WITHOUT the
--exclude driven-backendworkaround#207 fixed the keyring mock, so the full suite runs again:
SQLX_OFFLINE=true cargo test --workspace- 48 suites, exit 0, no exclusionspgrep SecurityAgentempty throughout - no keychain prompt raised-D warningsclean,cargo fmt --all --checkcleanformat:checkcleanThe
app-shell.test.tslistener count remainsmain+ 1 forFdaBanner; the merge-conflict note earlier in this description still applies.