Codumentor logo Codumentor

Configuration

Codumentor is configured through YAML, typically named codumentor.yaml. That file is one layer of configuration. It is the instance only when you pass --config or the process uses workspace home (the usual full-edition server). Community / user-home runs keep data under $CODUMENTOR_HOME even when a project YAML is discovered as an overlay.

This file controls repositories to analyze, file patterns, model settings, and tool behavior.

Which configuration guide is this? This is the complete operator reference — every option, including authentication, the API server, logging, and config inheritance. If you just want a single-user instance running quickly, start with the shorter Setup → Configuration. Community "install once, run anywhere" is in Community. To stand up a team/production instance end-to-end, follow the Self-Hosting Walkthrough.

Tip — edit config from a chat. Instead of hand-editing this YAML, admins can use the Configuration Assistant: an admin-only chat (Admin page → Configuration Assistant) that changes the running instance's config in plain language. It grounds itself in this reference and the live schema, validates the change, shows you the exact YAML diff and whether it applies live / needs a restart, and only writes after you approve — with a timestamped backup and one-step rollback. Enable it with the configuration_assistant plugin. Pass target=user for models and keys, target=project for repos and project plugins.

Two axes: config layers vs instance home

These are independent. Mixing them is allowed; naming them is the product.

Config layers (what the process believes), later wins:

bundled flavor default  →  user overlay  →  project overlay  →  --config

Instance home (where bytes live): workspace (next to the YAML) · user ($CODUMENTOR_HOME) · ephemeral (tempdir).

Community without --config uses user home. A discovered codumentor.yaml is then a project overlay (repos / project plugins only; it cannot redirect model URLs or auth). Full edition with a project YAML uses workspace home: that YAML is the instance. --config is always trusted.

See How a process finds its configuration below for origins and the untrusted-overlay strip.

Environment Variable Substitution

All string values in the configuration file support environment variable substitution using ${VAR_NAME} syntax. This allows you to keep sensitive values (like API keys) or environment-specific paths out of your configuration files.

Example:

models:
  agent: "${AGENT_MODEL}"
  agent_base_url: "${BASE_URL}/v1"
  embedding_api_key: "${GOOGLE_API_KEY}"

vector_db:
  path: "${DATA_DIR}/vector_db"

repos_dir: "${REPOS_DIR}"

Environment variables are substituted when the configuration is loaded. If a variable is not set, the original ${VAR_NAME} string is kept as-is.

Note: Environment variable substitution works for all string values throughout the configuration, including nested dictionaries and list items.

Config inheritance with extends

You can let one config build on another using the extends key. The value can
be a relative path (resolved from the file that declares it) or an absolute
path. Extends are applied depth-first: parents load first, then the child
overrides them.

# child.yaml
extends: base.yaml        # or a list: [base.yaml, extras/feature.yaml]
models:
  agent: "gemini-2.0-pro" # overrides the parent value
vector_db:
  path: "./data/vector_db_variant" # override while inheriting other settings

Cycles are detected and raise an error. Extends works together with the CLI
-c/--config merging: each file is resolved with its own extends chain, then
the resulting configs are merged in the order you pass on the CLI.

How a process finds its configuration

An effective config is always a merge, later wins:

bundled flavor default  →  user overlay  →  project YAML / --config

codumentor status and GET /ui/config report which layers contributed.

A discovered project YAML (found in CWD or via the git-root search, not
passed as --config) is an overlay when the instance home is user
(community / cd into a clone). That overlay cannot override inference
redirects: models.* base URL and API key fields, and the entire auth:
section. A cloned repo can still name repos and the agent model; it cannot
point the process at an attacker's proxy. When the YAML is the instance
(workspace home, full edition), it is trusted. Explicit --config is always
trusted.

OriginWhen
bundledThe packaged flavor default. Always the first layer on a loader-produced config.
user$CODUMENTOR_HOME/config.yaml contributed (skipped if the file is missing).
explicitYou passed -c / --config PATH.
discoveredThe flavor's default filename (codumentor.yaml, or codumentor-<flavor>.yaml) was found in CWD, at the git root, or in the git root's parent.
ephemeralRetained for reload of processes that recorded this origin before the floor flip. New no-YAML loads record the bundled file as bundled; instance home kind=ephemeral is the persistence axis.

Inspect a running (or about-to-run) process:

codumentor status           # banner: instance home, workspace, config, flavor
codumentor status --json

GET /ui/config includes the same facts as instance_home and config_layers.

Migration: omitted keys now inherit the flavor file

