@clankie/credential-broker
The credential broker is the local secret and short-lived capability boundary. Provider credentials stay in the macOS Keychain by default; trusted adapters receive signed grants that name only their principal, profile hash, allowed capabilities, optional resources, and expiry.
The operator-facing credential guide owns token names, bot-versus-user setup, rotation, and the distinction between Discord account credentials and local Clankie bearers.
Credential storage
KeychainCredentialStorestores one generic-password item per provider and a serialized provider index. Operations sharing a service are queued across in-process store instances. Creation writes the index before the secret, and deletion removes the secret before pruning the index, so a partial failure cannot leave an unindexed credential behind.FileCredentialStoreis the non-macOS/CI fallback. It writes atomically with collision-proof temporary names, serializes same-path writers, enforces mode0600inside a mode-0700directory, and never returns secrets fromlist().createDefaultCredentialStore()selects Keychain on macOS. SettingCLANKIE_CREDENTIALS_FILEexplicitly selects the file backend.- Credential summaries pass through
redactCredential()before entering a UI or structured log.
The broker is canonical, but provider consumers retain a compatibility fallback:
when no broker entry exists they may read the provider's declared API-key
environment variable. The clankie service also fills absent keys from the
gitignored root .env.local; an existing shell value wins. Discord account
tokens, bridge identities, the activity producer, and play voice do not use
that fallback and reject their forbidden environment names.
CLANKIE_OPERATOR_TOKEN and CLANKIE_CAPTAIN_TOKEN remain explicit test/CI
overrides.
Local operator credential
The broker owns the clankie_operator bearer used by trusted local operator
surfaces. The first service start and the fullscreen launcher mint 256
bits of cryptographic entropy when the entry is absent. The clankie service and
TUI resolve that same entry automatically; CLANKIE_OPERATOR_TOKEN is an
explicit CI/test override and is never persisted by bootstrap.
clankie health reports only presence, selected source, and whether an env
override matches the stored value. clankie operator-credential rotate
atomically replaces the broker entry without printing it. The service
resolves the entry at each authenticated operator request, so rotation rejects
the old token immediately and the next CLI request loads the replacement.
Rotation refuses to run while an env override is active because a stored
replacement cannot invalidate an independently managed environment secret.
The Keychain implementation invokes /usr/bin/security through execFile, not
a shell. Secret JSON is passed as a single argv value because the CLI has no
non-interactive stdin form; it is never placed in a child process environment or
written to a plaintext config file.
Discord providers
discord_bot stores the official bot token as an API credential. The trusted
presence adapter issues discord.presence.act or discord.presence.read grants
bound to a principal, profile hash, expiry, and explicit
discord:guild:* / discord:channel:* resources. The provider refuses resources
outside its configured allowlists and resolves connection material only after a
matching grant is verified. The token is never placed in a captain, service,
or bridge process environment.
clankie_discord_bridge is a separate, broker-owned local bearer. The clankie
service creates it on first start and authenticates it only as the
discord-bridge captain on the discord_text lane. The bridge resolves it
directly from the broker after the service starts. It is never supplied
through CLANKIE_CAPTAIN_TOKEN, so Discord cannot inherit another captain's
identity or source lane.
clankie_discord_voice_bridge is a second broker-owned local bearer with a
distinct discord_voice service identity. Official-bot voice ingress
cannot reuse the text bridge bearer, and the text bridge cannot submit a
voice_event. The bridge resolves both internal credentials directly from the
store after the service starts; neither enters an environment variable.
OpenAI transcription and speech reuse the brokered openai API credential;
Grok streaming transcription and speech reuse the brokered xai API
credential. The selected key stays inside the voice process and is never
exposed to the captain. OAuth entries do not satisfy the voice boundary. When the
external voice is configured (ADR 0070),
ElevenLabs speech synthesis uses the brokered elevenlabs API credential the
same way — connection headers only, and the ELEVENLABS_API_KEY / XI_API_KEY
environment names are hard startup errors in the bridge.
OPENAI_API_KEY and XAI_API_KEY are likewise hard errors while voice is active.
discord_user_session holds the personal-lab normal-user credential
(ADR 0048).
DiscordUserSessionCredentialProvider mirrors the bot provider — expiring,
resource-scoped grants exchanged only by the trusted transport adapter — and
adds one gate the bot plane does not need: a durable owner opt-in bound to the
recorded character and scope, re-checked at redemption so a revocation stops the very
next action instead of waiting for grant expiry. A DISCORD_USER_TOKEN
environment variable is a startup error in every process that could reach it.
clankie_discord_user_bridge and clankie_discord_user_voice_bridge are the
user plane's local bearers, distinct from the bot plane's pair. The service
derives transportKind from which bearer authenticated, so a request body
cannot claim a transport it does not hold. All four bearer patterns are mutually
exclusive: clankie_discord_ prefixes every one of them, and an unanchored
match would let a user-plane bearer authenticate as the bot bridge.
Play voice bearer
clankie_play_voice authenticates only Clankie's local or hosted play loop to
the active Discord body's loopback @clankie/play-voice listener. The Discord
body owns first-run minting; play resolves the stored value. The bearer has no
environment fallback, and CLANKIE_PLAY_VOICE_TOKEN is a hard error.
GBA MCP and external harnesses neither receive nor depend on this credential.
The retired clankie_possessor_voice id is not accepted as the current bearer;
there is no path from owning an emulator process to hearing or speaking in
Clankie's room (ADR 0129).
Capability boundary
CapabilityTokenIssuer signs and verifies bounded HMAC grants. Resource-scoped
grants require the caller to present an exact resource; omitting a resource does
not widen a grant. Tokens are rejected before issuedAt and at or after
expiresAt, use canonical base64url encoding, and have a maximum 15-minute
lifetime. Signed obligation identifiers travel with the grant so a consumer
receives the constraints attached to the original allow decision. A
profile-hash mismatch invalidates the grant immediately.
CapabilityTokenIssuer is a signing/verification primitive, not a runtime
broker. Callers own issuance policy and any evidence they require; this package
validates grant shape, lifetime, signature, and exact resource matching.
