Skip to content

fix(keyring): cache availability probe with OnceLock to prevent repeated keychain dialogs#2651

Merged
M3gA-Mind merged 5 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/keyring-probe-onclock
May 26, 2026
Merged

fix(keyring): cache availability probe with OnceLock to prevent repeated keychain dialogs#2651
M3gA-Mind merged 5 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/keyring-probe-onclock

Conversation

@M3gA-Mind
Copy link
Copy Markdown
Contributor

@M3gA-Mind M3gA-Mind commented May 25, 2026

Summary

  • AuthProfilesStore::new() is called on every CoreStateProvider poll (~every 2s), which triggered the OS keyring availability probe on each call
  • On macOS the probe touched the Security framework keychain, causing the system permission dialog to appear in a loop — trapping users on the loading screen
  • Probe result is now cached in a process-lifetime static AVAILABILITY_CACHE: OnceLock<bool>; the macOS keychain dialog appears at most once per app launch
  • Extracted probe logic into a private probe_availability() fn; is_available() calls get_or_init — semantics unchanged, cost reduced from O(poll rate) to O(1)

Problem

  • macOS users opened the prod app and saw a repeating keychain access prompt ("openhuman wants to use the login keychain") with no way to proceed
  • Root cause: is_available() was called on every snapshot poll; each call attempted a keychain write/read/delete cycle which triggered the system dialog
  • No guard existed to short-circuit after the first successful (or failed) probe

Solution

  • Added static AVAILABILITY_CACHE: OnceLock<bool> at module scope in src/openhuman/keyring/ops.rs
  • is_available() now delegates to get_or_init(probe_availability) — the probe runs exactly once per process, result is shared for all subsequent callers
  • Added unit tests in keyring/tests.rs covering the cached probe and verifying the underlying backend operations (file backend used in CI to avoid keychain permission requirements)

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) — added keyring/tests.rs with probe caching and backend operation tests
  • Diff coverage ≥ 80% — new probe_availability() and is_available() paths covered by keyring/tests.rs; coverage CI gate will validate
  • N/A: Coverage matrix updated — internal caching change; no new user-facing feature rows in docs/TEST-COVERAGE-MATRIX.md
  • N/A: All affected feature IDs from the matrix listed — no feature matrix rows affected
  • No new external network dependencies introduced — OnceLock is from Rust std; file backend used in tests
  • N/A: Manual smoke checklist updated — keyring caching fix only; no release-cut surface changes
  • N/A: Linked issue closed via Closes #NNN — no linked issue (user-reported prod repro, not tracked in GitHub issues)

Impact

  • Desktop (macOS) only — fixes repeated keychain permission dialog on every CoreStateProvider poll
  • No behavior change for non-macOS platforms or when OPENHUMAN_KEYRING_BACKEND=file is set
  • is_available() return value is identical; only the call frequency to the OS keychain is reduced

Related

  • Closes:
  • Follow-up PR(s)/TODOs:

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/keyring-probe-onclock
  • Commit SHA: dbd77b9b228a568ccd6a75b8fd7f1103ac2f6bfa

Validation Run

  • pnpm --filter openhuman-app format:check — ran via pre-push hook; auto-fixed import line wrap in oauthAuthReadiness.ts, committed
  • pnpm typecheck — passed
  • Focused tests: pnpm test:rust (keyring unit tests)
  • Rust fmt/check (if changed): cargo check --manifest-path Cargo.toml — exit 0
  • N/A: Tauri fmt/check (if changed) — no Tauri shell Rust changes

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: OS keychain probe runs once per process instead of on every snapshot poll
  • User-visible effect: macOS keychain permission dialog appears at most once per app launch (was repeating every ~2s)

Parity Contract

  • Legacy behavior preserved: is_available() returns the same boolean; callers unchanged
  • Guard/fallback/dispatch parity checks: file backend fallback path unchanged; OnceLock result is consistent across all callers in the same process

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: this one
  • Resolution (closed/superseded/updated): N/A

Summary by CodeRabbit

  • Bug Fixes

    • Fixed keyring probe failures on repeated launches and ensured UI readiness always advances to the result state.
  • Performance

    • Reduced repeated keyring probes with process-wide caching.
  • Reliability

    • Desktop build now selects local core mode early on initial startup to improve readiness and startup flow.
  • Tests

    • Added regression and end-to-end tests validating keyring idempotence and readiness behavior.

