Skip to main content

Configuration reference

Korvun reads one JSON file, passed with --config (default korvun.json):

korvun serve --config /etc/korvun/korvun.json

The field shape is a contract: once you write a config, the names and structure are stable across releases. Validate any file offline with korvun config check <file> (add --preflight to also resolve secrets and reach the providers).

Secrets are environment variables, by NAME — never by value. Fields ending in _env (token_env, api_key_env) hold the name of an environment variable; Korvun reads the value at boot. A secret is never read from the command line, the config file, logs, or error messages. A missing secret is a loud, named boot error. On Korvun Desktop, the SECRETOS card in Ajustes manages the values behind those names over the OS keychain — write-only (no value is ever displayed or returned), with presence shown per name; a value set in the environment wins over the keychain, and the card says so. It works even while the core is stopped — exactly when a boot broken by a missing secret needs it.

Top level​

FieldTypeRequiredMeaning
channelsarrayyes (≥1)Messaging channels to run.
brainsarrayyes (≥1)Orchestrating brains.
routesarrayyes (≥1)Bindings of a channel to a brain.
storageobjectnoDurable conversation store. Absent ⇒ stateless.
observabilityobjectnoAdmin HTTP server. Absent ⇒ ON (loopback).
adminobjectnoThe write surface + the builder. Absent ⇒ read-only.

The asymmetry is deliberate: no storage means off (stateless), no observability means on with safe loopback defaults, no admin means read-only — each the safe default.

channels[]​

FieldTypeRequiredValues / meaning
typestringyestelegram, discord, or webhook.
modestringconditionaltelegram → polling; discord → gateway; webhook takes no mode.
token_envstringyesName of the env var holding the channel's secret (bot token, or the webhook's inbound Bearer secret).
webhookobjectwebhook onlyThe webhook block (below).

A channel registers under its type as its name — that is the value routes reference.

{ "type": "telegram", "mode": "polling", "token_env": "TELEGRAM_BOT_TOKEN" }
{ "type": "discord", "mode": "gateway", "token_env": "DISCORD_BOT_TOKEN" }

Discord needs one manual switch in the Developer Portal — the Discord guide walks through it.

The webhook block​

FieldTypeRequiredValues / meaning
bindstringnoListen address. Default 127.0.0.1:8090 (loopback). A non-loopback bind warns at boot.
pathstringnoInbound POST path. Default /webhook.
outbound_urlstringyesWhere brain replies are POSTed.
outbound_token_envstringnoName of the env var holding an outbound Bearer secret. If named, it must resolve at boot.
mappingobjectnoYour JSON field names → Korvun's message fields.

Edge validation on every request: POST only (405), application/json (415), body ≤ 1 MiB (413), full buffer answers 503 (retry later). The webhook guide is the zero-to-round-trip path.

brains[]​

FieldTypeRequiredValues / meaning
namestringyesUnique name, referenced by routes.
sensitivitystringyespublic | private. private drops cloud models before dispatch — sensitive payloads never leave the box.
dispatchstringnofanout (default: all models in parallel) | sequential (in order, stop at first success — a paid provider is only contacted if the local one failed).
policyobjectyesThe reducer that picks the reply (below).
modelsarrayyes (≥1)The provider catalog for this brain.
agentobjectnoMounts a bounded tool-use agent instead of the default orchestrator.

brains[].policy​

FieldTypeRequiredValues / meaning
kindstringyespriority | consensus.
orderarraynoProvider priority list both reducers use.
  • priority — the reply from the highest-priority provider that answered, in order.
  • consensus — the answer a strict majority of successful providers agree on (needs at least two; a tie or a lone success ⇒ no consensus).

brains[].models[]​

FieldTypeRequiredValues / meaning
providerstringyesollama | groq.
model_idstringyesThe provider's model name (e.g. llama3.2).
localitystringyeslocal | cloud — declared, not derived; the privacy selector routes on it.
base_urlstringnoOverride the adapter default (Ollama: http://127.0.0.1:11434).
api_key_envstringcloud onlyName of the env var holding the API key. Required for groq.

brains[].agent (optional)​

Present ⇒ the brain is a bounded tool-use agent.

FieldTypeRequiredMeaning
toolsarrayyes (≥1)Built-in tools to register. Pure: time, echo, calc. Caged (each REQUIRES its cage block): read_file, http_fetch, webhook_call, memory_note.
max_iterationsintnoHard loop cap.
system_promptstringnoOperator prompt appended after the protocol block.
governancearraynoTri-state grants — tool, mode (allow | shadow | deny), optional channels. Absent ⇒ ungoverned: every listed tool allowed on every channel.
read_fileobjectwith the toolThe jail: root (required), max_bytes.
http_fetchobjectwith the toolThe cage: allow_hosts (required), max_bytes, max_redirects.
webhook_callobjectwith the toolThe cage: allow_hosts (required), max_bytes, timeout_seconds.
memoryobjectwith the toolThe memory block for memory_note: scope (conversation default | brain), max_notes, max_note_runes, budget_runes. Requires storage; scope: "brain" requires the brain's selected model to be local.
skills_dirstringnoAgentSkills-compatible skills directory.
skills_body_budgetintnoTotal rune budget for injected skill bodies.

The full operating guide — what each tool can reach, shadow rehearsal, the network shield, /tools, writing a skill — is on governed tools and skills; notes and recall are on governed memory.

routes[]​

FieldTypeRequiredMeaning
channelstringyesA configured channel's type name.
brainstringyesA configured brain's name.
{ "channel": "telegram", "brain": "assistant" }

storage (optional)​

FieldTypeRequiredMeaning
pathstringnoSQLite file. Empty ⇒ <os user config dir>/korvun/korvun.db.

Present ⇒ durable per-conversation memory that survives restarts. Absent ⇒ stateless. Under the hardened systemd unit, use /var/lib/korvun/korvun.db.

session (optional)​

FieldTypeRequiredMeaning
triggersarraynoExact-match first-token reset commands. Omitted ⇒ ["/new", "/reset"].
daily_atstringnoLocal-time "HH:MM" boundary for daily expiry. Empty ⇒ none.
idle_minintnoIdle expiry in whole minutes. 0 ⇒ none.
recall_maxintnoEnables /recall (0 ⇒ disabled; 1..50): imports the previous session's tail as ONE quoted block — only into an empty session, only on demand.

Present ⇒ session dispatch is on: a conversation is a series of sessions, the newest one active, and a trigger (or a lazy daily/idle expiry) cuts the context hard. Requires the storage block. Absent ⇒ no session behavior at all. An empty block ("session": {}) enables the default triggers with no automatic expiry — that is what the desktop provisions. The operator's view of sessions, /recall and /notes is on the operator console.

observability (optional)​

FieldTypeRequiredMeaning
enabledboolnoUnset ⇒ true.
addrstringnoBind address. Empty ⇒ 127.0.0.1:2112.

The admin server exposes /metrics (Prometheus), /healthz, the read-only control API, and the live view at /ui. It binds loopback by default so a fresh boot exposes nothing to the network; binding 0.0.0.0 is a conscious choice that puts auth/TLS/firewall on you.

admin (optional)​

FieldTypeRequiredMeaning
token_envstringyes (when present)Name of the env var holding the admin bearer token.

The admin block turns on the write surface and the visual builder at /builder. No block, or the variable unset ⇒ read-only: the builder is not mounted at all. The token travels as Authorization: Bearer (constant-time checked, never a cookie) and is only safe over the default loopback bind or behind TLS.

{ "admin": { "token_env": "KORVUN_ADMIN_TOKEN" } }