docs/credentials.md

Credentials and identities

Clankie keeps account secrets in the credential broker (macOS Keychain by default). Non-secret application, guild, channel, role, and allowlist settings live in ~/.config/clankie/settings.json. Do not put Discord tokens in that file, .env.local, shell profiles, commands, logs, or issue text. The headless CLI never takes secrets as flags; its contract is docs/cli.md.

Discord bot token versus user token

These credentials are not interchangeable.

CredentialBroker idOwnerUsed byAuthorization form
Official bot tokendiscord_botA Discord application botapps/discord-bridgeDiscord bot gateway/REST
Normal-user tokendiscord_user_sessionA normal Discord accountapps/discord-user-sessionBare user gateway/REST

The official bot token comes from the Discord Developer Portal's Bot page. It is the supported default for text, voice, slash commands, and the embedded Activity. There is one discord_bot slot and one running bot client; Clankie does not implement a bot-token pool.

The user token is the credential of a normal account, not an application bot token. Discord forbids automating normal user accounts. Clankie keeps this personal-lab body off by default and requires explicit enablement, non-empty allowlists, a durable owner acknowledgement, and activeBody=user_session. Only this body can watch another person's share or publish Go Live.

Both account tokens may remain stored, but the launcher starts exactly one Discord body. The processes do not share credentials or gateways.

The active account token remains inside its TypeScript body and authenticates that body's gateway/REST connection. It is never sent to @clankie/vox-client or clankvox. After Discord accepts a voice or stream join, only the short-lived voice/stream endpoint, session, token, user, channel, and server credentials required by that role cross the bounded IPC process boundary. They are held for the role lifetime and are not broker entries or receipt fields (ADR 0128).

The older credential-routing JPG is a historical snapshot. Current credential ownership is:

Configure Discord

Use the TUI's direct /discord flow. /auth is for model/vendor credentials; using its advanced custom-provider entry for Discord reaches the same broker but skips the Discord-specific setup and checks.

Machine access is a separate grant from ingress. Under /discordMachine control from Discord:

  • named users get durable machine access in their official-bot DMs and one-shot access in ordinary shared rooms;
  • named servers grant every admitted member a shared durable machine session, optionally refined to named channels; and
  • empty grant lists keep Discord social.

These tools run unsandboxed as the Clankie service user. A server grant is appropriate only when every admitted member in its selected rooms may control that machine. Removing a grant takes effect for the next message; it does not cancel work already running.

Official bot

  1. Create a Discord application and bot, enable the required intents, and copy the bot token.
  2. Run /discord, store Bot token, and set the application, guild/channel, text, voice, and Activity identifiers you use. guild-id is the command and live-proof server. swarm-guild-id is separate and names the one server Clankie controls, the only one his agents can be given rooms in (ADR 0146); it needs Manage Channels, Manage Webhooks, and Send Messages there. The last permission lets it create a post when a forum is selected. Servers he merely inhabits belong on the ingress, presence, and voice allowlists and nowhere else.
  3. Generate/install the invite from /discord or /discord invite.
  4. Select the Official bot active body and run clankie restart discord.
  5. Verify with /discord status, pnpm discord:readiness — which reports whether he holds Manage Channels, Manage Webhooks, and Send Messages in the swarm home — and, when voice is enabled, pnpm discord:voice-readiness.

Personal-lab user body

  1. Run /discord directly, store User token, and enable the lab body.
  2. Set non-empty guild, text-channel, and voice-channel allowlists.
  3. Record the ToS/account-risk acknowledgement in that flow.
  4. When spoken requests are required, set discord.userSessionVoiceEnabled=true in settings.json; /discord status shows the effective value. Enter explicit voice-channel ids in the lab wizard rather than relying on a blank fallback.
  5. Select the Lab user body and build Vox with pnpm --filter @clankie/vox build.
  6. Run clankie restart and pnpm --filter @clankie/discord-user-session readiness.

Replacing either account token requires restarting the process that logged into that gateway. Revoking the lab opt-in blocks the next privileged action without waiting for a restart.

Local Clankie bearers

Local bearers authenticate Clankie processes to each other. They are not Discord account tokens and must never be pasted into the Discord portal.

Broker idPrincipal
clankie_operatorTrusted local operator APIs
clankie_captainCaptain dispatch and lane APIs
clankie_discord_bridgeOfficial-bot text lane
clankie_discord_voice_bridgeOfficial-bot voice lane
clankie_discord_user_bridgeUser-body text lane
clankie_discord_user_voice_bridgeUser-body voice lane
clankie_activity_producerPrivate Activity frame producer/snapshot listener
clankie_play_voiceClankie's gameplay commentary/hearing seam

