Skip to content

fix(host): answer Settings diagnostics without waiting on a keychain prompt - #367

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

iamnbutler merged 2 commits into
mainfrom
diagnostics-keychain

Conversation

@iamnbutler

Copy link
Copy Markdown
Contributor

Fixes #360 (found fixing two-person study scenario 18, #356 / #218).

Settings → This Mac read ACE_SECRET from the Keychain on every diagnostics request with no time limit. When macOS gated that read behind a prompt, the whole diagnostics answer waited for it. Settings then showed stale or empty state, such as "Waiting for the first sync…" even after the host had recorded a sync error.

  • Diagnostics never waits on the Keychain (apps/host/src/keys.ts latest, apps/host/src/diagnostics.ts). A read that doesn't finish within 50 ms answers with the last finished read, marked waiting: "Ace Helper is waiting for keychain access". If there was no earlier read, it answers source: "unavailable", stored: null. The read keeps running and the next poll picks up the result. If a read had to wait on a prompt, later polls reuse its result, and only Refresh status reads again, so polling doesn't keep raising prompts. Saving or removing the secret clears the cached result.
  • One read at a time (keychain): concurrent reads of the same name share one in-flight Bun.secrets.get. Settings, the directory heartbeat sync, and a move's credential read (fix(host): read move credentials before fencing the channel, and report keychain waits #353) therefore raise at most one prompt between them. The move path keeps fix(host): read move credentials before fencing the channel, and report keychain waits #353's bounded credentials() helper and now takes its waiting text from the shared KEYCHAIN_WAITING.
  • Refresh: diagnostics sync: true no longer waits up to 25 s on a directory sync while the secret read is waiting. The sync result shows up on a later poll.
  • Settings: the Team secret row shows "Ace Helper is waiting for keychain access…" under the last known state. This Mac re-polls every 3 s while a read is pending, the same way it does for the first directory sync. CredentialStatus gains an optional waiting, so no protocol bump: an older app ignores it.
  • docs/architecture.md describes the behavior.

Not changed: the Providers section's settings op still reads provider keys, and pi-ai's model availability, without a time limit. In the test below it read about 40 provider env names from the Keychain while a read was delayed. That is the same class of problem for Settings → Providers and should be a follow-up.

Verification (isolated ace serve on port 4379 with ACE_HOME, ACE_CONFIG_HOME, and ACE_KEYCHAIN_SERVICE=ace-test-360 under /tmp, Tailscale shimmed out, and ACE_DIRECTORY=http://127.0.0.1:9). A --preload replaced Bun.secrets with an in-memory fake whose get waits 8–9 s. No real Keychain item, launchd job, or Canary Helper was touched.

  • bun types and bun run ci pass. The apps/app vite build passes.
  • Over the gateway, while the read was pending: every diagnostics answered in 51–73 ms. The first answer was unavailable + waiting when nothing had been read yet, and keychain + waiting after an earlier read. sync: true also answered in 73 ms. Once the read finished, the next poll returned keychain without waiting, and the directory row had the sync error. The fake logged a single ACE_SECRET read across the startup sync and all polls.
  • Headless Chromium against the built app, Settings → This host: the Team secret row showed "Keychain access needed." with "Ace Helper is waiting for keychain access…" from 0.4 s to 9.5 s. At 10.5 s it switched to "Saved in Keychain." with Remove…, without a click. Only one ACE_SECRET read happened.

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.

…prompt

Diagnostics now reports ACE_SECRET from the host's last finished read, marked
"Ace Helper is waiting for keychain access" while a read is under way, and
Settings polls until that read lands. Keychain reads of the same name share one
in-flight read, so Settings, directory syncs, and moves raise at most one
prompt. Refresh skips waiting for a directory sync that waits on the keychain.

Fixes #360
Why: apps/host/src/keys.ts#function latest | Settings must never await a keychain read; after a prompted read only a fresh (Refresh) request reads again, or each poll raises another prompt
Why: apps/host/src/keys.ts#function keychain | concurrent reads of one name share a single in-flight Bun.secrets.get so a person sees at most one prompt
@iamnbutler
iamnbutler merged commit abc7666 into main Oct 15, 2026
1 check passed
iamnbutler added a commit that referenced this pull request Oct 15, 2026
…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
iamnbutler added a commit that referenced this pull request Oct 15, 2026
…ompt (#370)

* fix(host): answer Settings providers without waiting on a keychain prompt

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

* lane: sync providers-keychain memory
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 waits on a keychain prompt when it reads diagnostics

1 participant