TypeClawTypeClaw
Internals

Secrets

.env vs secrets.json policy, bridge idempotency, KakaoTalk encryption-at-rest

src/secrets/ owns credential storage. Two files compose the surface:

  • <agentDir>/.envKEY=value lines. Loaded by Docker via --env-file. User-owned. Gitignored.
  • <agentDir>/secrets.jsonv2 envelope managed by SecretsBackend (src/secrets/storage.ts), wrapping pi-coding-agent's AuthStorage. Gitignored.
    • providers.<id>{ type: 'api_key', key: <Secret> } or { type: 'oauth', access_token, refresh_token, ... }.
    • channels.<adapter> — per-adapter fields. discord-bot/telegram-bot: { token }. slack-bot: { botToken, appToken }. kakaotalk: { currentAccount, accounts: Record<id, KakaoAccountRecord> } (multi-account, holds email + encryptedPassword for renewal — see Host daemon).

Channel adapters never read the envelope directly. hydrateChannelEnvFromSecrets injects resolved values into process.env[TOKEN_ENV] at boot so src/channels/manager.ts keeps its env-based contract.

The Secret shape

Secret = string | { value?: string, env?: string }. String form is shorthand for { value }. Schema normalises both to object form. Precedence in resolveSecret:

  1. process.env[secret.env] — explicit binding wins.
  2. process.env[defaultEnv] — canonical fallback (from CHANNEL_FIELD_ENV / KNOWN_PROVIDERS[id].apiKeyEnv in src/secrets/defaults.ts).
  3. secret.value — on-disk.
  4. Missing.

Env-wins, file-never-auto-mutated

  • API-key providers: env var set at boot → authStorage.setRuntimeApiKey(...) (in-memory only). secrets.json stays untouched.
  • Channel fields: existing process.env[TOKEN_ENV] kept as-is. Only when env is unset does hydrate inject the resolved value.
  • No auto-promotion. The promoteChannelEnvIntoSecrets module is gone.

Bridge idempotency

SecretsBackend.withLock diffs AuthStorage's flat slice against the prior envelope. Provider unchanged → preserve on-disk Secret bytes verbatim (prevents OAuth-refresh writes from accidentally persisting env-resolved api-key values). API-key value changed → rewrap, preserving prior env field. Added → write as string-value. Removed → actually remove. Unknown type → pass through verbatim (forward-compat). OAuth always passes as flat strings (not env-injectable; refresh tokens are stateful).

Versioning

Writes are v2. Only the v2 envelope is accepted on read. Legacy shapes (v1 envelope, pre-envelope flat shape, auth.json) are rejected with an error — migrate to v2 manually before booting.

KakaoTalk renewal (encryption-at-rest)

KakaoTalk sub-device tokens have a ~7-day TTL; both access and refresh tokens expire on the same cycle. Renewal replays attemptLogin(email, password, device_uuid) with the saved device_uuid. The password is stored as an AES-256-GCM envelope at secrets.json#channels.kakaotalk.accounts.<id>.encryptedPassword. AAD is typeclaw:kakaotalk-password:v1:<containerName>:<accountId>. The 32-byte key lives at ~/.typeclaw/keys/<containerName>.key (0600 / dir 0700), outside the agent folder to defeat agent-folder-only leaks.

Threat model: defense against agent-folder-only leaks. Does NOT protect against full host compromise (OAuth tokens next to the blob already grant equivalent capability) or whole-home backups. Overriding TYPECLAW_HOME into the agent folder collapses the separation.

getAccount() strips email/encryptedPassword; renewal callers use getAccountWithRenewalFields(). mergeUpstreamAccount re-attaches the typeclaw-only fields on every write so SDK-driven refreshes don't strip them.

Rules of thumb

  • Adding an adapter/provider with credentials requires coordinated edits in src/secrets/defaults.ts + schema.ts: per-adapter field schema and CHANNEL_FIELD_ENV entry. Forgetting the env entry silently breaks injection at boot.
  • Never write env-resolved values to disk. Use setRuntimeApiKey(...) (in-memory) instead of authStorage.set(...) (persists).
  • pi-coding-agent's AuthStorage.set always writes through withLock. Our bridge must handle full-slice rewrites that touch providers the caller didn't intend to change. Don't simplify mergeProvidersIntoEnvelope to a wholesale replace.
  • .env and secrets.json are peers, not migration source/sink. typeclaw doctor surfaces resolution origin; no boot-time auto-migration.

On this page