Review Change Stack

…ted keychain dialogs

AuthProfilesStore::new() is called on every CoreStateProvider poll (every 2s),
which triggered the keyring availability probe repeatedly. On macOS this caused
the system keychain access dialog to appear in a loop, blocking users on the
loading screen.

Probe result is now cached in a process-lifetime OnceLock<bool> so macOS only
shows the permission dialog once per app launch.
@M3gA-Mind M3gA-Mind requested a review from a team May 25, 2026 21:56
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 34d488df-055b-47bd-86ea-8fd371cda4cd

📥 Commits

Reviewing files that changed from the base of the PR and between 88a3ef3 and febc239.

📒 Files selected for processing (2)
  • src/openhuman/keyring/mod.rs
  • src/openhuman/keyring/ops.rs

📝 Walkthrough

Walkthrough

Simplify BootCheckGate's success handling, add a Tauri-specific core-mode fallback in OAuth readiness, move auth-profile loading into a blocking task, and make the keyring availability probe idempotent by caching results and deleting stale probe keys; add regression tests for probe idempotence.

Changes

OAuth Initialization Flow

Layer / File(s) Summary
BootCheckGate success path cleanup
app/src/components/BootCheckGate/BootCheckGate.tsx
Unconditionally set phase to 'result' and store checkResult on successful run, removing a redundant branch.
Tauri core mode readiness fallback
app/src/components/oauth/oauthAuthReadiness.ts, app/src/components/oauth/__tests__/oauthAuthReadiness.test.ts
Expand configPersistence imports and add a Tauri-only early fallback that stores 'local' as core mode and proceeds when core mode is unset; update tests and mocks to assert this behavior and exercise non-Tauri branch.
Async auth-profile snapshot loading
src/openhuman/app_state/ops.rs
Load auth session profile inside tokio::task::spawn_blocking, cloning config into the task and mapping panics into formatted errors.

Keyring Availability Probe Idempotence

Layer / File(s) Summary
OnceLock caching and stale-key cleanup
src/openhuman/keyring/ops.rs
Introduce a process-wide OnceLock<bool> cache for the availability probe and delete leftover probe keys before writing to avoid "key already exists" failures on subsequent runs.
Wire keyring tests module
src/openhuman/keyring/mod.rs
Conditionally compile mod tests; under #[cfg(test)].
Probe idempotence regression test suite
src/openhuman/keyring/tests.rs
Add StrictSetBackend to emulate OS "item already exists" errors, old_probe/new_probe helpers, and tests validating the new probe is idempotent and that is_available() returns true across repeated calls (including a gated OS backend E2E test).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant waitForOAuthAuthReadiness
  participant BootCheckGate
  participant CoreProcess
  Client->>waitForOAuthAuthReadiness: request readiness
  waitForOAuthAuthReadiness->>BootCheckGate: await core mode commit (may be unset)
  alt Tauri & core mode unset
    waitForOAuthAuthReadiness->>waitForOAuthAuthReadiness: storeCoreMode('local') and continue
  else core mode committed
    BootCheckGate-->>waitForOAuthAuthReadiness: commit core mode
  end
  waitForOAuthAuthReadiness->>CoreProcess: start local core / poll core.ping
  CoreProcess-->>waitForOAuthAuthReadiness: core ready / timeout
  waitForOAuthAuthReadiness-->>Client: readiness result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • senamakel

"🐰 I nudged checks to settle, and probes to stay true,
Tauri wakes quick with local mode anew.
Spawned a task so profiles sleep no more,
Stale keys removed — probes pass sure.
Hooray for tests that hop and prove!"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely describes the main change: caching the keyring availability probe to prevent repeated dialogs, which directly addresses the core problem described in the PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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

@coderabbitai coderabbitai Bot added rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. bug labels May 25, 2026
Copy link
Copy Markdown
Contributor

@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: 1

🤖 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 `@src/openhuman/keyring/tests.rs`:
- Around line 514-532: The test is meant to validate repeated calls but only
invokes super::ops::is_available() once; update the test function
is_available_returns_true_on_repeated_calls_os_backend to call
super::ops::is_available() twice (or more) after seeding the OS keyring via
backend::OsBackend and probe_key, asserting true on each call to exercise the
cached/second-call path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 27ec4a5e-9f0c-4d98-a329-16749c6f2d4a