Before this floor flip, a workspace YAML was composed with Pydantic defaults
for any key it omitted. Config.include_patterns defaults to [] ("ingest
by known extension"); default_codumentor.yaml ships a language whitelist
instead. A YAML that never set include_patterns used to mean ingest-by-extension
and now inherits the flavor whitelist.

**If you omitted include_patterns in order to ingest every known
extension, set it explicitly:**

include_patterns: []

The same rule applies to every other key the flavor file sets
(session_report, logging.level, exclude_patterns, plugins, …).
Keys the flavor file also omits still use Pydantic defaults. Complete
files such as codumentor-selfdev.yaml are unchanged: they already set
the keys they care about.

Config() constructed in tests (not via load_config) still uses
Pydantic defaults only.

flavor:

flavor: codumentor    # or marketing | minimal | root | investigate

The build-time edition (what the binary contains) is a different axis;
see Editions. The old config key edition: is refused.

--flavor / CODUMENTOR_FLAVOR / invoking codumentor-<flavor> select the
bundled default_<flavor>.yaml floor (and the first-run scaffold when no
file exists). An existing file that sets flavor: still wins. An explicit
--flavor that the merge result contradicts is a hard error.

Configuration File Structure

The configuration file uses the following structure:

repos:
  - "https://github.com/user/repo1.git"
  - "path/to/local/repo2"

repos_dir: "repos"

# include_patterns: []  # empty = ingest every known extension; omit = flavor whitelist
# exclude_patterns use sensible defaults (see below)

ingestion:
  max_file_size: 1048576  # 1 MB (default) — skip files larger than this

chunking:
  size: 1000
  overlap: 100

models:
  embedding: "text-embedding-004"
  embedding_base_url: "https://generativelanguage.googleapis.com/v1beta/openai/"
  agent: "gemini-2.0-flash"
  agent_base_url: "https://generativelanguage.googleapis.com/v1beta/openai/"

vector_db:
  backend: "chroma"
  path: "./data/vector_db"

cache:
  embedding_enabled: true
  embedding_db_path: null
  embedding_max_age_days: 365

agent:
  max_iterations: 10
  storage_dir: "./data/agent_storage"

tools:
  tool_result_max_content_length: 30000  # Max chars before tool output is chunked
  shell_output_max_size: 10240
  file_listing_max_size: 10240
  shell_timeout: 900
  shell_always_allow: false
  search_concurrency: 5

api:
  host: "127.0.0.1"
  port: 2638
  reload: false
  enable_web_ui: true        # Serve the protocol-native /ui/* API + SPA
  enable_openai_api: false   # Legacy OpenAI-compatible /v1/* surface (opt-in)
  stream_flush_ms: 250       # Coalesce streamed deltas into one UI event per beat (0 = per-chunk)
  stream_flush_bytes: 4096   # Flush the delta buffer early once it reaches this size

ui:
  welcome_title: "Welcome to Codumentor"
  welcome_description:
    en: "Your self-hosted AI coworker."
    hu: "Önállóan üzemeltethető AI munkatárs."
  disabled_extensions: []

auth:
  provider: "none"  # Options: "none", "gitea"
  gitea:
    base_url: "https://gitea.example.com"
    token_cache_ttl_minutes: 30  # Token cache TTL in minutes

prompt:
  template_dir: "prompts"
  system_prompt_template: "system_prompt.jinja2"
  max_iterations_notification_template: "max_iterations_notification.jinja2"
  custom_instructions: []
  debug_mode: false
  show_tool_calls: true

logging:
  level: "INFO"
  file: "./logs/codumentor.log"
  logger_levels: {}

plugins:
  - module: "codumentor.plugins.filesync"
    class: "FileSyncPlugin"

Configuration Options

repos

List of repositories to analyze. Entries can be either simple strings (Git URLs or local paths) or structured objects that describe alternative SCM providers:

repos:
  # Git support (string form)
  - https://github.com/example/project.git

  # Explicit configuration for alternate SCMs provided by plugins
  - scm: svn
    url: https://svn.example.com/repos/legacy
    name: legacy_svn
    options:
      branch: trunk

  # Pin to a branch/commit
  - url: https://github.com/example/project.git
    branch: release         # or commit: <sha> / pin: <tag>

  # Inline ref pin using URL suffix (common git syntax)
  - https://github.com/example/project.git@release
  - https://github.com/example/project.git#v1.2.3

  # Use an existing local checkout
  - path: /opt/shared/repo
    branch: feature/search  # optional: will checkout this ref before ingestion

  # Pre-synced directories (requires filesync plugin)
  - scm: filesync
    name: shared_docs
    path: "\\\\fileserver\\engineering\\docs"

  # Ingest only part of a repository
  - name: knowledge-repo
    path: /srv/kb
    ingest_paths: knowledge      # or a list: [knowledge, docs]

##### Managing repositories from the UI

Administrators can add, edit and remove repositories from **Admin →
Repositories** (/admin/repos) instead of editing this file by hand. There is no
separate repository store: this YAML file remains authoritative. The page
edits the repos: list in the leaf config file (the last entry of
CODUMENTOR_CONFIG_FILES) and then drives the same hot-reload the
/admin/config button does, so a hand-edit on the host and a UI edit can never
disagree. Requires the admin:config permission.

What it writes, for an entry added through the form:

repos:
  # your existing entries and their comments are left untouched
  - name: service          # the identity: the clone folder, the file-key prefix
                           # and the @-mention token
    url: https://gitea.example.com/team/service.git
    branch: develop        # omitted entirely when you keep "Remote default"
    options:
      platform: gitea      # pre-filled from the host when recognizable

An scm: svn entry, and an scm: filesync one:

repos:
  - name: legacy-erp
    scm: svn
    url: https://svn.example.com/erp/trunk
    options:
      username: builduser
      password: ${SVN_PASSWORD}   # a reference — never the password itself
      revision: "48213"           # omit to follow the latest
  - name: shared-docs
    scm: filesync
    path: "\\\\fileserver\\projects\\docs"   # the source; copied, never modified
    options:
      target_dir: /srv/codumentor/repos/shared-docs
      clear_target: true          # the target is emptied before every copy

Notes on behaviour worth knowing before you rely on it:

##### ingest_paths: ingesting part of a repository

include_patterns / exclude_patterns are global; ingest_paths is per-repo,
and it narrows the walk rather than filtering its result, so excluded files
are never opened, hashed, or embedded. Use it when a repository's contents are
not homogeneous:

Notes:

Plugins can register additional SCM providers (e.g., "svn", "filesystem") to handle these entries during ingestion.

Generic *_overrides

Any top-level key can have a companion <key>_overrides entry. After configs
are merged (and env vars substituted), overrides are applied automatically:

repos_overrides entries can be plain URL strings (same formats accepted by
repos) — they are normalized to dicts before merging.

Examples:

# base.yaml
logging:
  level: INFO
  logger_levels:
    codumentor.tools: WARNING

repos:
  - name: kb
    url: https://example.com/kb.git
    branch: main

# variant.yaml
logging_overrides:
  json_logging: true
  logger_levels:
    codumentor.tools: ERROR
    codumentor.agent: DEBUG

include_patterns_overrides:
  - "**/*.md"

repos_overrides:
  # Match existing repo by name — only override the branch
  - name: kb
    branch: feature-branch
  # Add a new repo using a plain URL string
  - ssh://git@gitea.example.com:2244/org/sandbox.git
  # Add a new repo with full dict syntax
  - scm: svn
    url: "https://svn.example.com/trunk"
    name: legacy-svn

logging keeps level: INFO, enables JSON logging, and merges logger levels.
include_patterns gains the additional patterns while preserving the base ones.
repos gets the kb repo's branch updated to feature-branch, plus two new
repos appended.

##### Nested *_overrides

*_overrides also works deeper than the top level, using the same merge
rules. This is handy for surgically overriding one item in a nested list of
named items — e.g. a single LDAP directory under auth.ldap_directories
without restating the whole list (a plain dict merge would replace it).

A nested <key>_overrides entry merges into its sibling <key> when present;
when <key> is absent (e.g. a parent config dropped it), the override applies
to an empty base, so list entries are added and dict values are set directly.

Because any key ending in _overrides is treated as a merge directive at
every level, config field names must never end in _overrides.

# base.yaml
auth:
  provider: ldap
  ldap_directories:
    - name: corp
      server_url: ldaps://corp.example.com
      bind_dn: cn=svc,dc=corp
      bind_password: pw
      base_dn: dc=corp
      security_mode: ldaps
    - name: partner
      server_url: ldaps://partner.example.com
      bind_dn: cn=svc,dc=partner
      bind_password: pw
      base_dn: dc=partner

# variant.yaml — flip only the corp directory to STARTTLS
auth:
  ldap_directories_overrides:
    - name: corp
      security_mode: starttls

corp keeps every other field and switches to starttls; partner is
untouched. An override entry whose name is not present is appended as a new
directory.

plugins

Codumentor supports plugins to extend functionality. Plugins are configured as a list of plugin specifications:

plugins:
  - module: "codumentor.plugins.filesync"
    class: "FileSyncPlugin"
  - module: "codumentor.plugins.mcp"
    class: "MCPPlugin"
    args:
      executable: "python"
      args: ["-c", "from mssql_mcp_server import main; main()"]

To change one plugin without restating the whole list, use plugins_overrides
on an extends: child. Entries match by module+class (see
Generic *_overrides above), not by name.

Workspace Isolation Plugin

The workspace isolation plugin sandboxes each conversation session using Linux bubblewrap with overlay filesystems. It requires bubblewrap with overlay support (apt install bubblewrap on current distros), an overlayfs-capable kernel, and — on Ubuntu 24.04+ — userns allowed for the bwrap binary under AppArmor; see Plugins in Production → Workspace isolation prerequisites for the install steps.

plugins:
  - module: codumentor.plugins.workspace_isolation
    class: WorkspaceIsolationPlugin
    priority: 5
    args:
      enabled: true
      workspace_ttl: 86400         # 24 hours
      persistent_home: true        # Persistent, shared $HOME (default: true)
      subagent_isolation: shared   # "shared" (default) or "isolated"
      sandbox:
        unshare_net: true         # Block networking inside sandbox (default)

Key options:

OptionTypeDefaultDescription
enabledbooltrueEnable/disable workspace isolation
workspace_ttlint86400Workspace time-to-live in seconds
persistent_homebooltrueBind-mount a persistent HOME (<workspace>/home/) instead of tmpfs. When true, $HOME survives across sandbox invocations and is shared among the main agent, subagents, and external agents. Set to false for legacy ephemeral HOME.
subagent_isolationstr"shared""shared": subagents share the main agent's sandbox (simple, recommended). "isolated": each subagent gets its own sandbox with VCS integration and merge conflict resolution.
sandboxdict{}Sandbox hardening (see plugin README)

repos_dir

Base directory where repositories will be cloned and stored.

What the file tools may reach outside it. read_file and ripgrep accept
an absolute path outside repos_dir — reading a scratch file the agent just
produced with shell (objdump -d prog > /tmp/dis.txt) no longer needs it
moved into the workspace first. There is no setting for this; the answer is
read from boundaries that are already declared elsewhere, and reads outside the
workspace are refused whenever one of them is in force:

In forceReads outside repos_dirWhy
workspace_isolation (overlay mode)refusedthe sandboxed shell cannot see the host either — the file tools must not become a wider door than the shell
An access profile scoped to named repos (repos: [names]), or one that withholds the shell tool (chat_only, qa_readonly)refusedthe operator declared a boundary; for a principal without shell, "it could read it anyway" is false
Neither (the default single-host install)allowed, after an approval promptshell is unconfined, so refusing the same path to read_file stopped nothing — but shell asks before reading a host file, so the file tools ask through the same permission provider (a Read(<path>) rule, auto-approved wherever shell is). Reads inside the workspace prompt for nothing

Writes are never relaxed. write_file, replace_in_file and the Office
save tools stay confined to the workspace in every configuration, so a run
cannot leave artifacts outside it. Relative paths (../..) are always resolved
inside their repository — only an absolute path can name a host location.

include_patterns and exclude_patterns

File patterns using gitignore-style glob syntax. These control which files are
considered for ingestion. Files must also pass binary detection and the
max_file_size limit (see ingestion below).

include_patterns (flavor default: language whitelist; Pydantic: [])

When empty, all files with known extensions are included automatically.
Codumentor recognizes ~180 file extensions covering source code, config
files, templates, markup, and documentation. Files with unknown extensions
are still included if they pass a binary-detection heuristic (first 8 KB
checked for null bytes and printable-byte ratio).

default_codumentor.yaml ships a language whitelist (*/.py,
*/.md, …) rather than []. Because that file is the merge floor,
a YAML that omits include_patterns inherits the whitelist. Set
include_patterns: [] explicitly if you want ingest-by-extension.
See Migration: omitted keys now inherit the flavor file.

When non-empty, only files matching at least one pattern are included (whitelist
mode).

exclude_patterns (broad defaults)

# Default exclude_patterns:
exclude_patterns:
  - "**/.git/**"
  - "**/.svn/**"
  - "**/.hg/**"
  - "**/node_modules/**"
  - "**/vendor/**"
  - "**/packages/**"
  - "**/.nuget/**"
  - "**/__pycache__/**"
  - "**/venv/**"
  - "**/.venv/**"
  - "**/*.egg-info/**"
  - "**/.tox/**"
  - "**/.mypy_cache/**"
  - "**/.pytest_cache/**"
  - "**/bin/**"
  - "**/obj/**"
  - "**/dist/**"
  - "**/build/**"
  - "**/out/**"
  - "**/target/**"
  - "**/.idea/**"
  - "**/.vscode/**"
  - "**/.vs/**"
  - "**/.env/**"
  - "**/coverage/**"
  - "**/htmlcov/**"
  - "**/*.min.js"
  - "**/*.min.css"

Pattern syntax:

Important: Setting include_patterns or exclude_patterns in your config
replaces the defaults entirely. To append patterns without losing the
defaults, use the _overrides suffix instead:

# REPLACES defaults — you lose all built-in excludes:
exclude_patterns:
  - "**/my_custom_dir/**"

# APPENDS to defaults — built-in excludes are preserved:
exclude_patterns_overrides:
  - "**/my_custom_dir/**"

The same applies to include_patterns_overrides.

ingestion

Controls document ingestion behavior:

chunking

Controls how documents are split into chunks for vector storage:

models

Model configuration for different AI services:

API keys can be specified in the configuration or through environment variables:

Note: API keys support environment variable substitution using ${VAR_NAME} syntax (e.g., embedding_api_key: "${GOOGLE_API_KEY}"). See Environment Variable Substitution above for details.

Thinking/Reasoning Mode:

Truncated-reply retry policy:

When a reply comes back with finish_reason: length and carries no content and no tool call, the whole reply budget was spent on something unusable (usually reasoning). The agent loop re-rolls that call, bounded at two retries per turn, and then surfaces an error naming agent_max_tokens.

How to tell it took effect. A knob can be accepted with a 200 and discarded before the chat template renders — agent_reasoning_passback above is exactly that on vLLM/SGLang — so "we sent the field" is not evidence. Codumentor logs the retry and its outcome as a pair at WARNING on the codumentor.agent logger:

truncated reply: retrying with {'extra_body': {'chat_template_kwargs': {'enable_thinking': False}}} (mode=less_deliberation), not the identical request
truncation retry probe: mode=less_deliberation attempt=1 overrides={...} -> finish_reason=stop reasoning_chars=0 (attempt that truncated: finish_reason=length reasoning_chars=46012)

If the second line reports the same finish_reason=length and a reasoning size of the same order as the attempt that failed, the ingress took the field and ignored it: the policy is inert and agent_truncation_retry_extra_body is where to state the spelling your ingress does read.

HTTP Timeouts:

Model Profiles (per-subagent model selection):

By default, all subagents use the same model as the main agent. Model profiles let you assign different models to different subagent types — for example, a powerful reasoning model for the main agent and a cheaper, faster model for subagents.

A profile is a named bundle of model configuration: model ID, endpoint, API key, and optional parameters. Profiles are defined under models.profiles and referenced by name from subagent_default (applies to all subagents) or subagent_profiles (per-tool assignments).

models:
  agent: "claude-sonnet-4-20250514"
  agent_base_url: "https://api.anthropic.com/v1/"
  agent_api_key: "${ANTHROPIC_KEY}"

  profiles:
    local-fast:
      model: "qwen3-8b"
      base_url: "http://localhost:8000/v1/"
      api_key: "none"
    cloud-cheap:
      model: "gpt-4o-mini"
      base_url: "https://api.openai.com/v1/"
      api_key: "${OPENAI_KEY}"

  # Default profile for all subagents
  subagent_default: "local-fast"

  # Per-tool assignments (take precedence over subagent_default)
  subagent_profiles:
    explore: "local-fast"
    subagent: "cloud-cheap"
    web_retrieve:
      model: "gpt-4o"
      base_url: "https://api.openai.com/v1/"
      api_key: "${OPENAI_KEY}"

Resolution order: subagent_profiles[tool_name] > subagent_default > global agent settings.

Override values can be a profile name (string) or an inline profile definition (dict). When a profile omits api_key, the global agent API key fallback chain is used.

Available profile fields:

Reasoning capability (reasoning)

Each model exposes reasoning differently (Qwen hybrids a chat-template switch, OpenAI/OpenRouter an effort enum, gpt-oss a system-prompt string). The record declares that once, per profile, so callers can ask for an effort on one ordinal scale — off · low · medium · high · max — and the model layer translates. agent_reasoning is the same record for the global agent model.

reasoning:
  control: chat_template_kwarg   # none | always | chat_template_kwarg | effort_enum |
                                 # budget_tokens | system_prompt | soft_switch
  levels: [off, medium]          # the rungs this model actually distinguishes
  default: medium                # used when a caller asks for no particular rung
  echo: tool_turns               # never | tool_turns | always — reasoning passback policy

Known subagent tool names: subagent, explore, agentic_developer, web_retrieve, web_search, context_summarization.

thinking_modes

Curated thinking modes — one named setting per conversation that decides which model answers, which model each background role uses, and how hard the assistant reasons. A mode is one altitude above models.profiles: it references profiles rather than replacing them. Delivered by the Thinking Modes plugin, which is enabled in the shipped default config.

Omitting the section ships three built-in modes — Fast, Balanced, Deep — that differ in reasoning effort alone and name no model, so nothing about your requests changes until someone picks one.

thinking_modes:
  default: balanced                # mode for a user who has expressed no preference
  modes:
    fast:
      title: Fast
      description: Quick answers, no deliberation.
      icon: bolt
      cost: 1                      # 1-3 relative badge
      main: local-qwen             # a name from models.profiles
      effort: { default: "off", allowed: ["off", "low"] }
    deep:
      title: Deep
      description: Plans before acting, strongest model for code changes.
      cost: 3
      main: cloud-strong
      roles: { context_summarization: local-qwen, explore: local-qwen }
      effort: { default: high, allowed: [medium, high, max] }
      prompt: |
        Before editing, write a short plan and verify it against the code.
FieldDefaultDescription
defaultfirst declared modeMode a user with no preference lands on.
modes.<id>.titlerequiredLabel shown in the picker.
modes.<id>.description""One line under the title.
modes.<id>.iconunsetIcon token for the picker.
modes.<id>.cost2Relative cost/speed badge, 1–3.
modes.<id>.mainunsetProfile the main agent uses, and what unlisted roles inherit. Unset leaves model selection to models:.
modes.<id>.roles{}role → profile name, overriding main for that role.
modes.<id>.effort.defaultunsetRung the mode starts at. Unset = the resolved model's own default.
modes.<id>.effort.allowedunsetRungs a user may select. Unset = every rung the model supports. Always intersected with the model's reasoning capability.
modes.<id>.promptunsetInstruction block injected as a stable prefix note while this mode is active.

speech

Speech-to-text / text-to-speech service used by the read_aloud message action, the Voice Mode overlay, and Telegram voice replies. It is a sibling of models, not part of it, because the audio provider is usually a different endpoint than the chat-model gateway (e.g. a local vLLM box serves chat but not Whisper/TTS). Uses OpenAI-compatible audio endpoints (/v1/audio/transcriptions, /v1/audio/speech).

speech:
  enabled: true
  base_url: "https://api.openai.com/v1"
  api_key: "${OPENAI_API_KEY}"
  tts_model: "tts-1"
  tts_voice: "alloy"
FieldDefaultDescription
enabledfalseMaster switch. The web read_aloud action and Voice Mode overlay light up only when this is true and api_key resolves to a real value.
base_urlhttps://api.openai.com/v1OpenAI-compatible audio endpoint base URL.
api_keynullSupports ${ENV_VAR} and ${secret:<name>}. Use an ${ENV_VAR} for read-aloud, Voice Mode, and the Telegram bot: a per-user ${secret:...} only resolves inside an agent turn, so it cannot drive those out-of-turn web calls.
stt_modelwhisper-1Speech-to-text model.
stt_languagenullnull = let the provider auto-detect the spoken language.
stt_max_audio_bytes26214400 (25 MB)Reject larger uploads before hitting the provider.
tts_modeltts-1Text-to-speech model.
tts_voicealloyTTS voice.
tts_formatopusopus → Ogg/Opus (native Telegram voice note). Also: mp3, aac, flac, wav, pcm.
tts_speed1.0Playback speed multiplier.
tts_max_input_chars4096Guard against synthesising an enormous reply. 0 disables it.
read_timeout120.0HTTP read timeout (seconds).
max_retries2Retry count for transient provider errors.

See Plugins in Production → Speech for the operational checklist.

vector_db

Vector database configuration:

cache

Embedding caching configuration:

agent

Agent behavior configuration:

tools

Tool-specific configuration:

api

API server configuration:

api.mcp_apps

Interactive MCP Apps are enabled
by default only after the deployment supplies a valid sandbox origin. With
no sandbox_origin, or with an unsafe one, Codumentor stays text-only and does
not advertise the Apps extension to MCP servers.

api:
  external_url: "https://codumentor.example.com"
  mcp_apps:
    sandbox_origin: "https://mcp-app-sandbox.example.net"

The sandbox hostname can reverse-proxy to the Codumentor process. The process
serves only /mcp-app-sandbox.html on that virtual host and returns 404 for API
and SPA routes. Do not attach a broad Domain= cookie to a parent domain shared
by the host and sandbox. If content_security_policy is custom, its
frame-src must explicitly include the normalized sandbox origin.

Roll out by configuring the origin on one instance, confirming the log does not
report an MCP Apps availability reason, and watching
GET /ui/admin/mcp-apps-metrics. Useful signals are lifecycle failed versus
initialized, resource failures/cache hits, RPC outcomes and p95 latency, and
security rejections. The endpoint requires admin:*; POST the corresponding
/ui/admin/mcp-apps-metrics/reset endpoint before a compatibility exercise.
If failures rise, set api.mcp_apps.enabled: false; textual tool results remain
available and existing conversation data needs no migration.

api.rate_limit

Budgets are per client IP per minute; 0 drops a rule. Only the paths listed
are limited — there is deliberately no blanket cap, which would throttle the
SSE stream and the UI's own polling. A refused request gets 429 with
Retry-After.

Two caveats worth knowing. A shared egress IP (corporate NAT) shares a budget —
raise the share limits first if that bites, not the login limit, since the
failure backoff is what actually stops guessing. And the counters are
in-process: a multi-worker deployment divides every budget by the worker count.

ui

Web UI customization:

When welcome_title or welcome_description are not set, the built-in translated defaults are used.

Plain string (same text for every language):

ui:
  welcome_title: "Welcome to ACME CodeBot"
  welcome_description: "Ask questions about our internal services and libraries."

Per-locale map (i18n):

ui:
  welcome_title:
    en: "Welcome to ACME CodeBot"
    hu: "Üdvözöl az ACME CodeBot"
  welcome_description:
    en: "Ask questions about our internal services and libraries."
    hu: "Kérdezz a belső szolgáltatásainkról és könyvtárainkról."

You can mix the two styles — for example a plain welcome_title with a locale-mapped welcome_description.

auth

Authentication configuration for API access:

A browser login sets two cookies, both HttpOnly, and both covered by the
settings above: codumentor_session (the access token) and
codumentor_refresh (the refresh token, SameSite=strict and scoped to
Path=/auth/refresh). Neither token is ever returned in a response body to
the browser, so no script — including an injected one — can read or exfiltrate
a session. Programmatic clients are unaffected: they still get tokens in the
body of POST /auth/login and send them back as
Authorization: Bearer <jwt>.

The configured provider is the only one that can serve an interactive
login: POST /auth/login refuses a caller-supplied provider, and
GET /auth/providers lists only that provider. A provider named in config
whose configuration block is missing is a startup error rather than a silent
fallback to unauthenticated access.

Sessions are revocable. Logging out invalidates both tokens server-side,
refreshing invalidates the token it consumed, and deactivating a user (or
taking a role away) invalidates everything they hold — so those are real events
rather than client-side gestures, and a copied token stops working. Revocations
live in data/auth/revoked_tokens.db so a restart cannot resurrect a
logged-out session; entries are purged automatically once the token they name
has expired anyway. No configuration; see
doc/security/session-credentials.md §6.

See the Authentication guide for the full per-provider reference.

Gitea Provider

Local Provider

A fixed set of username/password users defined in config. The simplest way to give a small team real logins without Gitea or LDAP:

auth:
  provider: local
  jwt_secret: "${CODUMENTOR_JWT_SECRET}"   # keep persistent sessions across restarts
  local:
    users:
      - username: admin
        password: "${CODUMENTOR_ADMIN_PASSWORD}"   # plaintext or pbkdf2_sha256$ hash
        email: admin@example.com
        roles: ["admin", "user", "power_user"]
      - username: dev
        password: "${CODUMENTOR_DEV_PASSWORD}"
        roles: ["user"]

Each entry takes username (required, unique), password (required — plaintext or a pbkdf2_sha256$... hash, ${ENV_VAR} substitution supported), and optional email, display_name, and roles (default ["user"]). Generate a hash with python -c "from codumentor.auth.providers.local import hash_password; print(hash_password('s3cret'))". See Authentication → Local for details.

LDAP Provider

For enterprise environments using LDAP or Active Directory:

auth:
  provider: ldap
  ldap:
    # Connection settings (required)
    server_url: "ldaps://ldap.example.com"  # or ldap:// for non-SSL
    bind_dn: "cn=service,dc=example,dc=com"
    bind_password: "${LDAP_BIND_PASSWORD}"
    base_dn: "dc=example,dc=com"

    # Server type: "active_directory", "openldap", or "generic"
    server_type: "openldap"

    # Security: "ldaps" (SSL), "starttls", or "none"
    security_mode: "ldaps"

    # User search settings
    user_search_filter: "(&(objectClass=inetOrgPerson)(uid={username}))"
    user_search_base: "ou=users,dc=example,dc=com"  # defaults to base_dn
    username_attribute: "uid"  # "sAMAccountName" for AD
    email_attribute: "mail"
    display_name_attribute: "displayName"

    # Group settings (for role mapping)
    group_search_base: "ou=groups,dc=example,dc=com"
    group_member_attribute: "memberOf"

    # Role mapping: LDAP groups → Codumentor roles
    group_role_mapping:
      "cn=admins,ou=groups,dc=example,dc=com": ["admin"]
      "cn=developers,ou=groups,dc=example,dc=com": ["developer"]
    default_roles: ["user"]

    # Optional: require membership in a specific group
    require_group: "cn=codumentor-users,ou=groups,dc=example,dc=com"

    # SSL certificate validation
    validate_cert: true
    ca_cert_file: "/path/to/ca-cert.pem"  # optional

Active Directory defaults (when server_type: active_directory):

OpenLDAP defaults (when server_type: openldap):

Multiple LDAP directories. Replace the ldap: block with a list under
ldap_directories: to authenticate against several independent directories.
Each entry takes the full set of LDAP options shown above plus a unique
name. On login the directories are tried in declared order; the first
successful bind wins. See Authentication
for the routing rules, identity behaviour, and audit-log labelling.

auth:
  provider: ldap
  ldap_directories:
    - name: corp
      server_url: "ldaps://corp.example.com"
      # ... full LDAP options ...
    - name: contractors
      server_url: "ldaps://contractors.example.com"
      # ... full LDAP options ...

prompt

Prompt/template system configuration (see Prompt System for the full reference):

tools

Tool-specific configuration:

When disable_vector_search is set to true, the agent will not use the vector database for semantic search, relying instead on other methods like file reading and shell commands to find information.

scheduler

Controls the scheduler — the component that fires recurring and one-off jobs onto the job queue (scheduled agent runs, periodic reports, ingestion refresh, etc.).

The scheduler shares its SQLite file with the job queue (jobs.sqlite3 by default, overridable via CODUMENTOR_JOBS_DB_PATH). WAL is enabled automatically.

scheduler:
  enabled: true
  idle_poll_interval: 30.0       # max seconds between loop ticks when idle
  disable_after_failures: 5      # consecutive infra failures before auto-disable
  max_catchup_age: 86400.0       # 24h — skip fires older than this
  max_schedules_per_user: 100
  default_timezone: "UTC"
FieldTypeDefaultDescription
enabledbooltrueMaster switch. When false, the scheduler loop never starts and schedule_* operations will fail. Existing schedules remain in the DB.
idle_poll_intervalfloat30.0Maximum seconds the loop sleeps between ticks when no fires are imminent. Lowering this costs a bit more DB I/O; raising it delays reaction to newly-created schedules up to this bound.
disable_after_failuresint5After this many consecutive infrastructure failures (e.g. database is locked on enqueue), the schedule is auto-disabled and its status becomes disabled. Handler exceptions do not count — they mark the fired job failed, not the schedule.
max_catchup_agefloat86400.0If the scheduler has been offline and a scheduled fire is older than this many seconds, skip it silently and jump to the next occurrence. Prevents firing a week-long backlog of stale runs after an outage.
max_schedules_per_userint100Cap on active+paused schedules per owner_user_id. Protects against unbounded growth if users can create schedules interactively.
default_timezonestr"UTC"Default IANA timezone for schedules that don't specify one.

Routing. By default, scheduled fires land on the shared agent_runs job queue. Plugins that need per-user isolation can pass a per-user queue name at schedule creation (target_queue=f"agent_runs_{user_id}"). Be aware that each distinct queue name spawns its own processor task — don't multiply them casually.

Disabling at runtime. Set enabled: false and restart. Schedules persist in the DB and resume when re-enabled. To stop a single schedule without touching config, use scheduler.pause(id) or scheduler.cancel(id) from code.

See Scheduler Guide for the plugin-developer view and firing semantics.

webhooks

Push-notification webhook endpoint — lets Gitea/GitHub trigger ingestion automatically. See Webhooks for full setup.

webhooks:
  enabled: true
  secrets:
    - "${CODUMENTOR_WEBHOOK_SECRET}"
    - "${CODUMENTOR_WEBHOOK_SECRET_PREVIOUS}"

logging

Logging configuration:

Built-in log rotation. Codumentor can rotate the log file itself (a TimedRotatingFileHandler), independent of an external logrotate:

logging:
  level: INFO
  file: ./logs/codumentor.log
  rotation_enabled: true
  rotation_when: midnight    # "midnight", "H" (hourly), "D" (daily), "W0".."W6", "S", "M"
  rotation_interval: 1       # rotate every N of the above unit
  rotation_backup_count: 30  # keep this many rotated files
  rotation_utc: true         # compute rollover times in UTC
FieldDefaultDescription
rotation_enabledfalseEnable timed rotation of the log file.
rotation_when"midnight"Rollover unit (Python TimedRotatingFileHandler codes).
rotation_interval1Number of rotation_when units between rollovers.
rotation_backup_count30How many rotated files to retain; older ones are deleted.
rotation_utctrueUse UTC when deciding rollover boundaries.

JSON logging (for ELK/Splunk/Datadog — see also Deployment → JSON Logging):

FieldDefaultDescription
json_loggingfalseEmit structured one-line-JSON logs.
json_log_filenullSeparate file for JSON logs. If unset, JSON is written alongside the text log.
json_include_contexttrueInclude the per-record context dict (session id, etc.).
json_include_stack_infofalseInclude stack info on records that carry it.
json_include_process_infofalseInclude process/thread identifiers.
json_date_formatnullOverride the timestamp format.

The text file handler (and the JSON handler when enabled) is always configured when a log file path is set.