Skip to content

fix(host): answer Settings providers without waiting on a keychain prompt - #370

Merged
iamnbutler merged 2 commits into
mainfrom
providers-keychain
Oct 15, 2026
Merged

iamnbutler merged 2 commits into
mainfrom
providers-keychain

Conversation

@iamnbutler

Copy link
Copy Markdown
Contributor

Fixes #368 (follow-up to #367, part of #5).

Before this change, Settings → Providers read each provider key's status with credential(), and read pi-ai's model availability (about 40 env names) through key(). Neither read had a time limit, so a key gated by a macOS prompt held the whole settings answer until someone answered the prompt.

  • Built on fix(host): answer Settings diagnostics without waiting on a keychain prompt #367's mechanism (apps/host/src/keys.ts): latest() and the shared in-flight keychain() reads now sit on one Settings read path, peek().
    • A read still running after 50 ms answers with the last finished read and is marked waiting. A later request picks up the result.
    • After a read that waited on a prompt, only a fresh request (Refresh) reads that name again, so polling doesn't keep raising prompts.
    • Saving or removing a key still clears the cached read, so Settings shows a newly saved key right away.
    • credential() is gone. latest() covers both diagnostics and providers.
  • One read at a time: Settings now reads keychain names one after another (look()), so it raises at most one prompt at a time instead of up to one for each of the ~40 names. A missing name reads in about 0.5 ms, so reading them in sequence costs little. While a prompt holds the queue, rows whose own read already finished keep their state. Only the name being prompted for, or a name that has never been read, shows waiting.
  • No existence check is possible: Bun.secrets has only get, set and delete, with no way to check whether an item exists without reading it. security find-generic-password without -w would work, but it is macOS-only and would mean starting a process for each name. Reading one name at a time is portable and solves the burst problem. This is recorded as a lane note on function look.
  • Model availability for Settings (settled()): this is pi-ai's catalog with a lookup that never waits on a prompt. While a name's read waits, it uses that name's last finished value, and Settings.waiting reports that the list may be incomplete. Values never leave Ace Helper.
  • Agents are unchanged: workers, the composer's models op, defaultModel, setModel and the CLI still use models()/key(), which wait for each read. That means no change to which models a running channel can use, or to when it learns about a newly saved key.
  • Protocol: { op: "settings", fresh?: boolean } and Settings.waiting? are optional fields, so there is no protocol bump.
  • App (apps/app/src/settings.tsx): Provider rows show "Ace Helper is waiting for keychain access…". The Default model row shows it while availability waits. General and Providers re-poll every 3 s while anything is waiting, and Refresh sends fresh.
  • docs/architecture.md describes the behavior.

Verification: an isolated ace serve on port 4381 with ACE_HOME/ACE_CONFIG_HOME under /tmp, ACE_KEYCHAIN_SERVICE=ace-test-368, --no-env-file, ACE_DIRECTORY=http://127.0.0.1:9, and Tailscale stubbed out. A --preload replaced Bun.secrets with an in-memory fake that logs every read. ANTHROPIC_API_KEY and GEMINI_API_KEY were stored and simulated prompts of 8 s and 6 s. No real Keychain item was touched.

  • bun types and bun run ci pass, and the apps/app vite build passes.
  • Over the gateway, every settings answered in 1–64 ms while prompts were pending. The full non-waiting read of ~40 names took 130–245 ms.
    • First answer: both rows unavailable + waiting, models empty, waiting.
    • After the Anthropic prompt answered: anthropic:keychain, openai:missing, 16 Anthropic models, still waiting for Gemini.
    • After Gemini answered: 38 models (Anthropic and Google), no waiting.
  • The fake's log showed the prompts in sequence: the Gemini read started only after the Anthropic prompt was answered. Each prompted name was read once across all polls.
  • fresh: true read both prompted names again, again one at a time. Meanwhile, polls showed anthropic:keychain + waiting and kept the last model list (82 models).
  • key-store OPENAI_API_KEY: the next settings showed openai:keychain and OpenAI models immediately.
  • diagnostics while the queue was held answered in 18 ms with the team secret marked waiting.
  • Headless Chromium against the built app, Settings → Providers: both rows showed "Keychain access needed." and "Ace Helper is waiting for keychain access…" from 0.1 s to 8.2 s. At 9.2 s they switched to "Saved in Keychain." / "No key added." without a click.

Built in Ace

No — built with Claude Code outside Ace by an overnight issue watcher, because this Mac's Ace Canary is a participant in the running two-person study (#218); dogfooding gap tracked in #5.

…ompt

Settings → Providers read each provider key and pi-ai's model availability
(about 40 env names) through the keychain with no time limit. Settings now
reads keychain names one at a time through #367's shared reads, answers with
the last finished read marked waiting while a read waits on a prompt, and
polls until it lands. Only Refresh reads a prompted name again. Workers keep
`models()`, which waits for each read.

Fixes #368

Why: apps/host/src/keys.ts#function look | Settings reads keychain names one at a time and never awaits a read held by a prompt; agents' models() still reads each key on every request
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.

Settings → Providers waits on keychain prompts when it reads provider keys

1 participant