5.8 KiB
Credential System
How Open Notebook stores, encrypts and provisions AI provider credentials — from the settings UI down to Esperanto model instantiation. This is the single reference for the subsystem; the frontend and backend halves are documented together on purpose.
Overview
Users can configure provider credentials through the UI instead of environment variables. Keys are stored as individual Credential records in SurrealDB, encrypted with Fernet, and resolved at model-provisioning time with a database-first, environment-variable-fallback strategy.
Settings UI ──► /credentials API ──► Credential record (encrypted, SurrealDB)
│
Model record ──credential─┘ (preferred: direct link)
│
ModelManager.get_model()
│
credential.to_esperanto_config() ──► Esperanto AIFactory
│
(no linked credential?)
└──► key_provider.provision_provider_keys() ──► env vars ──► Esperanto
The Credential domain model (open_notebook/domain/credential.py)
- One record per credential (e.g. "My OpenAI Key", "Work Anthropic") — multiple credentials per provider are supported.
- Fields:
name,provider,modalities,api_key(PydanticSecretStr, masked in logs), plus provider-specific config (base_url,endpoint,api_version, mode-specific endpoints,project,location,credentials_path). api_keyis encrypted withencrypt_value()before save and decrypted on read (get()/get_all()are overridden). Encryption requiresOPEN_NOTEBOOK_ENCRYPTION_KEY(see content-processing.md for the encryption utility itself).to_esperanto_config()builds the config dict passed to Esperanto'sAIFactory.create_*.provider_config.pystill exists only to migrate legacyProviderConfigrecords.
Provisioning: two paths
- Credential-linked model (preferred). A
Modelrecord has acredentialfield pointing at a Credential.ModelManager.get_model()callscredential.to_esperanto_config()and passes the config directly — no env var mutation, multiple credentials per provider work naturally. - Env-var fallback (
open_notebook/ai/key_provider.py). When a model has no linked credential,provision_provider_keys(provider)copies DB-stored keys intoos.environso Esperanto can read them; pre-existing env vars are left untouched when no DB config exists. ThePROVIDER_CONFIGmap inkey_provider.pydefines the env-var ↔ config-field mapping for simple providers; multi-field providers (Vertex, Azure, OpenAI-compatible) are handled by the dedicated_provision_vertex()/_provision_azure()/_provision_openai_compatible()functions.
The API surface (api/routers/credentials.py)
CRUD plus lifecycle operations: POST /credentials/{id}/test (connection check), /discover (list available models), /register-models (create Model records from discovery), and two migration endpoints (/migrate-from-env, /migrate-from-provider-config). Swagger at /docs documents the shapes.
Supported providers (17) are defined once in the provider registry (open_notebook/ai/provider_registry.py PROVIDERS) — env vars, modalities, test models, discovery URLs and docs links all live there, and connection_tester.TEST_MODELS, credentials_service.PROVIDER_ENV_CONFIG/PROVIDER_MODALITIES and model_discovery.OPENAI_COMPAT_PROVIDERS are derived from it. GET /api/providers exposes the registry to clients — the frontend fetches it at runtime (useProviders() in frontend/src/lib/hooks/use-providers.ts) and renders providers in response order (the registry declaration order). One manual copy remains, enforced by tests/test_credential_provider_validation.py: the SupportedProvider Literal in api/models.py (typing can't be derived at runtime):
- Simple API key: openai, anthropic, google, groq, mistral, deepseek, xai, openrouter, voyage, elevenlabs, deepgram, dashscope, minimax
- URL-based: ollama
- Multi-field: azure, vertex, openai_compatible
Security properties:
- API key values are never returned by any endpoint — only metadata (
has_api_key, counts). - Every URL field passes
validate_url()(SSRF protection); private IPs/localhost are allowed by design for self-hosted services (Ollama, LM Studio). Hostname resolution runs inasyncio.to_threadto avoid blocking the event loop. - Connection testing of Vertex credentials collapses "file missing / not JSON / wrong shape" errors into one generic message so the tester can't be used as a filesystem oracle.
Connection testing (open_notebook/ai/connection_tester.py)
test_provider_connection() makes a minimal API call using the cheapest model per provider (TEST_MODELS map). URL-based providers get a server ping instead (/api/tags for Ollama, /models for OpenAI-compatible). Error messages are normalized for the UI: 401 → "Invalid API key", rate-limit → success ("connection works"), model-not-found → success ("key valid").
Frontend half
src/lib/api/credentials.ts— typed client mirroring the endpoints above. TheCredentialinterface never carries the key value, onlyhas_api_key.src/lib/hooks/use-credentials.ts— TanStack Query hooks (useCredentials,useCreateCredential,useTestCredential, …) with toast feedback. Mutations invalidateCREDENTIAL_QUERY_KEYS.all+ provider/model keys; test results are kept in local state, not the query cache.
Migration paths
Both migration endpoints are idempotent summaries (migrated / skipped / errors):
- From env vars: creates Credential records for providers whose env vars are set.
- From legacy ProviderConfig: converts old singleton records into individual Credentials.