# Denkeeper — Full Reference

> A security-first, single-binary personal AI agent written in Go.
> Self-hostable on anything from a Raspberry Pi to Kubernetes.

## Quickstart

- Docs:    https://denkeeper.io/docs/
- GitHub:  https://github.com/Temikus/denkeeper
- Install: curl -fsSL https://raw.githubusercontent.com/Temikus/denkeeper/main/install.sh | sh

## API

Running instances expose a REST API documented with OpenAPI 2.0.
Fetch the spec from any live instance at: GET <base-url>/api/v1/openapi.json

The instance also serves a compact discovery file at: GET <base-url>/llms.txt

## Key Concepts

- Agents:    named LLM personas with their own skills, model, and permission tier
- Skills:    markdown files injected into the system prompt to shape agent behaviour
- Channels:  named routing endpoints decoupling sessions from Telegram/Discord adapters
- MCP Tools: denkeeper connects outward to MCP servers to give agents external tools
- Approvals: supervised-tier tool calls require explicit human or API approval
- Fallbacks: automatic provider/model switching on cost limits, rate limits, or errors

## Architecture

Messages flow through:

    Adapter (Telegram/Discord)
    Web Dashboard (WS/SSE)      → Dispatcher → Engine (per agent) → LLM Router → Provider
    REST API (POST /api/v1/chat)                    ↕                    ↕
                                                MemoryStore          CostTracker
                                                (SQLite)             + Pricing Registry

Three permission tiers: autonomous (all actions), supervised (chat + tools with approval), restricted (chat + read-only tools).

## TOML Configuration Reference

Configuration is a single TOML file (default: ~/.denkeeper/denkeeper.toml).
All paths default relative to `data_dir`. Override with `DENKEEPER_*` env vars.

### Root

    data_dir = "~/.denkeeper"        # base data directory
    max_tools = 50                   # combined tools + plugins limit

### [telegram]

    token = ""                       # bot token from @BotFather
    allowed_users = [123456789]      # allowed Telegram user IDs (int array)

### [discord]

    token = ""                       # Discord bot token
    allowed_users = ["snowflake_id"] # Discord user snowflake IDs

### [llm]

    default_provider = "openrouter"  # default provider name
    default_model = ""               # default model identifier
    max_cost_per_session = 0.0       # legacy alias for cost_limit_hard
    cost_limit_soft = 0.0            # soft cost limit per session (USD)
    cost_limit_hard = 1.0            # hard cost limit per session (USD)
    stream_idle_timeout_secs = 120   # idle timeout for LLM SSE streams

    [llm.openrouter]
    api_key = ""                     # OpenRouter API key

    [llm.openrouter.reasoning]
    enabled = false                  # activate reasoning mode
    effort = "medium"                # "xhigh", "high", "medium", "low", "minimal", "none"
    max_tokens = 0                   # reasoning token budget (0 = provider default)
    exclude = false                  # omit reasoning from response

### [[llm.providers]]

Named provider instances (array). Multiple instances of the same type allowed.

    [[llm.providers]]
    name = "my-anthropic"            # unique provider name
    type = "anthropic"               # "anthropic", "openai", "openrouter", "ollama"
    api_key = ""                     # API key (required except ollama)
    base_url = ""                    # API endpoint override
    organization = ""                # OpenAI organization ID (openai type only)
    cost_limit_soft = 5.00           # per-provider soft cost limit (USD per session)
    cost_limit_hard = 10.00          # per-provider hard cost limit (USD per session)
    default_rate_per_1k_tokens = 0.01 # per-provider fallback pricing rate

    [llm.providers.model_prices."custom-model"]
    input = 2.0                      # USD per million input tokens
    output = 8.0                     # USD per million output tokens
    cached_input = 0.5               # USD per million cached input tokens

Legacy single-slot syntax (`[llm.anthropic]`, `[llm.openai]`, etc.) is still supported.

    [llm.ollama]
    base_url = "http://localhost:11434"  # Ollama server URL

### [[llm.fallback]]

