Skip to content

fix(keyring): store all tokens in one keyring item (single Keychain prompt) - #7

Merged
codesoda merged 1 commit into
mainfrom
fix/keyring-single-blob
Sep 8, 2026
Merged

codesoda merged 1 commit into
mainfrom
fix/keyring-single-blob

Conversation

@codesoda

@codesoda codesoda commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Problem

On macOS, running a single command like slack -w mlai-aus messages search ... triggered a dozen-plus Keychain permission prompts.

Cause: the keyring backend stored each workspace as its own Keychain item (token:<team_id>), plus separate default and workspaces items. macOS authorizes Keychain access per item, so any operation that lists workspaces or resolves -w/SLACK_WORKSPACE looped over every workspace and read each item individually — one prompt each. Resolving -w also read the matched token a second time.

Fix

Store all state in a single keyring item (store) as one JSON KeyringData blob ({ tokens, default, workspaces }) — the same shape the file-backed store already uses. The blob is read once per process and cached, so:

  • get_workspace_info / list_workspaces / resolve -w / get_default_or_first1 read
  • The user is prompted at most once, and "Always Allow" silences it thereafter.

That takes slack -w <ws> … from ~21 Keychain reads down to 1–2.

Migration (transparent, safe)

Existing installs are migrated on first run: the old per-workspace items are read one final time (the only remaining multi-prompt, once), consolidated into the blob, the blob is persisted first, and only then are the legacy items deleted — so an interrupted migration never loses tokens. The cold-start migration writes via a lock-free helper to avoid re-locking the non-reentrant cache mutex held during load (would otherwise deadlock).

Notes

  • Public API (KeyringStore/TokenStore) and all CLI behavior/flags are unchanged.
  • The file-backed store (SLACK_TOKEN_STORE_PATH) already worked this way — no change there.
  • Trust boundary is unchanged vs. the file store: one unlock exposes all workspaces (same as before, functionally, since any command already had access to all items).
  • Bumps to 0.2.1.

Verified: fmt, clippy (-D warnings), docs (-D warnings), and full test suite with SLACK_INTEGRATION_TESTS=1 all pass.

🤖 Generated with pi


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

The keyring backend stored each workspace as its own Keychain item
(token:<team_id>) plus separate default/workspaces items. macOS authorizes
Keychain access per item, so listing workspaces or resolving -w read every
item and prompted for permission once PER WORKSPACE — a dozen-plus prompts
for users with many workspaces.

Consolidate all state into a single keyring item ('store') as one JSON
KeyringData blob (tokens map + default + workspace order), matching the
file-store shape. The blob is read once per process and cached, so:

- get_workspace_info / list / resolve -w / get_default_or_first = 1 read
- the user is prompted at most once (Always Allow silences it after)

Existing installs migrate transparently on first run (migrate_legacy):
the old per-workspace items are read one final time, consolidated, the
blob is persisted, and only THEN are legacy items deleted — so an
interrupted migration never loses tokens. The cold-start migration writes
via a lock-free write_entry to avoid re-locking the (non-reentrant) cache
mutex held by load().

Public API and CLI behavior are unchanged. Bumps to 0.2.1.
Comment thread src/auth/storage.rs
Comment on lines +189 to 191
if let Ok(mut guard) = cache().lock() {
*guard = Some(data.clone());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The persist() function silently ignores failures when locking the in-memory cache, leading to a stale cache and causing all subsequent keyring operations to fail.
Severity: MEDIUM

Suggested Fix

The persist function should propagate the error when cache().lock() fails instead of silently ignoring it. The if let Ok(...) pattern should be replaced with error handling, such as using ? or map_err, to ensure the caller is notified that the cache update failed and can handle the inconsistent state.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/auth/storage.rs#L189-L191

Potential issue: In the `persist` function, after successfully writing data to the
system keyring, the code attempts to update an in-memory cache. If locking the cache's
`Mutex` fails, for instance due to being poisoned by a panic in another thread during an
FFI call, the `if let Ok(...)` construct silently ignores the error. The function then
incorrectly returns `Ok(())`, creating a state inconsistency where the persisted data
and the in-memory cache are out of sync. Consequently, all subsequent keyring operations
for the lifetime of the process will fail when they try to access the poisoned cache.

Did we get this right? 👍 / 👎 to inform future reviews.

@codesoda
codesoda merged commit 9908b96 into main Sep 8, 2026
10 checks passed
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