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
~/.digitorn/oauth_providers.tomlOAuth client_id / secret per provider
DIGITORN_MASTER_KEY (env / file / KMS)Master encryption key
credentials table (SQLite / Postgres)Encrypted vault rows, envelope-encrypted
credential_audit tableHash-chained audit log

Scopes

Four scopes with different access semantics:

ScopeResolved whenVisible to
system_widedeployevery app, every user
per_app_shareddeploythis app, every user
per_usersession startthis user, every app they own
per_app_per_usersession startthis user, this app

OAuth flows are forced to per_user (the access token is a delegation from one specific human user). Lookup is scope-strict - no fallback cascade between scopes.

YAML reference

Two equivalent shapes:

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

credential.provider (optional) is a compile-time sanity check - the daemon verifies the named vault entry's provider_name matches.

Modules expose slots

A consumer module declares one or more CredentialSlot instances:

class LLMProviderModule(BaseModule):
credential_slots = [
CredentialSlot(
id="brain_credential",
label="LLM provider credential",
handler_types=["api_key", "bearer_token", "oauth2"],
providers=["openai", "anthropic", "deepseek", ...],
scopes_preferred=["per_user", "system_wide"],
inject={
"api_key": "{block}.config.api_key",
"organization": "{block}.config.organization",
"base_url": "{block}.config.base_url",
},
required=False,
),
]

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

19 handler types

(verified):

TypeUse case
api_keySingle-field secret (most LLM providers).
bearer_tokenOAuth-style bearer (GitHub PAT, MCP).
basic_authusername + password.
oauth2Authorization-code (Google, Slack, Notion, ...).
oauth2_pkcePublic clients (mobile, CLI).
device_codeTVs, CLIs, IoT devices.
multi_fieldGeneric key / value bag.
connection_stringDB urls (Postgres, Mongo, Redis, ...).
aws_access_keyAKID + secret + region.
gcp_service_accountService-account JSON.
azure_adTenant + client + secret.
ssh_keyPrivate key + passphrase.
client_certificatemTLS cert + key.
mcp_serverstdio MCP config.
mcp_httpHTTP MCP url + auth.
hmac_signing_secretWebhook signing.
database_fieldsDiscrete host / port / user / password.
file_uploadUp to 10 MB files.
customSchemaless escape hatch.

Add a handler by subclassing CredentialHandler and registering it on default_registry.

Provider catalog (TOML)

Each provider ships a TOML template under 18 builtins shipping today: anthropic, aws, azure_openai, deepseek, discord_oauth, gcp, github_copilot, github_oauth, github_pat, google_oauth, mongodb, notion, openai, postgres, redis, slack_oauth, stripe, mockprovider.

The TOML overrides handler defaults (icon, display_name, field labels, verify endpoint) without touching code:

[provider]
name = "stripe"
display_name = "Stripe"
handler_type = "multi_field"
icon = "stripe"
category = "payments"

[[fields]]
name = "secret_key"
label = "Secret key"
prefix_check = "sk_"
required = true

[verify]
endpoint = "https://api.stripe.com/v1/balance"
method = "GET"
auth_template = "Authorization: Bearer {secret_key}"
success_codes = [200]

Drop a TOML file in the directory + restart the daemon.

Security architecture

  • Master key - DIGITORN_KMS=env|file|aws_kms|gcp_kms|azure_kv|vault. Default is file (~/.digitorn/master.key) unless DIGITORN_MASTER_KEY is set in env, which auto-selects env. env reads DIGITORN_MASTER_KEY (32 bytes base64url-encoded). Production deployments use a real KMS; the data key is wrapped inside each row's ciphertext (envelope encryption).
  • Cipher - AES-256-GCM with a per-record nonce. Versioned format: 1-byte version, 1-byte flags, 1-byte backend id, 2-byte wrapped-DEK length, then nonce || ct.
  • Audit log - every CRUD + inject + auth flow writes one row to credential_audit. Rows are chained (prev_hash || this_hash). Integrity is verified through the admin API.
  • Log scrubbing - every plaintext value is registered with the global LogScrubber at decryption time. Subsequent log lines carrying the value get redacted before write.
  • RBAC - 4 roles (system_admin, app_admin, app_user, viewer) enforced via FastAPI deps (require_role, require_scope_read, require_scope_write).

OAuth flow

5 builtin OAuth providers in: Notion, Google, GitHub, Slack, Discord.

Background refresh loop runs every 5 minutes and refreshes any credential whose expires_at - now < 600 s. Failures flip the status to expired so the next chat shows the picker dialog.

Revocation in handlers/oauth2.ts::revoke.

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) is routed by 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

digitorn credentials list
digitorn credentials show <id>
digitorn credentials create --provider X -f api_key=sk-...
digitorn credentials delete <id>
digitorn credentials grants <id> # list apps that have access
digitorn credentials grant-add <id> <app-id> # authorize one app
digitorn credentials grant-revoke <id> <app-id> # revoke; add --hard to delete
digitorn credentials admin-list
digitorn credentials admin-create --provider X -f api_key=sk-...
digitorn credentials admin-delete <id>

digitorn yaml migrate-credentials <file-or-dir> [--write] [--recursive]

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

Old apps used inline templates:

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):

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

Run the migrator:

digitorn yaml migrate-credentials path/to/app.yaml --write

The compiler emits a warning when an app uses templates without a credential: block, pointing at this command.

Lifecycle states

StateMeaning
filledUser just stored fields, never verified.
validPassed test_live_connection or got a successful refresh.
expiredTTL hit, refresh failed, or admin marked it.
invalidRevoked, or remote rejected the credential.
pendingOAuth flow in progress.

Cross-references