# 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 | `--node `, `--chain-id`, `--database` | | 2 | environment | `DEGENBOT_RPC_{HTTP,WS,IPC}_CHAINID_`, `DEGENBOT_DEFAULT_CHAIN_ID`, `DEGENBOT_DB_PATH` | | 3 (base) | `--config` file | the `[nodes.*]` endpoint tables, `session.chain_id`, `database.path`, and every other typed section | | 4 | declared default | the schema default (`database.path` → the XDG state home) | `[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`](rust-config-keys.md)). Every assignment is recorded with its winning layer, so nothing resolves anonymously. ## The base layer, spelled out ```toml [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 | |---|---| | `[rpc]` per-chain endpoints (`[rpc]\n1 = "http://…"`) | the declared `[nodes.http]` table (env `DEGENBOT_RPC_HTTP_CHAINID_`, or `--node `, still override it per chain) | | `[ws]` per-chain endpoints | the declared `[nodes.ws]` table (env `DEGENBOT_RPC_WS_CHAINID_`, or `--node `) | | — (no pre-0.6 spelling) | the new `[nodes.ipc]` table (env `DEGENBOT_RPC_IPC_CHAINID_`, or `--node ipc://…`) | | `[database]` `filepath` | the declared `[database] path` (env `DEGENBOT_DB_PATH`, or `--database`, still override it) | | `[otel]` `endpoint` / `enabled` | **retired** — the modern `telemetry` section: `telemetry.otel` (toggle) and `telemetry.jaeger_endpoint` (OTLP endpoint) | | top-level `default_chain_id` | **retired** — `[session] chain_id` in the file, or the `DEGENBOT_DEFAULT_CHAIN_ID` env name / `--chain-id` | 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 | |---|---|---| | `solve.executor` / `DEGENBOT_SOLVE_EXECUTOR` | P6YXA6 | the worker fleet (per-bin hosting needs no executor selection) | | `solve.lpt_partition` / `DEGENBOT_LPT_PARTITION` | P6YXA6 | bins are always LPT-pre-balanced | | `fleet.stance` / `DEGENBOT_FLEET` | LW-T9 (ergo CQLMM2) | fleet is the only stance since LW-T9 — the worker fleet is the only behavior | | `solve.solve_sim_inflight` / `DEGENBOT_SOLVE_SIM_INFLIGHT` | LW-T9 (ergo CQLMM2) | SimDriver capacity: `fleet.sim_slot_cap`; inline-sim sizing: `solve.inline_sim_workers` | ## 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): ```toml default_chain_id = 1 [rpc] 1 = "http://localhost:8545" [database] filepath = "./degenbot.db" [otel] endpoint = "http://localhost:4318" ``` After (0.6): ```toml # 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_ }`). ## 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.