Automatic failover strategies.

    [[llm.fallback]]
    trigger = "cost_limit"           # "cost_limit", "rate_limit", "error"
    action = "switch_provider"       # "switch_provider", "switch_model", "wait_and_retry"
    provider = "fallback-provider"   # target provider (for switch_provider)
    model = ""                       # target model (for switch_model)
    scope = "soft"                   # "soft" or "hard" (for cost_limit)
    max_retries = 3                  # max retries (for wait_and_retry)
    threshold = 0.0                  # cost threshold (for cost_limit trigger)
    backoff = "exponential"          # "exponential" or "constant"

### [session]

    tier = "supervised"              # default permission tier
    approval_timeout = "5m"          # timeout for pending approvals
    approval_retries = 0             # retries for timed-out approvals

### [[agents]]

Multi-agent definitions. If absent, a single "default" agent is synthesized.

    [[agents]]
    name = "my-agent"                # unique agent name
    description = ""                 # human-readable description
    persona_dir = ""                 # path to persona files (SOUL.md, USER.md, MEMORY.md)
    skills_dir = ""                  # agent-specific skills directory
    adapters = ["telegram"]          # adapter bindings
    llm_provider = ""                # override default provider
    llm_model = ""                   # override default model
    session_tier = ""                # override default permission tier
    browser_url_allowlist = []       # allowed domains for browser automation
    cost_limit_soft = 0.0            # soft cost limit override
    cost_limit_hard = 0.0            # hard cost limit override
    max_context_messages = 0         # max messages in LLM context (0 = 50)
    max_tool_rounds = 0              # max tool-call rounds (0 = 50)
    supervisor = ""                  # supervisor agent name for tool call review
    supervisor_timeout = ""          # supervisor call timeout
    supervisor_context_messages = 5  # recent messages in supervisor prompt
    supervisor_body_excerpt_len = 0  # max chars of skill body in supervisor prompt (0 = use default 500)
    supervisor_tool_desc_len = 0     # max chars of tool description in supervisor prompt (0 = use default 200)
    reviewer_model = ""              # LLM model for post-turn review (empty = disabled)
    reviewer_provider = ""           # LLM provider for reviewer (empty = inherit agent's)
    review_max_iterations = 0        # reviewer tool-call rounds (0 = 6)
    review_timeout = ""              # reviewer timeout (empty = 2m)
    nudge_memory_interval = 0        # user turns between memory review nudges (0 = disabled)
    nudge_skill_interval = 0         # tool rounds between skill review nudges (0 = disabled)

### [[channels]]

Named routing endpoints decoupling sessions from adapters.

    [[channels]]
    name = "general"                 # unique channel identifier
    agent = "my-agent"               # target agent name
    adapters = ["telegram:123"]      # adapter bindings
    delivery = "single"              # "single" or "broadcast"
    session_mode = "persistent"      # "persistent" or "ephemeral"

### [memory]

    db_path = "data/memory.db"       # SQLite database path
    retention_days = 90              # conversation retention period
    max_conversations = 10000        # max stored conversations
    cleanup_interval = "1h"          # cleanup frequency
    persona_memory_char_limit = 2200 # MEMORY.md character limit (0 = unlimited)
    persona_user_char_limit = 1375   # USER.md character limit (0 = unlimited)

### [log]

    level = "info"                   # "debug", "info", "warn", "error"
    format = "text"                  # "text" or "json"

### [api]

    enabled = true                   # enable API server
    listen = ":8080"                 # bind address
    tls = false                      # enable HTTPS
    cert_file = ""                   # TLS certificate path
    key_file = ""                    # TLS private key path
    cors_origins = []                # allowed CORS origins
    rate_limit = 0                   # max requests/sec per key (0 = unlimited)
    login_rate_limit = 5             # login attempts per window
    login_rate_window = "15m"        # login rate window
    websocket_enabled = true         # enable WebSocket endpoint
    websocket_max_connections = 0    # max concurrent connections (0 = unlimited)
    websocket_replay_buffer_ttl = "5m"  # event replay buffer duration
    external_url = ""                # public URL for OAuth callbacks
    timezone = "UTC"                 # IANA timezone for cron expressions
    onboarding_dismissed = false     # hide onboarding checklist (set via API)
    wizard_completed = false         # post-auth setup wizard completed (set via API)

