feat: per-user LLM provider configuration with encrypted API-key storage #333
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol#333
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Let each user configure the provider + model + API key the built-in agent will use, stored per-user, encrypted at rest — the prerequisite (ADR-0029 §1 "pluggable provider" + §5 "per-user keys, encrypted at rest") that the agent-loop / chat-UI tickets consume. Part of epic #47.
This ticket is config + storage + crypto + settings UI only. The actual
LlmProvideradapters and the agent loop (inference, SSE, conversations) are deferred to the agent-loop ticket, which reads the decrypted key via a server-only helper this ticket provides.Scope
apps/api/lib/crypto/) — AES-256-GCMencrypt/decryptkeyed by a new env varCAROL_ENCRYPTION_KEY(32-byte key, base64/hex). Random IV per op; persist iv‖tag‖ciphertext. Validate the env var on use (not at boot) with a clear error if missing/malformed. AddCAROL_ENCRYPTION_KEYto the README Configuration table.024_user_llm_credentials:user_id(pk, FK users.id cascade),provider(anthropic|openai_compatible),model(text),base_url(text null — foropenai_compatibleincl. local Ollama),api_key_ciphertext(text null — null for keyless local),created_at,updated_at. Schema + entity + repository (get/upsert/delete; ciphertext never leaves the repo except via an internal decrypt helper).{ provider, model, baseUrl, keyConfigured: boolean }(never the key); request{ provider, model, baseUrl?, apiKey? }(apiKey write-only — set encrypts + stores; omitted on update keeps the existing key; explicit clear removes it).GET /api/settings/llm(config DTO +keyConfigured),PUT /api/settings/llm(set),DELETE /api/settings/llm(clear). Plus a server-onlygetDecryptedLlmKey(userId)for the future agent loop (not exposed via the API). Validate provider/model; require a base URL foropenai_compatible(allow http for localhost/private, https otherwise).@carol/api-clienthooks; i18naccount.llm.*in Carol's voice.CAROL_ENCRYPTION_KEY→ clear error. OpenAPI for the new routes; api-client regen.Acceptance criteria
keyConfigured).getDecryptedLlmKey(userId)returns the plaintext server-side for the future loop; local (no-key) configs are supported.CAROL_ENCRYPTION_KEYdocumented in the README.Out of scope
LlmProvideradapters + inference and the agent loop / SSE / conversations (next ticket consumes this). Voice. A shared instance-level fallback key (ADR-0029 notes there is none by default).Implements ADR-0029. Part of epic #47.