Skip to content

feat(consent): local source discovery + per-source opt-in (task 002)#2

Merged
ogrodev merged 3 commits into
mainfrom
task/002-source-discovery-consent
Jun 1, 2026
Merged

feat(consent): local source discovery + per-source opt-in (task 002)#2
ogrodev merged 3 commits into
mainfrom
task/002-source-discovery-consent

Conversation

@ogrodev
Copy link
Copy Markdown
Owner

@ogrodev ogrodev commented Jun 1, 2026

Implements task 002 — local source discovery + consent screen. Closes the consent half of PRD §4 / [§9].

The app shows what it could connect to and lets you opt in, source by source, before any secret is read.

ADR: 0012 — metadata-only discovery → per-source opt-in.

What changed

  • core — new sources module: source descriptors + catalog, the pure consent gate (active = present && enabled), and discover_sources / active_sources over two new ports. New SourceProbe (metadata-only presence) and ConsentStore ports in core::ports. Stays IO-free (purity gate green).
  • adaptersClaudeCredentials::is_present stats the creds file / queries the Keychain item without -w, so discovery never reads the token; LocalSourceProbe dispatches by id; FileConsentStore persists opt-in as JSON in the app config dir (consent is not a secret, so it never touches the keychain) and fails closed on a corrupt file.
  • app — the refresh loop and fetch_claude_usage are now consent-gated; new list_sources / set_source_enabled commands. Opting a source in kicks an immediate refresh, so it takes effect without a restart. Claude Code now connects through this flow instead of fetching unconditionally.
  • ui — a Sources/consent screen (presence badge, plain-language access note shown before opt-in, per-source switch) and a connect-gated usage view.

Acceptance criteria