### [api.mcp_server]

    [api.mcp_server]
    enabled = false                  # opt-in MCP server at /api/v1/mcp
    session_timeout = "30m"          # idle session cleanup duration
    transport = "streamable"         # "streamable" (default) or "sse" (legacy)
    chat_timeout = "2m"              # max time for chat tool calls
    stateless = false                # disable session tracking

### [[api.keys]]

    [[api.keys]]
    name = "my-key"                  # key label
    key = "sk-..."                   # secret key value
    scopes = ["chat", "admin"]       # permission scopes

Available scopes: chat, admin, sessions:read, sessions:write, agents:read,
approvals:read, approvals:write, schedules:read, schedules:write,
skills:read, skills:write, tools:read, tools:write, costs:read,
channels:read, channels:write, audit:read

### [api.auth]

    password_hash = ""               # bcrypt hash (use `denkeeper passwd`)
    session_secret = ""              # AES-256 hex key (64 hex chars)
    session_max_age = "24h"          # cookie lifetime
    preferred_login_method = "auto"  # "auto", "password", "apikey"
    session_record_retention = "720h"  # session record retention

### [api.auth.oidc]

    enabled = false                  # enable OIDC SSO
    issuer = ""                      # OIDC issuer URL
    client_id = ""                   # OAuth2 client ID
    client_secret = ""               # OAuth2 client secret
    redirect_url = ""                # callback URL
    scopes = ["openid", "email", "profile"]
    allowed_emails = []              # allowed email addresses

### [tools.*]

MCP tool server definitions.

    [tools.my-tool]
    transport = "stdio"              # "stdio" or "sse"
    command = "my-mcp-server"        # server binary (stdio)
    args = ["--flag"]                # arguments (stdio)
    env = { API_KEY = "..." }        # environment variables
    url = ""                         # remote URL (sse)
    headers = {}                     # HTTP headers (sse)
    request_timeout_secs = 30        # per-request timeout
    sse_keep_alive_secs = 15         # TCP keepalive interval (sse)
    auth = ""                        # "" or "oauth"
    client_id = ""                   # OAuth client ID
    client_secret = ""               # OAuth client secret
    scopes = []                      # OAuth scopes
    allow_loopback = false           # bypass SSRF loopback block
    disabled_tools = []              # MCP tool names to exclude from LLM

### [mcp]

Global MCP settings.

    request_timeout_secs = 30        # per-request timeout
    sse_keep_alive_secs = 15         # TCP keepalive interval
    auto_restart = true              # auto-restart crashed servers
    max_restart_attempts = 3         # failure threshold before giving up
    restart_cooldown = "5m"          # cooldown before reset
    init_retry_attempts = 5          # initial connection retries
    init_retry_backoff = "2s"        # backoff between retries
    url_allowlist = []               # allowed hosts for SSE servers

### [[schedules]]

    [[schedules]]
    name = "daily-summary"           # unique schedule identifier
    type = "agent"                   # "system" or "agent"
    schedule = "0 9 * * *"           # cron expression or interval
    skill = "daily-summary"          # skill to invoke
    agent = "default"                # target agent
    session_tier = "supervised"      # permission tier
    channel = "telegram:123"         # delivery channel
    tags = ["daily"]                 # freeform labels
    enabled = true                   # enable/disable

### [voice]

    stt_provider = ""                # "openai" or "" (disabled)
    tts_provider = ""                # "openai" or "" (disabled)
    tts_voice = "alloy"              # voice ID
    auto_voice_reply = false         # reply with voice when user sends voice