📥 Commits

Reviewing files that changed from the base of the PR and between e05cab9 and dbd77b9.

📒 Files selected for processing (6)
  • app/src/components/BootCheckGate/BootCheckGate.tsx
  • app/src/components/oauth/oauthAuthReadiness.ts
  • src/openhuman/app_state/ops.rs
  • src/openhuman/keyring/mod.rs
  • src/openhuman/keyring/ops.rs
  • src/openhuman/keyring/tests.rs

Comment thread src/openhuman/keyring/tests.rs
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 25, 2026
CodeRabbit flagged that the repeated-calls test only invoked is_available()
once, leaving the OnceLock cache path untested. Add a second assertion.
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 25, 2026
…_mode_unset scenario

storeCoreMode was missing from the vi.mock factory, causing vitest to throw
on the Tauri branch. The core_mode_unset test also needed isTauri=false:
in Tauri mode the function defaults to 'local' and never returns that reason.
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 25, 2026
…diness

Lines 95-97 of oauthAuthReadiness.ts (storeCoreMode + break inside the
isTauri() guard) were uncovered, dropping diff-cover to 50%. Add a test
that starts with no stored core mode in a Tauri environment and asserts
that storeCoreMode('local') is called and the function returns ready.
Copy link
Copy Markdown
Contributor

@sanil-23 sanil-23 left a comment

Choose a reason for hiding this comment

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

Reviewed the full diff against PR-head sources. The keyring OnceLock cache + the -25299 delete-before-set fix are correct and worth merging. Two issues should be resolved first — one blocking, one to justify.

🔴 Blocking — the keyring fix has no CI coverage; the regression tests reimplement the logic instead of exercising it

The delete-first fix lives strictly inside the os-backend-only path, after the if name == "file"|"mock"|"encrypted_file" { return true } early-return in probe_availability() (src/openhuman/keyring/ops.rs). CI never selects the os backend, and the only test that calls the real is_available() (tests.rs::is_available_returns_true_on_repeated_calls_os_backend) early-returns unless OPENHUMAN_TEST_OS_KEYCHAIN=1. So the actual fix line is never executed in CI.

The old_probe/new_probe helpers in tests.rs are hand-copied reimplementations of the probe, not calls to probe_availability(). Deleting the real let _ = delete(...) line leaves new_probe green — the regression test guards a duplicate, not the shipped code. (Separately, the profiles tests call AuthProfilesStore::new(dir, use_keychain: bool) with the flag passed explicitly, so they bypass keyring::is_available() too — the OnceLock path also goes uncovered.)

This makes the checklist line "new probe_availability() and is_available() paths covered… coverage CI gate will validate" inaccurate. Please add a test that drives the real is_available()/probe_availability() through the OS-style path without a live keychain — e.g. inject the existing StrictSetBackend via force_backend_for_test(...) so the genuine delete→set→get→delete sequence (and the OnceLock caching) run in CI.

🟠 Justify — Tauri storeCoreMode('local') default is not a safe assumption and persists through

oauthAuthReadiness.ts writes (and persists) storeCoreMode('local') on the first poll iteration whenever getStoredCoreMode() is null in Tauri, with the comment "the core is always embedded locally." That assumption is false on desktop: BootCheckGate.tsx renders the cloud option under {isDesktop && (...)} (with a Core RPC URL + auth-token form), and confirming it calls storeCoreMode('cloud'). So a desktop user can deliberately run a remote core.

Failure path: a user who runs resetCoreMode() to switch local→cloud, or whose first-launch OAuth deep-link callback fires while the picker is still unset, gets silently re-pinned to 'local' — and because the write persists, they must manually reset core mode to recover. The window is narrow (only bites cloud-intending desktop users during an unset/just-cleared moment), but it's a real, persistent regression rather than a transient default. Please either gate the default so it doesn't write through while a picker selection may still be pending, or correct the "always embedded locally" rationale and confirm cloud-on-desktop isn't a supported first-launch path.

Note (non-blocking)

The diff also bundles the app_state/ops.rs spawn_blocking change (a separate Windows thread-pool fix) and the BootCheckGate cleanup, neither of which is mentioned in the PR description. Consider documenting them in the body or splitting them out so reviewers see the full scope.

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

Labels

bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants