Secrets
.env vs secrets.json policy, bridge idempotency, KakaoTalk encryption-at-rest
src/secrets/ owns credential storage. Two files compose the surface:
<agentDir>/.env—KEY=valuelines. Loaded by Docker via--env-file. User-owned. Gitignored.<agentDir>/secrets.json—v2envelope managed bySecretsBackend(src/secrets/storage.ts), wrappingpi-coding-agent'sAuthStorage. 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, holdsemail+encryptedPasswordfor 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:
process.env[secret.env]— explicit binding wins.process.env[defaultEnv]— canonical fallback (fromCHANNEL_FIELD_ENV/KNOWN_PROVIDERS[id].apiKeyEnvinsrc/secrets/defaults.ts).secret.value— on-disk.- Missing.
Env-wins, file-never-auto-mutated
- API-key providers: env var set at boot →
authStorage.setRuntimeApiKey(...)(in-memory only).secrets.jsonstays 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
promoteChannelEnvIntoSecretsmodule 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 andCHANNEL_FIELD_ENVentry. Forgetting the env entry silently breaks injection at boot. - Never write env-resolved values to disk. Use
setRuntimeApiKey(...)(in-memory) instead ofauthStorage.set(...)(persists). pi-coding-agent'sAuthStorage.setalways writes throughwithLock. Our bridge must handle full-slice rewrites that touch providers the caller didn't intend to change. Don't simplifymergeProvidersIntoEnvelopeto a wholesale replace..envandsecrets.jsonare peers, not migration source/sink.typeclaw doctorsurfaces resolution origin; no boot-time auto-migration.