Odin reads configuration from config.yml at startup. Environment variable substitution is supported via ${VAR} and ${VAR:-default} syntax.
discord:
token: ${DISCORD_TOKEN} # Required — bot token
allowed_users: [] # User IDs (empty = all allowed)
channels: [] # Channel IDs (empty = all)
respond_to_bots: false # Reply to other bots
require_mention: true # Only respond when @mentioned
ignore_bot_ids: [] # Bot IDs to never respond totools:
enabled: true
ssh_key_path: /home/service-user/.ssh/id_ed25519
ssh_known_hosts_path: /home/service-user/.ssh/known_hosts
command_timeout_seconds: 300 # Default per-handler-attempt timeout
tool_timeouts: # Per-tool handler-attempt overrides
hosts:
localhost:
address: 127.0.0.1
ssh_user: root
os: linux
my-server:
address: 203.0.113.10
ssh_user: deploy
os: linux
max_tool_iterations_chat: 30 # Model/tool-batch iterations in one Discord message
max_tool_iterations_loop: 100 # Model/tool-batch iterations per autonomous loop cycleThese caps count model/tool-batch iterations, not individual tool calls: one
iteration can dispatch a batch containing multiple tool calls. The loop cap is
per autonomous loop cycle, not the loop's lifetime; start_loop.max_iterations
is the separate total-cycle limit.
tools:
ssh_retry:
max_retries: 2
base_delay: 0.5
max_delay: 10.0
ssh_pool:
enabled: true
control_persist: 60 # Seconds to keep connections alive
socket_dir: /tmp/odin_ssh_sockets
bulkhead:
ssh_max_concurrent: 10
subprocess_max_concurrent: 20
browser_max_concurrent: 3tools:
disabled_tools: [] # Built-in tool names hidden from the modelOperator-disabled built-ins are removed from the model catalog on every
surface (chat, agents, loops, schedules) and rejected at dispatch with a
typed tool_disabled result. Manage them from the WebUI Tools page — the
switches apply live, no restart. Names are case-sensitive; unknown entries
are preserved and ignored so a list survives catalog drift. Disabled names
stay reserved: a skill or MCP tool can never shadow a disabled built-in.
This leaf is read-only on the generic config route; the Tools management
API (GET /api/tools/builtins, POST /api/tools/builtins/{name}/enabled)
owns it.
openai_codex:
enabled: true
model: gpt-5.6-sol # ChatGPT subscription path; GPT-6 models also selectable where entitled
reasoning_effort: xhigh # none | low | medium | high | xhigh | max
agent_reasoning_effort: auto # spawned agents; "auto" = per-spawn choice, null = inherit
credentials_path: ./data/codex_auth.json
request_timeout_seconds: 3600 # whole-request backstop; long reasoning turns stream past 10 min
stream_stall_timeout_seconds: 180 # fail fast when no stream bytes arrive for this long
retry:
max_retries: 3
base_delay: 1.0
max_delay: 30.0
context_compression:
enabled: true
max_context_chars: null # null = auto (model-derived ceiling); a number only lowers it
keep_recent_iterations: 3
# Per-model usable-input-budget overrides (tokens, 50192-2000000). Empty =
# built-in known-safe floors. Consumed by the context-budget resolver.
context_budget_overrides: {}
# Working-set policy: percent of the effective budget compaction targets
# (30-100). Never reduces budgets at or below 272K tokens.
context_utilization: 60
auxiliary: # cheaper model for background jobs
enabled: true
model: gpt-5.6-terraAgent model selection is provider-neutral and lives under agents, rather
than under the Codex provider. A bare model name means Codex; use
compat:<model> or ollama:<model> to select an OpenAI-compatible endpoint
or Ollama model. agents.model: null inherits the chat model and auto
selects per spawn. agents.auto_model_allowlist contains concrete model
references that Auto may select. The WebUI keeps unknown entries and the
codex-auto-review alias visible, so a catalogue refresh cannot erase policy
that the running configuration already accepts.
agents:
model: auto
# Discrete compatible-provider switch, not a Codex reasoning effort.
thinking_mode: null # null | adaptive | enabled | disabled
auto_model_allowlist:
- gpt-5.6-luna
- compat:deepseek-v4-flash
- ollama:qwen3:32bA persisted max_context_chars: 750000 from the pre-campaign default is
migrated to auto once (a provenance marker under data/ records it, and one
warning names the marker); saving the compression settings afterwards makes
any explicit value — including 750000 — stick permanently.
The native Codex selectors and default agent catalogue list gpt-6-astra,
gpt-6.1-sol, gpt-6-sol, gpt-6-luna, gpt-5.6-sol, gpt-5.6-terra, and gpt-5.6-luna
in that order. Fresh-install main defaults to gpt-6.1-sol; auxiliary and
agent defaults are unchanged. Upgrade-compatibility defaults retain existing
installs' model selections.
Reasoning effort max is served by both families. GPT-6 Sol and Luna accept
all six efforts (none, low, medium, high, xhigh, max) and each has
a measured input-budget floor of 921,799 tokens (2026-09-22).
GPT-6 Astra rejects none. GPT-6.1 Sol accepts low, medium, high,
xhigh, and max, rejects none, and has a measured input-budget floor of
921,849 tokens (2026-09-29).
The retired gpt-5.5 is no longer selectable. On configuration-file load,
explicit main, fixed-agent, and auxiliary selections migrate in memory to
gpt-6-sol, with a warning. Existing effort selections are preserved.
Its context-budget overrides are discarded rather than transferred to a
different model; any existing Terra override remains unchanged. YAML and
environment placeholders are not rewritten. Live configuration updates and
explicit per-call requests for the retired model are rejected, not silently
rerouted. Historical usage/provenance records are unchanged. An old image
outer_model selection (including a pin) uses the established image successor
gpt-6-astra on load instead.
Model entitlement is per ChatGPT account: gpt-6-astra rolled out to Personal/Pro accounts before Team accounts (2026-09-04); an account that does not serve a model answers "not supported when using Codex with a ChatGPT account". Odin refuses a known-incompatible model/effort pair everywhere it can be introduced — config load, the admin API, per-spawn agent overrides, and final request construction — naming the pair and the efforts that model does accept. Unknown model strings pass through unchecked (the server stays the authority).
The auxiliary model is an optional cheaper Codex model that runs the fixed background jobs — compaction, reflection, consolidation, and background follow-up — with automatic fallback to the primary model on error. It shares the main Codex OAuth credentials; only the model differs. Set it live from the Web UI (Auxiliary Model dropdown, "Off" to run those jobs on the primary).
Generate credentials: python3 scripts/codex_login.py
Codex access tokens are refreshed automatically when needed, with refresh serialized per account and refreshed credentials persisted. Re-run the login flow only when refresh or authorization fails and Odin reports that the account needs reauthorization; do not schedule periodic credential replacement. No fixed refresh-token lifetime is promised here.
agents:
max_nesting_depth: 2 # Sub-agent nesting levels (root = 0)
max_children_per_agent: 3
max_iterations: 120 # LLM turns per interactive spawn
scheduled_max_iterations: 180 # Scheduled workflow spawns
hard_max_iterations: 300 # Ceiling for per-spawn overrides
final_warning_iterations: [20, 10, 5, 1]
iteration_timeout_seconds: 900 # Per-LLM-call backstop (60-86400)
max_lifetime_seconds: 14400 # Hard per-agent deadline (60-86400)OpenAI-compatible presets ship verified full endpoint URLs for DeepSeek, Z.ai
GLM, Moonshot, Groq, Together, Fireworks, Mistral, xAI, Cerebras, DashScope,
and OpenRouter. URLs are used verbatim: Odin never appends /v1. Local examples
are vLLM http://127.0.0.1:8000/v1, llama.cpp :8080/v1, and LM Studio
:1234/v1. Profiles declare reasoning dialect plus reasoning-content feedback;
the safe default is not to echo provider reasoning into history.
A compatible model's usable prompt budget is its total window minus the output
each request asks for (the profile's max output, capped at 32,768 tokens);
openai_compatible.context_utilization sets how much of that history may use.
iteration_timeout_seconds bounds each agent LLM call. It is a backstop
against a hung call, not a working limit — set it well above a legitimate
high-effort generation (5–10+ minutes at high reasoning effort); the
streaming transport already fails dead connections fast via
stream_stall_timeout_seconds.
max_lifetime_seconds is a hard deadline enforced during LLM and tool
waits, not just between iterations. Both values are snapshotted at
spawn — changing them live affects newly spawned agents only, never the
deadline of an agent already running.
sessions:
max_history: 50 # Messages per conversation
max_age_hours: 24 # Auto-expire sessions
persist_directory: ./data/sessions
token_budget: 128000 # Auto-compact when exceeded
adaptive_compaction: truelearning:
# enabled: false # Default when omitted; opt in explicitlyAutomatic learning is off by default. An existing explicit
learning.enabled: true stays enabled until an administrator switches it off;
upgrading does not override that choice. In the WebUI, open Capabilities →
Learned and use Automatic learning. This persisted setting applies live,
without a restart, including to already-created reflection and prompt services.
When off, Odin neither creates automatic lessons nor adds stored learned entries
to model context. Existing entries are retained. Operation, session, compaction
and loop reflection are all gated; an in-flight reflection cannot publish while
disabled. The loop-reflection setting remains a subordinate switch, not an
override of learning.enabled.
The Learned panel and /api/learned inspection, editing and deletion remain
available while learning is off. Disabling is not a deletion or migration of the
learned store. Re-enabling makes retained lessons eligible for context again.
This switch controls only automatic reflections and learned context.
Deliberate persistent memory (memory_manage and memory.json), its system-prompt
section, and ordinary conversation compaction are unaffected.
browser:
enabled: true
cdp_url: "" # Empty = native Playwright launch
default_timeout_ms: 30000
viewport_width: 1920
viewport_height: 1080Leave cdp_url empty to launch a local headless Chromium. Set to ws://host:port?token=secret for remote Browserless.
Each browser tool call runs in a new, empty browser context that is closed when the call ends; cookies, storage, form input and page state never carry over between calls.
Run playwright install chromium after installation.
image:
openai:
enabled: true # kill switch for the native wire implementation
outer_model: gpt-6-astra # Responses model hosting the image tool (pinned)
image_model: gpt-image-2.5-flareThe generate_image tool uses the native OpenAI image_generation tool on the
Codex ChatGPT OAuth backend, riding the same account Odin uses for chat (no
separate auth; subscription-quota-backed, so it draws on that account's usage
limit). It is available only while the active provider is codex and native
image generation is enabled. The tool accepts only a prompt; output dimensions
and aspect ratio are selected by the provider.
outer_model is pinned here
rather than following your chat model, so changing the chat model (Sol/Terra/…)
never alters image generation. The native backend and route are recorded in the
audit log, queryable via search_audit, not shown in the tool's reply.
Set a strong, private web.api_token before starting or exposing the service.
If it is empty and both web.api_tokens and the managed-token store have no
entries, the general API authentication gate is disabled: routes relying on it
are unauthenticated. A blank legacy token alone does not disable authentication
when other token entries exist. Computer observation/evidence routes additionally
require an authenticated admin identity; those checks do not protect the rest of
a tokenless installation. Restrict web.host to loopback unless deliberately
exposing it behind TLS and access controls.
web:
enabled: true
port: 3000
api_token: ${WEB_API_TOKEN} # Required for production
session_timeout_minutes: 0 # 0 = persist until logoutThe management console groups workspace, operations, history, capabilities, and system surfaces. MCP server management lives under Capabilities; credentials remain write-only.
permissions:
default_tier: user # admin, user, or guest
tiers:
"123456789012345678": admin # Per-user overridesRuntime overrides persist in data/permissions.json and take precedence.
| Tier | Access |
|---|---|
| admin | All enabled built-in tools |
| user | Eleven tools: get_tool_output, search_history, search_knowledge, web_search, fetch_url, list_schedules, list_tasks, list_skills, list_knowledge, manage_list, parse_time (no shell; manage_list can change list state) |
| guest | Conversation only, no tools |
For scheduled HTTP actions and inbound webhook delivery, see Schedules & webhooks. This includes retry and partial-delivery behaviour; outbound notifications use a separate path.
webhook:
enabled: false
secret: '' # HMAC-SHA256 verification
channel_id: '' # Default notification channelPlace .md files in data/context/ — they are injected into every LLM prompt as infrastructure context.
context:
directory: ./data/contextlogging:
level: INFO # DEBUG, INFO, WARNING, ERROR
directory: ./data/logs # reserved path kept out of the command workspace; Odin writes no log files hereOdin writes application logs to standard output/error: for the service use
journalctl -u odin -f, for Docker use docker logs odin-bot, and for a source
run use its terminal. logging.level sets verbosity. Tool executions and events
go to the audit log (data/audit.jsonl), which the WebUI Audit and Logs pages read.
| Purpose | Path |
|---|---|
| Config | /etc/odin/config.yml |
| Secrets | /etc/odin/.env |
| Data | /var/lib/odin/ |
| Logs | systemd journal (sudo journalctl -u odin); /var/log/odin is created but not written |
| Application | /opt/odin/ |
| Systemd | /usr/lib/systemd/system/odin.service |
The WebUI self-updater (Updates page) requires a git-clone install —
.deb installs have no repository and should upgrade via apt instead
(the endpoint answers 409 with the same hint).
After a successful update, Odin restarts in place by re-executing itself
once graceful shutdown completes. The first-boot setup wizard does not schedule
or perform that re-exec: it saves settings and reports when an operator restart
is required. Startup-owned settings may remain unapplied until that restart.
Update recovery therefore does not depend on the service unit's Restart=
policy, Docker restart policy, or any supervisor at all.
Restart=always (what the packaged unit ships) is still recommended so the
service also recovers from crashes and reboots.
Model-capacity errors (e.g. server_is_overloaded, which arrives inside an
HTTP 200 as an SSE error event) are retried with a deadline-based policy
shared by chat, agents, and autonomous loops, coordinated by a per-model
circuit breaker. Quota handling is unchanged: HTTP 429 still rotates
accounts inside the provider client; capacity never does.
llm_recovery:
generation_deadline_seconds: 300 # retry budget per LLM generation (waiting, not the attempt)
backoff_cap_seconds: 45 # full-jitter backoff ceiling between attempts
breaker_generation_threshold: 1 # failed generations before the model breaker opens
breaker_cooldown_base_seconds: 30 # first cooldown; doubles per failed probe
breaker_cooldown_cap_seconds: 300 # cooldown ceilingAll keys are optional (schema defaults shown); the section does not need to
exist in config.yml.
Discord chat turns are checkpointed to a durable store so a capacity outage
suspends the turn with its work preserved instead of discarding it. A
suspended turn auto-resumes when capacity returns (if nothing else has
happened in the channel), or the original requester can reply resume
within the resumable window. Interrupted tool executions are recorded as
outcome-unknown and are never re-run automatically.
turn_state:
enabled: true
db_path: "./data/turn_state/turns.sqlite3"
auto_resume: true
resume_ttl_hours: 24 # resumable window from last real progress
payload_retention_days: 7 # diagnostic payloads, then compacted to tombstones
ledger_retention_days: 90 # side-effect ledger (outcome-unknown rows never expire)All keys are optional; disabling turn_state.enabled restores the previous
behavior (capacity exhaustion ends the turn with an error).
MCP servers are managed from Manage → Capabilities → MCP Servers in the
WebUI. That page is the sole editor: Configuration Center deliberately shows
only a read-only MCP summary and links to it. Changes are persisted to
config.yml, applied live, and report saved configuration separately from the
current connection state. A server that cannot connect remains saved so its
sanitized error can be inspected and the connection retried.
mcp:
enabled: true
servers:
local_tools:
enabled: true
transport: stdio
command: /usr/local/bin/my-mcp-server
args: ["--flag"]
cwd: /srv/my-mcp-server
env:
API_KEY: "${MCP_API_KEY}"
tool_allowlist: [] # empty = all validated discovered tools
timeout_seconds: 120
remote_tools:
enabled: true
transport: http
url: https://mcp.example.com/mcp
headers:
Authorization: "Bearer ${MCP_HTTP_TOKEN}"
tool_allowlist: [search, lookup]
timeout_seconds: 120The global switch and each server's enabled switch must both be on. A tool is
published only while its current configuration generation is connected and a
complete discovery result has passed schema, provider, and limit validation.
Disabled, stale, disconnected, errored, removed, or blocked servers publish no
tools. Servers that exceed a safety limit are blocked, not partially
published; use tool_allowlist to narrow discovery when appropriate.
Odin supports MCP tools over stdio and Streamable HTTP in both the deployed
sessionful 2025 generation and the stateless 2026-07-28 generation. The
supported revisions are 2024-11-05 (stdio only), 2025-03-26, 2025-06-18,
2025-11-25, and 2026-07-28. The deprecated HTTP+SSE transport is not
supported.
Authentication is static in v1: HTTP headers and stdio environment variables.
Secret values are write-only in the API and WebUI. Reads return key names only;
rotations and removals use explicit operations so a masked or blank display
value can never overwrite a credential accidentally. Prefer ${ENV_VAR}
placeholders in YAML. Interactive OAuth, MCP resources/prompts, sampling,
elicitation, roots, and tasks are not supported in v1.
The server panel exposes two different recovery actions:
- Refresh tools re-runs discovery on the existing connection.
- Reconnect retires and rebuilds the transport before discovery.
Server-reported instructions, errors, and stderr are untrusted diagnostic text; the management API scrubs configured credential values and bounds all exposed fields before the WebUI renders them.