Criterion How it's met
Discovers + lists local sources discover_sources over the catalog
Metadata only — no secret read until opt-in The only secret-reading path (claude_usage → creds.load) is gated by active_sources; the probe only stats. Test: a present-but-garbage creds file reads as detected while parse_creds rejects it
Plain-language note shown before opt-in Catalog note (test asserts it's an honest disclosure, not a placeholder); UI always renders it on the Sources screen
Enable/disable each source individually set_source_enabled per id; per-source independence + persistence tested
Takes effect without restart; Claude connects via this flow Opt-in spawns refresh_active; the loop re-reads consent each tick

Tests / fixtures added

  • core sources: gate logic, discover/active pairing, consent-first short-circuit, honest-note assertion.
  • adapters: presence-is-metadata-only invariant, consent persists across a reload, per-source independence, unknown-source absent.
  • No new HTTP fixtures (no new network surface; discovery is local).

Verification

  • make check green: fmt, clippy (-D warnings), core-purity, 26 tests, cargo-deny (advisories/bans/licenses/sources). Plus cargo machete, core line coverage 88% (sources.rs 97%, floor 80%), Biome, svelte-check 0/0, pnpm build.
  • No secrets in code, logs, or DB — consent lives in plain JSON; tokens stay in the keychain only. gitleaks clean.
  • Remaining (human DoD step): manual QA via make qa — confirm with a network/Keychain monitor that nothing is read before opt-in. Default on a fresh install is Claude off until opted in (per ADR 0012).

Summary by cubic

Added local source discovery (metadata-only) and per-source opt-in so secrets are only read when a source is present and enabled. Claude Code now connects through this consent gate, meeting PRD §4/§9.

  • New Features

    • Core: sources module with catalog, consent gate (present && enabled), and discover_sources / active_sources; new SourceProbe and ConsentStore ports.
    • Adapters: LocalSourceProbe; ClaudeCredentials::is_present checks file/Keychain presence without reading the token; FileConsentStore persists JSON consent using an in-process parking_lot Mutex.
    • App: consent-gated fetch_claude_usage; list_sources and set_source_enabled; opt-in triggers an immediate refresh without a restart.
    • UI: Sources screen (presence badge, plain-language note before opt-in, per-source switch); switches are enabled when present || enabled so a now-absent source can be revoked; usage view is connect-gated.
  • Bug Fixes

    • Consent: persist toggles before committing to memory and roll back on write failure; fails closed so runtime consent never diverges from disk.
    • UI: After opting in, don’t show “Loading” if a usage snapshot already arrived; re-discover presence when the popover regains focus to keep detection fresh.

Written for commit 3b8dee3. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Added source discovery and per-source consent management to the connect screen
    • Users can now enable/disable individual sources individually with changes taking effect immediately
    • Each source displays an access disclosure note before opt-in
    • Discovery reads only metadata without accessing sensitive credentials
  • Documentation

    • Updated product requirements and task tracking to reflect completed source discovery and consent features

Sources are now discovered (metadata only) and gated behind explicit
consent (ADR 0012). Nothing reads a credential until the user opts a
source in; the already-shipped Claude Code source connects through this
flow instead of fetching unconditionally.

What changed
- core: `sources` module (descriptors + catalog + the present&&enabled
  gate), plus `SourceProbe` (metadata-only presence) and `ConsentStore`
  ports. `discover_sources`/`active_sources` are pure over the ports.
- adapters: `ClaudeCredentials::is_present` (stat the creds file /
  Keychain item WITHOUT `-w` — never reads the token), `LocalSourceProbe`
  dispatch, and `FileConsentStore` (JSON in app config dir; consent is
  not a secret, so it never touches the keychain; persists across restart).
- app: refresh loop + `fetch_claude_usage` are consent-gated; new
  `list_sources` / `set_source_enabled` commands. Opting in kicks an
  immediate refresh — effective without a restart.
- ui: a Sources/consent screen (presence badge, plain-language access
  note shown before opt-in, per-source switch) and a connect-gated usage
  view.

Risk & fallback
- A corrupt consent file fails closed (every source opted-out) — it can
  never accidentally enable a read. Discovery is decoupled from reading:
  a present-but-garbage creds file reads as "detected" yet the secret
  path rejects it (covered by a test).

Verification
- `make check` green (fmt, clippy, core-purity, 26 tests, cargo-deny),
  `cargo machete`, core coverage 88% (>=80), Biome, svelte-check (0/0),
  `pnpm build`. Manual QA (tray + network/keychain confirmation that
  nothing is read pre-opt-in) is the remaining human step per the DoD.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a complete source discovery and consent system, allowing users to discover login-reuse sources (currently Claude Code) by checking metadata-only presence, opt in per-source, and gate API calls on both consent and presence. The implementation spans core contracts, adapter implementations, Tauri backend refactoring, and frontend UI changes.

Changes

Source discovery and consent system

Layer / File(s) Summary
Core port contracts and discovery model
crates/core/src/ports.rs, crates/core/src/sources.rs, crates/core/src/lib.rs
Adds SourceProbe and ConsentStore traits for presence detection and opt-in persistence. Defines SourceDescriptor, SourceState data models and functions (source_catalog, discover_sources, active_sources) to orchestrate metadata-only discovery without reading secrets until consent is confirmed.
Claude metadata presence detection
crates/adapters/Cargo.toml, crates/adapters/src/claude.rs
Implements ClaudeCredentials::is_present() with stat-based presence checks for credentials file and Keychain item existence (no reading/parsing). Adds parking_lot dependency for mutex use.
File-backed consent store
crates/adapters/src/consent.rs
Introduces FileConsentStore with JSON persistence and mutex-protected in-memory state. Loads with best-effort defaults (missing/invalid files default to disabled) and writes updates with parent directory creation.
Local source probe and adapter wiring
crates/adapters/src/sources.rs, crates/adapters/src/lib.rs
Implements LocalSourceProbe dispatcher for Claude presence checks and unknown ID fallback. Declares public adapter modules and re-exports types.
Tauri backend consent-gated refresh and commands
src-tauri/src/lib.rs
Refactors from unconditional Claude fetching to consent- and presence-gated active-source refresh. Adds AppSources state management, updates commands (fetch_claude_usage gated, list_sources, set_source_enabled), implements refresh_active pipeline, and wires app startup to load/persist consent.
Frontend TypeScript API for source discovery
src/lib/usage.ts
Adds SourceState interface and listSources() and setSourceEnabled() Tauri invocation wrappers.
Svelte source discovery and toggle UI
src/routes/+page.svelte
Adds source discovery on mount, introduces view toggle between usage and sources, implements toggleSource handler, updates connection indicator to gate on active sources, and renders source-list UI with per-source enable checkboxes and access notes.
PRD and task documentation updates
docs/PRD.md, docs/tasks/002-source-discovery-consent.md
Updates delivery checklist to mark source discovery + consent UI ✅ and documents metadata-only discovery, access notes, and stateless toggles as complete.

Sequence Diagram

sequenceDiagram
    participant User
    participant Frontend as Svelte UI
    participant Tauri as Tauri Backend
    participant Probe as SourceProbe
    participant Consent as ConsentStore
    participant Fetch as API Fetch
    User->>Frontend: Mount page / list sources
    Frontend->>Tauri: listSources()
    Tauri->>Probe: is_present("claude-code")
    Tauri->>Consent: is_enabled("claude-code")
    Tauri->>Frontend: SourceState[]{present, enabled}
    Frontend->>User: Show sources list with checkboxes
    User->>Frontend: Enable source
    Frontend->>Tauri: setSourceEnabled("claude-code", true)
    Tauri->>Consent: set_enabled("claude-code", true)
    Tauri->>Tauri: spawn_refresh_loop
    Tauri->>Probe: is_present (enabled only)
    Tauri->>Fetch: fetch usage
    Tauri->>Frontend: emit usage-updated
    Frontend->>User: Show usage snapshot
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hop along the presence trail,
No secrets read—just metadata pale.
Consent gates open, sources discovered with care,
Each user decides what credentials to share!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing local source discovery and per-source opt-in functionality for task 002, which is the primary purpose of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/002-source-discovery-consent

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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@crates/adapters/src/claude.rs`:
- Around line 27-29: The keychain probe (keychain_present) runs a blocking
std::process::Command::output() directly from the async path (is_present called
by LocalSourceProbe::is_present), which can stall the tokio runtime and has no
timeout; change the probe to execute off the async executor and enforce a hard
timeout: either (preferred) rewrite keychain_present to use
tokio::process::Command (spawn the child, await output) and wrap the await in
tokio::time::timeout with a short bound, killing the tokio::process::Child via
child.kill() on timeout, or (if keeping std::process) call it inside
tokio::task::spawn_blocking and wrap that future in tokio::time::timeout but
ensure you spawn the child so you can kill it if the timeout fires; update the
call site in is_present (claude.rs) to await the new async-safe probe (or await
the spawn_blocking) and preserve the file_present check.

In `@src/routes/`+page.svelte:
- Around line 88-101: toggleSource can set loading=true after setSourceEnabled
even if an asynchronous usage update already populated snapshot via
onUsageUpdated, causing the UI to incorrectly show the loading state; update
toggleSource to only set loading=true when enabling a source if snapshot is
currently null/undefined (i.e., check snapshot before assigning loading=true)
and/or ensure you compare the current source.id/state to avoid overriding a
fresh snapshot (refer to toggleSource and onUsageUpdated to coordinate).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8c2c5142-211b-4af4-99ca-77f6010a5173

📥 Commits

Reviewing files that changed from the base of the PR and between b49f6b7 and e8700dc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • crates/adapters/Cargo.toml
  • crates/adapters/src/claude.rs
  • crates/adapters/src/consent.rs
  • crates/adapters/src/lib.rs
  • crates/adapters/src/sources.rs
  • crates/core/src/lib.rs
  • crates/core/src/ports.rs
  • crates/core/src/sources.rs
  • docs/PRD.md
  • docs/tasks/002-source-discovery-consent.md
  • src-tauri/src/lib.rs
  • src/lib/usage.ts
  • src/routes/+page.svelte

Comment on lines +27 to +29
pub fn is_present() -> bool {
file_present() || keychain_present()
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Jun 1, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 '\b(spawn_blocking|timeout)\b' --type=rust
rg -nP -C3 'is_present\s*\(' --type=rust

Repository: ogrodev/MLT

Length of output: 3990


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect claude adapter presence helpers around keychain_present/file_present
rg -n "fn (file_present|keychain_present|is_present)\b" crates/adapters/src/claude.rs
rg -n "keychain_present|security|find-generic-password" crates/adapters/src/claude.rs
wc -l crates/adapters/src/claude.rs

# Print relevant parts without dumping entire file
# (Try to stay within ~200 lines around the match)
for pat in "fn keychain_present" "fn file_present" "fn is_present" "security" "find-generic-password"; do
  echo "----- context for $pat -----"
  rg -n -C5 "$pat" crates/adapters/src/claude.rs || true
done

# 2) Confirm callers don't add timeout/spawn_blocking wrappers
# Search for probe.is_present callsites + any timeout/spawn_blocking around them in the async call chain.
rg -n "probe\.is_present\(&" -S crates src-tauri || true
rg -n "sources\.probe\.is_present\(&" -S src-tauri crates || true

# 3) Specifically scan sources.rs to see how LocalSourceProbe is used (no wrapper inside it?)
rg -n "impl SourceProbe for LocalSourceProbe|LocalSourceProbe::is_present|timeout|spawn_blocking" crates/adapters/src/sources.rs
sed -n '1,220p' crates/adapters/src/sources.rs

Repository: ogrodev/MLT

Length of output: 6499


Presence probe can block tokio runtime and lacks per-probe timeout

crates/adapters/src/claude.rs (keychain_present, ~147-153) runs /usr/bin/security find-generic-password ... via synchronous std::process::Command::output() with no timeout; it’s called directly from the async LocalSourceProbe::is_present (crates/adapters/src/sources.rs) without spawn_blocking/bounded wait. If security stalls, it can block runtime worker threads. Errors are mapped to false (unwrap_or(false)), but hangs aren’t bounded—ADR 0015 requires a per-probe timeout/failure gate for upstream probes. Move the blocking subprocess off the async executor and enforce a hard timeout (and kill the child on timeout).

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

In `@crates/adapters/src/claude.rs` around lines 27 - 29, The keychain probe
(keychain_present) runs a blocking std::process::Command::output() directly from
the async path (is_present called by LocalSourceProbe::is_present), which can
stall the tokio runtime and has no timeout; change the probe to execute off the
async executor and enforce a hard timeout: either (preferred) rewrite
keychain_present to use tokio::process::Command (spawn the child, await output)
and wrap the await in tokio::time::timeout with a short bound, killing the
tokio::process::Child via child.kill() on timeout, or (if keeping std::process)
call it inside tokio::task::spawn_blocking and wrap that future in
tokio::time::timeout but ensure you spawn the child so you can kill it if the
timeout fires; update the call site in is_present (claude.rs) to await the new
async-safe probe (or await the spawn_blocking) and preserve the file_present
check.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Verified, but pushing back on doing this in this PR.

The finding is real in the narrow sense (a blocking subprocess awaited on the async executor, no timeout), but two things matter:

  1. This specific probe is the low-risk one. keychain_present calls security find-generic-password without -w — it reads the item's attributes, never decrypts the secret, so it does not trigger a Keychain ACL prompt and returns promptly. The variant that can actually block on a user prompt is read_keychain (-w), which is pre-existing and on the consented load path.
  2. The blocking-in-async pattern is codebase-wide and pre-existing, not introduced here: read_keychain (-w), detect_user_agent (claude --version), and ClaudeCredentials::load's std::fs::read_to_string all run synchronously inside async fns. The same refresh loop that calls this probe also calls claude_usage → load → read_keychain, so wrapping only the new probe in spawn_blocking + timeout would create a divergent pattern and false assurance while the larger exposure (the fetch path) stays unbounded.

ADR 0015's per-probe timeout / failure-gate is a mandated-but-not-yet-implemented resilience pattern. It should be applied uniformly across the adapter's blocking calls and the usage fetch as a dedicated resilience change, not spot-welded onto this one probe. Deferring it there rather than diverging here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@ogrodev, that's a well-reasoned pushback — accepted. The distinction between keychain_present (attributes-only, no ACL prompt) and read_keychain (-w, secret decrypt) is the key point I missed, and the argument for uniform ADR 0015 treatment rather than a divergent spot-fix is sound. Applying spawn_blocking + timeout to only this probe while leaving read_keychain, detect_user_agent, and fs::read_to_string in load() unbounded would create inconsistency without meaningfully reducing the actual exposure.

Would you like me to open a follow-up issue to track the ADR 0015 resilience work (uniform spawn_blocking + bounded timeout across all blocking adapter calls)?


✏️ Learnings added
Learnt from: ogrodev
Repo: ogrodev/MLT PR: 2
File: crates/adapters/src/claude.rs:27-29
Timestamp: 2026-06-01T00:56:24.222Z
Learning: In the `ogrodev/MLT` repository (`crates/adapters/src/claude.rs`), the blocking-in-async pattern (synchronous `std::process::Command` and `std::fs` calls inside async fns) is pre-existing and codebase-wide, covering `read_keychain` (with `-w`), `detect_user_agent`, and `ClaudeCredentials::load`. The `keychain_present` helper intentionally omits `-w` so it reads only Keychain item attributes and never triggers an ACL/user prompt. ADR 0015 mandates a per-probe timeout/failure-gate that must be applied uniformly across all blocking adapter calls as a dedicated resilience change, not spot-fixed on individual probes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Thanks — I went ahead and opened #3 to track the uniform ADR 0015 pass (spawn_blocking + bounded timeout across read_keychain, detect_user_agent, load's fs read, and this probe), so no need for the bot to file one.

Comment thread src/routes/+page.svelte
set_source_enabled emits usage-updated from a spawned refresh while its
invoke promise resolves independently (Tauri gives no ordering guarantee
between the two). If the event populates the snapshot before toggleSource
runs its post-await line, flipping loading=true masked the live snapshot
until the next 60s poll. Guard with `&& !snapshot` so opt-in only shows
the loading state when there's nothing to show yet.

Addresses CodeRabbit review on PR #2.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 14 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/adapters/src/sources.rs">

<violation number="1" location="crates/adapters/src/sources.rs:22">
P2: `ClaudeCredentials::is_present()` is synchronous (calls `std::process::Command::output()` for the macOS Keychain check) but is invoked directly from the async `SourceProbe::is_present`. This blocks the tokio worker thread if the `security` subprocess stalls. Wrap the call in `tokio::task::spawn_blocking` and add a bounded timeout (e.g., `tokio::time::timeout`) so a hung child process cannot starve the runtime.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/adapters/src/consent.rs Outdated
Comment thread src/routes/+page.svelte
Comment thread src/routes/+page.svelte Outdated
impl SourceProbe for LocalSourceProbe {
async fn is_present(&self, id: &ProviderId) -> bool {
match id.as_str() {
"claude-code" => ClaudeCredentials::is_present(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: ClaudeCredentials::is_present() is synchronous (calls std::process::Command::output() for the macOS Keychain check) but is invoked directly from the async SourceProbe::is_present. This blocks the tokio worker thread if the security subprocess stalls. Wrap the call in tokio::task::spawn_blocking and add a bounded timeout (e.g., tokio::time::timeout) so a hung child process cannot starve the runtime.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/adapters/src/sources.rs, line 22:

<comment>`ClaudeCredentials::is_present()` is synchronous (calls `std::process::Command::output()` for the macOS Keychain check) but is invoked directly from the async `SourceProbe::is_present`. This blocks the tokio worker thread if the `security` subprocess stalls. Wrap the call in `tokio::task::spawn_blocking` and add a bounded timeout (e.g., `tokio::time::timeout`) so a hung child process cannot starve the runtime.</comment>

<file context>
@@ -0,0 +1,36 @@
+impl SourceProbe for LocalSourceProbe {
+    async fn is_present(&self, id: &ProviderId) -> bool {
+        match id.as_str() {
+            "claude-code" => ClaudeCredentials::is_present(),
+            _ => false,
+        }
</file context>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Verified, but deferring (same as CodeRabbit's note on the same probe). It is a real blocking-in-async call, but:

  1. This probe runs security find-generic-password without -w — an attribute lookup, no decrypt — so it does not trigger a Keychain ACL prompt and returns promptly. The prompt-capable -w read is read_keychain, on the consented load path.
  2. The blocking-in-async pattern is pre-existing and shared by read_keychain (-w), detect_user_agent (claude --version), and load's std::fs::read_to_string — all driven by the same refresh loop. Wrapping only this probe in spawn_blocking + timeout would diverge and give false assurance while the larger exposure stays unbounded.

ADR 0015's per-probe timeout/failure-gate is mandated but not yet implemented; it should land as one uniform resilience pass over the probe and fetch paths, not a spot-fix here. Tracking it there rather than diverging in this consent PR.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Tracked as #3 — uniform spawn_blocking + bounded timeout across the probe and fetch paths (per ADR 0015), rather than a spot-fix here.

…shness

- consent (FileConsentStore): persist the toggle *before* committing it to
  the in-memory map, rolling back on write failure, so runtime consent never
  diverges from disk. Fail closed — a source is only treated as opted-in once
  the choice is durably recorded. Adds a deterministic persist-failure test.
- ui: gate the source switch on `present || enabled` instead of `present`, so
  an opted-in-but-now-absent source can still be turned OFF (revoked); only
  enabling a never-detected source stays disabled. Adds a matching hint.
- ui: re-discover sources when the popover regains focus, so presence reflects
  logins/logouts that happened since the webview loaded (it was fetched once).

Addresses cubic review (P1 + two P2) on PR #2.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant