Skip to main content

Credentials

Centralised encrypted vault for all secrets. Apps reference credentials by name in YAML; users own their secrets in the vault; the runtime injects the right value at the right scope, at the right moment. Replaces inline {{secret.X}} / {{env.X}} templates (which still work as a fallback).

Quick map​

WhereWhat
auth.client_id / client_secret in the MCP server's YAMLOAuth client_id / secret for that server
~/.digitorn/server.keyLocal encryption key
credentials table (SQLite / Postgres)Encrypted vault rows

Scopes​

The schema accepts three scope values on credential.scope:

ScopeStatus
per_userImplemented - every vault entry is stored against the user who added it.
per_app_sharedAccepted by the schema, no runtime behavior yet - resolves the same as per_user.
system_wideAccepted by the schema, no runtime behavior yet - resolves the same as per_user.

There's no system-wide or app-shared credential store behind the latter two today; every vault row belongs to one user, keyed by (user_id, provider_name). ref isn't part of the lookup either - at session start the daemon takes that user's most recently updated vault entry for the brain's provider, not a specific named ref. OAuth flows go through the same per-user path.

YAML reference​

Two equivalent shapes:

yaml
# Compact form (defaults to scope: per_user)
agents:
- id: assistant
brain:
provider: openai
model: gpt-4o
backend: openai_compat
credential: openai_main
yaml
# Explicit form (recommended)
agents:
- id: assistant
brain:
provider: openai
model: gpt-4o
backend: openai_compat
credential:
ref: openai_main
scope: per_user
provider: openai

credential.ref and credential.scope parse and are shown back in the UI, but the actual lookup at session start ignores both - see Scopes above for what really drives resolution.

Modules expose slots​

A consumer module declares one or more credential slot configurations at compile time, specifying what type of credential it accepts, which providers it works with, and how the fields map to the module's internal config paths.

The compiler walks slots + manifests every consumer block; the runtime injector reads the mapping to write decrypted fields at the right path.

Credential types​

Six types, compile-time enum-checked wherever a module declares a credential slot:

TypeUse case
api_keySingle-field secret (most LLM providers).
multi_fieldGeneric key / value bag (e.g. an AWS access key + secret pair).
oauth2Authorization-code flow (Google, Slack, Notion, ...).
connection_stringDB urls (Postgres, Mongo, Redis, ...).
mcp_serverstdio/HTTP MCP server config.
customSchemaless escape hatch.

Provider catalog​

Each entry in the built-in catalog is a small record - display name, category, credential type, icon, its field list (name, label, whether it's masked in the UI, a prefix to sanity-check like sk_ for a Stripe key), and an optional verify call (an HTTP endpoint, method, and the success codes that mean "this key works"). 18 providers ship today: anthropic, deepseek, discord, github, github_copilot, google, groq, mistral, mongodb, mysql, ollama, openai, openrouter, postgres, redis, telegram, webhook_secret, whatsapp.

The catalog is part of the daemon binary, not a directory of files it reads at startup - adding a provider means adding an entry and rebuilding.

Security architecture​

  • Key - a single local key file, ~/.digitorn/server.key (created with 0600 permissions the first time the daemon needs it, 32 random bytes, base64-encoded). There is no KMS backend or environment-variable override - the key lives on the machine running the daemon.
  • Cipher - NaCl secretbox (XSalsa20-Poly1305), a random 24-byte nonce per value, stored as nonce || ciphertext, base64-encoded as one string per credential field.

OAuth flow​

5 well-known OAuth providers: Google, GitHub, Slack, Microsoft, Notion. (Discord uses a bot token instead - an api_key credential, not OAuth.)

The token is refreshed lazily, the moment something needs a fresh one - not on a background timer. Revocation is only wired up for Google and Slack, the two providers with a revoke endpoint on file; revoking for the others just deletes the stored token locally.

MCP stdio token bridging​

For stdio MCP servers, the OAuth token is injected as an environment variable named in auth.env_token_var, and the subprocess is restarted when the token refreshes. SSE / HTTP MCP servers send the token in Authorization: Bearer ... header on every request.

API surface​

The credentials surface (catalog browsing, vault CRUD, OAuth start / refresh / status / callback, per-app manifest and schema, health check, and the admin endpoints) exists as HTTP routes on the daemon, but the full endpoint reference is not documented publicly. Public clients use the SDK or the CLI (next section). For direct integration outside of those, contact your daemon administrator.

CLI​

bash
digitorn secret list <app-id>
digitorn secret get <app-id> <key>
digitorn secret set <app-id> <key> [value]
digitorn secret delete <app-id> <key>

App credential schema (what keys an app expects) is declared in YAML under security.credentials_schema. Values are stored with digitorn secret, not inlined in the manifest.

Migration from {{secret.X}} / {{env.X}}​

Old apps used inline templates:

yaml
brain:
provider: deepseek
config:
api_key: "{{env.DEEPSEEK_API_KEY}}"

New apps add a credential: block (the inline template can stay as a dev fallback):

yaml
brain:
provider: deepseek
backend: openai_compat
credential:
ref: deepseek_main
scope: per_user
provider: deepseek
config:
api_key: "{{env.DEEPSEEK_API_KEY}}" # dev-only fallback

There's no automated migration command - add the credential: block by hand and keep the template as the dev fallback shown above.

Lifecycle states​

StateMeaning
validStored and, for most types, marked good on save.
invalidVerification against the remote provider failed.
pendingOAuth flow in progress (Copilot device-code flow).
expiredThe flow timed out waiting for the user.

Cross-references​