nuncio-cli hangs forever reading the gRPC token in non-interactive sessions #419

Closed
opened 2026-08-15 00:46:15 +00:00 by KofTwentyTwo · 1 comment
KofTwentyTwo commented 2026-08-15 00:46:15 +00:00 (Migrated from github.com)

Symptom

nunciod boots correctly (keys minted, gRPC listening on 127.0.0.1:9420, port confirmed), but nuncio-cli system status hangs indefinitely and prints nothing. No error, no timeout, no output.

Diagnosis (verified, not guessed)

It is a macOS keychain ACL problem, not a broken keyring:

  • a direct security find-generic-password returns immediately, and the nuncio keychain entries exist;
  • nunciod created the grpc-bearer-token item, so the item's ACL trusts nunciod;
  • nuncio-cli is a different binary, so reading that item raises a "nuncio-cli wants to access key … in your keychain" consent prompt.

Interactively a human clicks Allow. In any non-interactive context — CI, ssh, a headless box, a background shell — nothing can answer the prompt, so the blocking keyring read never returns and the CLI stalls with no output and no error.

Impact

The reference client is unusable headlessly, which also means the CLI-driven offline E2E story cannot run anywhere without a logged-in desktop session.

Fix

  1. An explicit, opt-in NUNCIO_GRPC_TOKEN override: when set and non-empty the CLI uses it and never touches the keyring. The keyring stays the default.
  2. The keyring path must never hang silently: bound the read and surface an honest, actionable error instead of blocking forever.
  3. Document how to drive the CLI headlessly, and why the prompt appears interactively.

The token is a secret: it must never be logged, echoed, or included in an error message, and never written to disk or SQLite.

Note

nunciod itself is not affected: it creates the item rather than reading one another binary owns, so its own startup raises no consent prompt.

## Symptom `nunciod` boots correctly (keys minted, gRPC listening on `127.0.0.1:9420`, port confirmed), but `nuncio-cli system status` **hangs indefinitely and prints nothing**. No error, no timeout, no output. ## Diagnosis (verified, not guessed) It is a macOS keychain **ACL** problem, not a broken keyring: - a direct `security find-generic-password` returns immediately, and the `nuncio` keychain entries exist; - `nunciod` **created** the `grpc-bearer-token` item, so the item's ACL trusts `nunciod`; - `nuncio-cli` is a **different binary**, so reading that item raises a *"nuncio-cli wants to access key … in your keychain"* consent prompt. Interactively a human clicks Allow. In **any non-interactive context** — CI, ssh, a headless box, a background shell — nothing can answer the prompt, so the blocking keyring read never returns and the CLI stalls with no output and no error. ## Impact The reference client is unusable headlessly, which also means the CLI-driven offline E2E story cannot run anywhere without a logged-in desktop session. ## Fix 1. An explicit, opt-in `NUNCIO_GRPC_TOKEN` override: when set and non-empty the CLI uses it and never touches the keyring. **The keyring stays the default.** 2. The keyring path must never hang silently: bound the read and surface an honest, actionable error instead of blocking forever. 3. Document how to drive the CLI headlessly, and why the prompt appears interactively. The token is a secret: it must never be logged, echoed, or included in an error message, and never written to disk or SQLite. ## Note `nunciod` itself is not affected: it *creates* the item rather than reading one another binary owns, so its own startup raises no consent prompt.
KofTwentyTwo commented 2026-08-15 00:54:37 +00:00 (Migrated from github.com)

Fixed in #420 (4732d6e), merged to dev.

NUNCIO_GRPC_TOKEN short-circuits the keyring in the CLI only — the daemon never accepts a token from its environment. The keyring remains the default when the variable is unset.

The keyring read is also bounded now, because the failure mode was not an error but a call that never returns: a macOS keychain ACL prompt that no one can answer in a headless context. It runs on a detached std::thread behind a 20s timeout rather than spawn_blocking, since Tokio joins its blocking pool at shutdown and that would merely relocate the hang to process exit.

Reported but not fixed: nunciod's own keyring read is not timeout-bounded. It creates the item rather than reading a foreign one, so it does not hit the ACL prompt, and a keyring failure already surfaces as an honest startup error.

Fixed in #420 (`4732d6e`), merged to `dev`. `NUNCIO_GRPC_TOKEN` short-circuits the keyring in the **CLI only** — the daemon never accepts a token from its environment. The keyring remains the default when the variable is unset. The keyring read is also bounded now, because the failure mode was not an error but a call that never returns: a macOS keychain ACL prompt that no one can answer in a headless context. It runs on a detached `std::thread` behind a 20s timeout rather than `spawn_blocking`, since Tokio joins its blocking pool at shutdown and that would merely relocate the hang to process exit. Reported but not fixed: `nunciod`'s own keyring read is not timeout-bounded. It creates the item rather than reading a foreign one, so it does not hit the ACL prompt, and a keyring failure already surfaces as an honest startup error.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: KofTwentyTwo/nuncio#419