The owning service mints these values. Models never receive them. The four Discord lane bearers are intentionally distinct, so a body or text lane cannot claim another transport by changing a request field.

clankie_play_voice is shared only by Clankie's play loop and the active Discord body. It is not issued to GBA MCP or any external harness. The old clankie_possessor_voice provider id is not a current principal.

Discord also issues short-lived voice and stream-server credentials after a gateway session is established. Those runtime values go through the Apache @clankie/vox-client boundary to the active body's one AGPL clankvox child. They are neither operator configuration nor broker entries.

World seat

A seat in a hosted PokeAgent MMO world is a bearer the world's operator mints and hands out, not a value Clankie can issue for himself. It lives in the broker under pokeagent_mmo_world.

Broker idPrincipalIssued by
pokeagent_mmo_worldClankie's player seat in a hosted worldThe world host's operator

CLANKIE_WORLD_CREDENTIAL is refused outright — setting it fails the join even when the broker also holds an entry, so an ambient environment value can never beat the broker (ADR 0103). This is the one credential with no environment fallback of any kind.

The world itself is dialed through WORLD_ADDRESS: a unix socket path, tcp://host:port, or tls://host:port. Unset, Clankie uses the host's unix socket under WORLD_STATE_DIR (default ~/.pokeagent-mmo/world/host.sock). Clankie uses the published @pokeagents/world-protocol package's shared WorldPlayerClient; the installed version is pinned in apps/clankie/package.json.

Each player or harness receives a different world credential and therefore a different player identity/session. Possessing another local process or sharing Clankie's seat is not part of the contract.

No /auth or /connect flow writes this slot yet; the operator stores it in the broker directly. Without an entry, pokeagent_join_mmo refuses with no_credential, which Clankie says out loud rather than retrying. The minting and holder-file side lives in the world's own joining guide.

Provider credentials

/auth manages model/vendor API keys and OAuth credentials such as openai, openai-codex, anthropic, xai, and elevenlabs. /connect manages service credentials such as Linear and email. Provider consumers may use their declared environment fallback when no broker entry exists; Discord account and internal body credentials remain broker-only. The only internal bearer environment exceptions are the documented operator and captain test/CI overrides.

For compatibility, the clankie service also fills absent environment keys from a gitignored root .env.local; existing shell values win. pnpm doctor reports broker status and exported OpenAI/Anthropic fallbacks without loading .env.local or printing secret values.

Storage implementation and grant validation details live in @clankie/credential-broker.

Clankie account

/gateway signs this Mac in with an invited email and a one-time Cognito code. The broker stores the access and rotating refresh token as clankie-account in Keychain. The non-secret doorway URL and random per-installation id live under publicGateway in settings.json; the public host id is derived from the authenticated account subject and installation id.

The Mac sends only the short-lived access token in its outbound WebSocket handshake. The token is never sent to the mobile app or forwarded with a device request. The gateway verifies its Cognito signature and claims without storing an account or host registry. /gateway disable removes the local account token and installation binding. The old clankie-public-gateway static bearer remains readable only for migration and local development; new users never enter it.

Who holds which secret

Remote access layers four secrets, each held by one party and checked by another. No user ever receives a certificate: one TLS certificate secures every pipe, and identity comes from tokens.

SecretLives onIssued byVerified byProves
TLS certificate for api.clankie.botCaddy's volume on the Lightsail instanceLet's Encrypt, renewed by Caddyevery phone's and Mac's TLS stackthe client reached the real doorway; nothing about who the client is
clankie-account access and refresh tokensMac Keychain via the brokerCognito, after the email one-time codethe gateway, offline against Cognito's JWKS on every connectwhich account and which installation this Mac is
device-session.key~/.clankie/device-session.key, mode 0600the Mac itself on first runthe Mac itself; it never leaves the machinenothing to anyone else; it signs the bearers below
Device session bearerthe phone's Keychainthe Mac at pairing completionthe Mac and relay on every request, with grants read from the projection; the gateway only forwards itwhich paired device is asking, and only for the Mac that signed it

Cognito therefore identifies Macs and only Macs. Phones never talk to Cognito, and the gateway never mints, validates, or stores a device session. Because public TLS terminates on the gateway instance, that process handles forwarded bytes in the clear while it relays them; it retains and logs none of them. Application-layer device-to-Mac encryption is the stated gate before unrelated customers share the doorway (ADR 0151, ADR 0153).