### [web]

    enabled = true                   # enable web search/fetch tools

    [web.search]
    provider = "duckduckgo"          # "duckduckgo" or "tavily"
    api_key = ""                     # provider API key
    max_results = 5                  # results to return

    [web.fetch]
    timeout = "30s"                  # HTTP timeout
    max_size_bytes = 5242880         # response size limit (5MB)
    user_agent = ""                  # HTTP User-Agent header
    respect_robots_txt = false       # check robots.txt
    respect_agents_txt = false       # check agents.txt

    [web.fetch.jina]
    enabled = false                  # enable Jina Reader fallback for JS-heavy pages

### [browser]

    enabled = false                  # enable browser automation
    image = "ghcr.io/temikus/denkeeper-browser:latest"
    memory_limit = "512m"            # container memory limit
    cpu_limit = "1"                  # container CPU limit
    profile_dir = "data/browser-profiles"  # browser profile directory
    session_ttl = "10m"              # session idle timeout
    max_pages = 5                    # max concurrent pages per agent

    [browser.url_allowlist]
    domains = ["*.example.com"]      # allowed domains (supports wildcards)

### [kv]

    max_keys_per_agent = 1000        # max keys per agent
    max_value_bytes = 65536          # max value size (bytes)
    cleanup_interval = "1h"          # expiry cleanup interval

### [audit]

    enabled = true                   # enable audit logging
    retention_days = 30              # event retention
    cleanup_interval = "1h"          # cleanup frequency
    buffer_size = 1000               # in-memory buffer capacity

### [costs]

    default_rate_per_1k_tokens = 0.01  # fallback pricing rate

    [costs.model_prices.my-custom-model]
    input = 2.0                      # per million input tokens
    output = 8.0                     # per million output tokens
    cached_input = 0.5               # per million cached tokens

### [security]

Plugin signature verification.

    trusted_keys = []                # paths to Ed25519 public keys
    allow_unsigned = true            # allow unsigned plugin binaries

### [sandbox]

Sandbox runtime backend for code execution.

    runtime = "docker"               # "docker" or "kubernetes"

    [sandbox.kubernetes]
    namespace = "denkeeper-sandboxes"  # K8s namespace
    kubeconfig = ""                  # path to kubeconfig
    runtime_class = ""               # RuntimeClassName (gvisor, kata)

### [plugins.*]

Plugin definitions.

    [plugins.my-plugin]
    type = "subprocess"              # "subprocess" or "docker"
    command = "my-plugin"            # plugin binary
    args = []                        # command arguments
    env = {}                         # environment variables
    capabilities = ["tools"]         # declared capabilities
    image = ""                       # Docker image (docker type only)
    memory_limit = ""                # container memory limit (docker only)
    cpu_limit = ""                   # container CPU limit (docker only)
    network = "none"                 # Docker network mode
    volumes = []                     # bind mounts (docker only)

### [otel]

    enabled = false                  # enable OpenTelemetry
    traces_endpoint = ""             # OTLP HTTP endpoint
    service_name = "denkeeper"       # OTel service name

## Environment Variable Overrides

Key environment variables (full list in docs):

    DENKEEPER_CONFIG                 # config file path
    DENKEEPER_DATA_DIR               # base data directory
    DENKEEPER_TELEGRAM_TOKEN         # Telegram bot token
    DENKEEPER_DISCORD_TOKEN          # Discord bot token
    DENKEEPER_LLM_PROVIDER           # default LLM provider
    DENKEEPER_LLM_MODEL              # default LLM model
    DENKEEPER_LLM_ANTHROPIC_API_KEY  # Anthropic API key
    DENKEEPER_LLM_OPENAI_API_KEY     # OpenAI API key
    DENKEEPER_LLM_OPENROUTER_API_KEY # OpenRouter API key
    DENKEEPER_LLM_OLLAMA_BASE_URL    # Ollama base URL
    DENKEEPER_API_LISTEN             # API listen address
    DENKEEPER_API_EXTERNAL_URL       # public API URL
    DENKEEPER_LOG_LEVEL              # log level
    DENKEEPER_SESSION_TIER           # default permission tier
    DENKEEPER_MEMORY_DB_PATH         # database path
