#420 bounded the CLI's keyring read. nunciod's own reads were left unbounded and explicitly reported as unfixed.
The daemon mostly mints its key material, so it does not normally hit the cross-binary ACL prompt — but "does not normally" is not "cannot". A keychain locked by screen-lock, a migrated or restored keychain whose ACL no longer trusts the binary, a re-signed or rebuilt nunciod, or a Linux Secret Service with no unlocked collection all block the read. When that happened, the daemon hung at startup with no error and no log line: the worst possible failure shape, and exactly what the CLI was just fixed for.
The change
Two daemon reads are now bounded:
gRPC bearer token (main.rs, at startup) — a blocked read now ends in the same clean, logged startup failure the daemon already used for other keyring errors. Only the hang case newly reaches it; the failure shape is unchanged.
Webhook signing key (outbox.rs, provisioned lazily on first CALL WEBHOOK) — a blocked read now fails that one dispatch honestly instead of parking the outbox worker.
Both errors name which key material could not be read and suggest the likely cause (locked keychain, or an ACL that no longer trusts this binary). Never a silent stall, never a fabricated default.
The bounded read itself moved into nuncio_store::vault::get_or_create_key_bytes_bounded, so the CLI and daemon share one implementation rather than two copies. The CLI's behaviour and its user-facing messages are unchanged — it keeps its own NUNCIO_GRPC_TOKEN phrasing for the timeout case.
It still runs on a detached std::thread behind a tokio::time::timeout, not spawn_blocking: a platform keyring read cannot be cancelled, so bounding it means abandoning the thread — and Tokio joins its blocking pool during runtime shutdown, which would merely relocate the hang to process exit. That reasoning is now written down next to the helper.
Deliberately not done
No environment-variable token override for the daemon.#420 kept that CLI-only on purpose; nunciod mints and owns the authoritative token and must never accept one from its environment.
No refactor of the CLI's working path beyond what sharing the helper required.
Security
Errors carry only the vault account name (a purpose, e.g. grpc-bearer-token), never key material. The no-secrets canary (crates/nunciod/tests/no_secrets_canary_test.rs) still passes; the new tests assert independently that a sentinel key never appears in any error string.
Tests
crates/nuncio-store/src/vault.rs — a vault whose reads never return produces a TimedOut error inside the bound, naming the key material and the cause; the normal mint/reload path returns exactly what the unbounded read does; a genuine vault failure is reported as a vault failure, not a timeout.
crates/nunciod/tests/daemon_keyring_bound_test.rs — the same three properties for both daemon reads, plus a check that the working path still mints and reloads a stable token.
Gate
cargo fmt --all -- --check, cargo check-all, cargo test-all all green; RUST_TEST_THREADS=2 cargo test-all run three times with zero FAILED lines.
Closes #421.
## The problem
#420 bounded the **CLI's** keyring read. `nunciod`'s own reads were left unbounded and explicitly reported as unfixed.
The daemon mostly *mints* its key material, so it does not normally hit the cross-binary ACL prompt — but "does not normally" is not "cannot". A keychain locked by screen-lock, a migrated or restored keychain whose ACL no longer trusts the binary, a re-signed or rebuilt `nunciod`, or a Linux Secret Service with no unlocked collection all block the read. When that happened, the daemon hung at startup with no error and no log line: the worst possible failure shape, and exactly what the CLI was just fixed for.
## The change
Two daemon reads are now bounded:
- **gRPC bearer token** (`main.rs`, at startup) — a blocked read now ends in the same clean, logged startup failure the daemon already used for other keyring errors. Only the hang case newly reaches it; the failure shape is unchanged.
- **Webhook signing key** (`outbox.rs`, provisioned lazily on first `CALL WEBHOOK`) — a blocked read now fails that one dispatch honestly instead of parking the outbox worker.
Both errors name which key material could not be read and suggest the likely cause (locked keychain, or an ACL that no longer trusts this binary). Never a silent stall, never a fabricated default.
The bounded read itself moved into `nuncio_store::vault::get_or_create_key_bytes_bounded`, so the CLI and daemon share one implementation rather than two copies. The CLI's behaviour and its user-facing messages are unchanged — it keeps its own `NUNCIO_GRPC_TOKEN` phrasing for the timeout case.
It still runs on a detached `std::thread` behind a `tokio::time::timeout`, not `spawn_blocking`: a platform keyring read cannot be cancelled, so bounding it means abandoning the thread — and Tokio joins its blocking pool during runtime shutdown, which would merely relocate the hang to process exit. That reasoning is now written down next to the helper.
## Deliberately not done
- **No environment-variable token override for the daemon.** #420 kept that CLI-only on purpose; `nunciod` mints and owns the authoritative token and must never accept one from its environment.
- No refactor of the CLI's working path beyond what sharing the helper required.
## Security
Errors carry only the vault *account name* (a purpose, e.g. `grpc-bearer-token`), never key material. The no-secrets canary (`crates/nunciod/tests/no_secrets_canary_test.rs`) still passes; the new tests assert independently that a sentinel key never appears in any error string.
## Tests
- `crates/nuncio-store/src/vault.rs` — a vault whose reads never return produces a `TimedOut` error inside the bound, naming the key material and the cause; the normal mint/reload path returns exactly what the unbounded read does; a genuine vault failure is reported as a vault failure, not a timeout.
- `crates/nunciod/tests/daemon_keyring_bound_test.rs` — the same three properties for both daemon reads, plus a check that the working path still mints and reloads a stable token.
## Gate
`cargo fmt --all -- --check`, `cargo check-all`, `cargo test-all` all green; `RUST_TEST_THREADS=2 cargo test-all` run three times with zero `FAILED` lines.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #421.
The problem
#420 bounded the CLI's keyring read.
nunciod's own reads were left unbounded and explicitly reported as unfixed.The daemon mostly mints its key material, so it does not normally hit the cross-binary ACL prompt — but "does not normally" is not "cannot". A keychain locked by screen-lock, a migrated or restored keychain whose ACL no longer trusts the binary, a re-signed or rebuilt
nunciod, or a Linux Secret Service with no unlocked collection all block the read. When that happened, the daemon hung at startup with no error and no log line: the worst possible failure shape, and exactly what the CLI was just fixed for.The change
Two daemon reads are now bounded:
main.rs, at startup) — a blocked read now ends in the same clean, logged startup failure the daemon already used for other keyring errors. Only the hang case newly reaches it; the failure shape is unchanged.outbox.rs, provisioned lazily on firstCALL WEBHOOK) — a blocked read now fails that one dispatch honestly instead of parking the outbox worker.Both errors name which key material could not be read and suggest the likely cause (locked keychain, or an ACL that no longer trusts this binary). Never a silent stall, never a fabricated default.
The bounded read itself moved into
nuncio_store::vault::get_or_create_key_bytes_bounded, so the CLI and daemon share one implementation rather than two copies. The CLI's behaviour and its user-facing messages are unchanged — it keeps its ownNUNCIO_GRPC_TOKENphrasing for the timeout case.It still runs on a detached
std::threadbehind atokio::time::timeout, notspawn_blocking: a platform keyring read cannot be cancelled, so bounding it means abandoning the thread — and Tokio joins its blocking pool during runtime shutdown, which would merely relocate the hang to process exit. That reasoning is now written down next to the helper.Deliberately not done
nunciodmints and owns the authoritative token and must never accept one from its environment.Security
Errors carry only the vault account name (a purpose, e.g.
grpc-bearer-token), never key material. The no-secrets canary (crates/nunciod/tests/no_secrets_canary_test.rs) still passes; the new tests assert independently that a sentinel key never appears in any error string.Tests
crates/nuncio-store/src/vault.rs— a vault whose reads never return produces aTimedOuterror inside the bound, naming the key material and the cause; the normal mint/reload path returns exactly what the unbounded read does; a genuine vault failure is reported as a vault failure, not a timeout.crates/nunciod/tests/daemon_keyring_bound_test.rs— the same three properties for both daemon reads, plus a check that the working path still mints and reloads a stable token.Gate
cargo fmt --all -- --check,cargo check-all,cargo test-allall green;RUST_TEST_THREADS=2 cargo test-allrun three times with zeroFAILEDlines.