The operator config.toml and the four-layer cascade¶
ADR-062 — one operator file, four layers (0.6, pre-release). The file is the BASE layer: it is where driver-domain settings live, and every retired item is refused at boot, never translated.
The file is the base layer¶
The operator file $XDG_CONFIG_HOME/degenbot/config.toml (else ~/.config/degenbot/config.toml; or its DEGENBOT_CONFIG override) is the typed Rust file layer of degenbot-config’s BotConfigLoader, and it is the lowest-precedence layer of the cascade the driver-domain resolvers read. Every top-level table must name a declared schema section, and every key must be a declared key — the loader fails closed, aggregating every problem before reporting.
Precedence |
Layer |
Supplies |
|---|---|---|
1 |
explicit CLI |
|
2 |
environment |
|
3 (base) |
|
the |
4 |
declared default |
the schema default ( |
[nodes] — the per-chain http, ws, and ipc tables — is the base layer, and the DEGENBOT_RPC_* env family and --node override it: an env entry for one chain outranks that chain’s file entry alone, and an explicit --node outranks both. The same shape holds for the session chain id ([session] chain_id below DEGENBOT_DEFAULT_CHAIN_ID below --chain-id) and the database path ([database] path below DEGENBOT_DB_PATH below --database).
The authoritative key reference is generated from the schema — regenerate with REGEN_CONFIG_DOCS=1 cargo test -p degenbot-config (lands in rust-config-keys.md). Every assignment is recorded with its winning layer, so nothing resolves anonymously.
The base layer, spelled out¶
[nodes]
http = { 1 = "http://localhost:8545" }
ws = { 1 = "ws://localhost:8546" }
# The nested table form is equivalent; `ipc` is a socket path or an ipc:// URL.
[nodes.ipc]
1 = "/tmp/anvil.ipc"
[session]
chain_id = 1
[database]
path = "./degenbot.db"
A machine-local or bind-mounted file holds these values; the environment is where a single value diverges for one run.
Replacement table for the pre-0.6 file vocabulary¶
The pre-0.6 file vocabulary was Python-driver domain the typed schema never carried. The driver-domain items it covered are now DECLARED file keys; the two layout items nothing replaced stay retired. There is no shim: the old spellings are refused, not translated.
Pre-0.6 file spelling |
Replacement |
|---|---|
|
the declared |
|
the declared |
— (no pre-0.6 spelling) |
the new |
|
the declared |
|
retired — the modern |
top-level |
retired — |
Boot behavior: a surviving pre-0.6 spelling fails the load and the process exits 2 with a message that names the section and, for an item that had a replacement, the key or section to write instead —
bot configuration invalid (1 problem(s)):
- --config /home/you/.config/degenbot/config.toml: unknown section [rpc]
for a table the schema never declared, and
bot configuration invalid (1 problem(s)):
- --config /home/you/.config/degenbot/config.toml: retired config-layout item [otel] is no longer supported — the [otel] table is retired: use the modern telemetry section (telemetry.otel, telemetry.jaeger_endpoint); see docs/config-migration.md
for the two names that DID have a replacement ([otel] and the top-level default_chain_id). [rpc] and [ws] are neither declared nor retired — ADR-062 D13 declined a shim — so they get the generic unknown-section shape with no replacement pointer. A stale [database] filepath is refused the same way as any other undeclared key: unknown key filepath in section [database].
In-schema key retirements (typed migrations)¶
Later hard cutovers retired SCHEMA keys the same way: a surviving env var or TOML key fails the load loudly with a pointed message, for one release.
Retired key |
Cutover |
Replacement |
|---|---|---|
|
P6YXA6 |
the worker fleet (per-bin hosting needs no executor selection) |
|
P6YXA6 |
bins are always LPT-pre-balanced |
|
LW-T9 (ergo CQLMM2) |
fleet is the only stance since LW-T9 — the worker fleet is the only behavior |
|
LW-T9 (ergo CQLMM2) |
SimDriver capacity: |
What is NOT retired¶
[failure_policy] is deliberately not typed and not rejected: it is the ADR-040 D3 free-form per-bucket override table, read as a raw TOML table from the same file the loader selected (BotConfigLoader::file_path()). Files may keep it unchanged.
[deployments] is deliberately not typed and not rejected either: it
is the ADR-062 D7 deployment-registry overlay table, read as a raw TOML table
by src/degenbot/registry/deployment_loader.py from the same file the loader
selected. It joins [failure_policy] in the loader’s sanctioned free-form list
(FREE_FORM_FILE_SECTIONS) for the same reason — the overlay is driver-domain
data the typed schema does not carry, so the typed file layer skips it and the
raw-table reader owns it. Files may keep it unchanged.
Example migration¶
Before (pre-0.6):
default_chain_id = 1
[rpc]
1 = "http://localhost:8545"
[database]
filepath = "./degenbot.db"
[otel]
endpoint = "http://localhost:4318"
After (0.6):
# The base layer now carries the endpoints, the session chain, and the
# database file — nothing has to be exported for them to take effect.
[nodes.http]
1 = "http://localhost:8545"
[session]
chain_id = 1
[database]
path = "./degenbot.db"
[telemetry] # the [otel] table, renamed
otel = true
jaeger_endpoint = "http://localhost:4318"
[failure_policy] # unchanged, still free-form
The Python driver resolves through the same cascade. Bot(chain_id=1, node="http://localhost:8545") still works — each keyword is the explicit override layer — and with no keywords at all Bot reads the same [nodes.*], [session], and [database] tables the console reads, from the same file, with the same DEGENBOT_RPC_* and DEGENBOT_DEFAULT_CHAIN_ID overrides above it. There is no second config authority and no asymmetry: the console, a pure-Rust consumer, and a Python-launched bot that boot from the same file and environment resolve the same endpoints, and degenbot config show --resolved names the winning layer for each.
CAUTION (2026-09-10 incident): the bind-mounted host config.toml is the base
layer inside the degenbot devcontainer, and devcontainer.json containerEnv
legitimately overrides its chain-1 [nodes] entries with the container-correct
URIs — http://host.containers.internal:8545 and
ws://host.containers.internal:8546. Do not re-export DEGENBOT_RPC_* from a
shell rc file (.bashrc etc.) or write localhost:8545 into the container’s
environment: an rc-file export is applied after containerEnv and silently
wins, pointing the bot at the container’s own loopback, where nothing listens
(connection refused at the first eth_chainId call). Override endpoints
in-container via the CLI (--node http://host.containers.internal:8545), which
outranks the environment, or edit devcontainer.json and rebuild.
Credentials in the operator file¶
config.toml is a machine-local file, so treat it as a secret carrier: chmod 600 it, keep keys out of it where a per-machine environment variable does the
job, and do not commit one. Interpolating an environment variable INTO the file
is a recorded follow-up (ergo MXFVVI’s sibling decision, ADR-063) and is not
implemented — the file’s values are read literally.
Debugging a value that did not take effect¶
degenbot config show --resolved prints the full inventory the process resolves,
each key annotated with the layer that won it (cli > env > file > default),
with (unresolved) where no layer supplied it — a per-chain entry reads
nodes.ws[8453] = … (env) when an export shadowed the file entry, which is the
first place to look when a value seems to be ignored. degenbot config show is
the file layer alone, and degenbot config path prints which file the cascade
reads. The config show, config path, and config get arms are read-only;
config set / config unset mutate the file (confirming unless --force),
write through the validate-before-write path, and config set warns when the
environment will shadow the write (Shadowed { env: DEGENBOT_RPC_HTTP_CHAINID_<id> }).
Database upgrades¶
A stale Alembic-marked database now heals automatically at open (ADR-052):
ensure_schema runs the ADR-011 out-of-place heal on any alembic_version
database — head-stamped or stale — preserves the old file as *.bak, and
proceeds Rust-owned. There is no degenbot database upgrade command: it renders
a pointed retirement error and exits non-zero, while degenbot database heal
remains the explicit repair entry point. Set DEGENBOT_DB_AUTO_HEAL=0 to
disable heal-at-open for pinned environments.