docs/book/src/architecture/config-lifecycle.md
Configuration is both an operator interface and a runtime contract. Treat it as state with a clear owner, not as loose settings copied into whichever subsystem needs them.
The canonical source is zeroclaw_config::schema::Config, loaded from
config.toml. User-facing config surfaces, the generated config reference, the
gateway config editor, env-var overrides, zeroclaw config set,
zeroclaw config patch, Quickstart, and RPC config methods all route through
that same typed schema.
| Surface | Owner | Persistence boundary | Runtime apply boundary |
|---|---|---|---|
| Config schema | crates/zeroclaw-config/src/schema.rs plus Configurable derives | Code, not generated docs | New binary build |
| Generated reference | cargo mdbook refs / markdown-schema | docs/book/src/reference/config.md at build time | Documentation only |
| Bootstrap location | ZEROCLAW_CONFIG_DIR, ZEROCLAW_DATA_DIR, deprecated ZEROCLAW_WORKSPACE | Environment only | Before Config exists |
| Schema-mirror overrides | ZEROCLAW_<lowercase_path> with __ for dots | In-memory only | Each Config::load_or_init() |
| CLI config writes | zeroclaw config set, config patch, aliases, model helpers | save_dirty() to config.toml | Next load/reload unless the current command uses the new in-memory value |
| RPC and TUI config writes | config/* RPC methods used by zerocode | save_dirty() to config.toml | RPC context updates immediately; daemon-owned subsystems need reload |
| Quickstart apply | Shared web, CLI, and zerocode apply path | save_dirty() to config.toml | Web and RPC can signal daemon reload; standalone CLI applies on next load/reload |
| Gateway config writes | Config API handlers and persist_and_swap() | save_dirty() to config.toml | Gateway-visible state updates immediately; daemon subsystems apply after reload |
| Daemon reload | /admin/reload, RPC config/reload, or the in-process reload channel | Re-reads config.toml | Recreates daemon subsystems in the same PID |
Do not hand-edit the generated config reference. If a field, enum, alias section, secret marker, or description is wrong there, fix the schema or the generator and regenerate the reference.
Config load has a few distinct phases:
Config exists, so bootstrap names keep their uppercase form and do not use
the schema-mirror grammar.config.toml, run schema migration in memory, decrypt configured
secrets, and record any malformed security-critical sections as degraded
security.__ maps to ., so ZEROCLAW_providers__models__openai__api_key
targets providers.models.openai.api_key.On a fresh install, defaults are saved before env overrides are applied. This keeps env-injected secrets and local CI values out of the new file.
Schema-mirror env vars are runtime injections. They land on the in-memory
Config at load time and are tracked in env_overridden_paths so the CLI,
dashboard, and quickstart can show the override marker.
Saving must mask these paths back to their pre-override disk or default value before encryption. This matters most for secrets: if an operator has an encrypted on-disk API key and temporarily boots with an env override for the same path, an unrelated config save must not replace the real credential with the env value or with a masked display string.
Review config changes with this invariant in mind:
ZEROCLAW_* schema-mirror values affect the running process after load.Most editing surfaces use Config::mark_dirty() plus save_dirty(), not a
full rewrite. save_dirty() writes only changed dotted paths, preserves
non-dirty entries and comments where possible, stamps the current
schema_version, and writes through an atomic temp-file replacement.
That path is also responsible for map-key sections. Creating an alias such as a model provider, MCP server, skill bundle, or knowledge bundle must dirty the right section so the alias survives a save and reload. A config edit that only updates the in-memory dashboard state is not complete.
When reviewing a config write, check that:
schema_version remains current after incremental writes;save_dirty() followed by reload.A successful save means the file changed. It does not always mean every runtime component has adopted the change.
The daemon owns the long-lived subsystem graph: gateway, channel listeners,
scheduler, MQTT listener, session wiring, memory backend, provider factories,
and cost wiring. POST /admin/reload signals the daemon loop, which re-reads
config.toml and re-instantiates those subsystems in the same process. The PID
stays the same, but listeners briefly rebind.
Gateway config writes call persist_and_swap(): save to disk, then replace the
gateway-visible in-memory config and set pending_reload. This makes the config
editor reflect the write immediately, while the reload banner tells the operator
that channels, providers, scheduler, or other daemon-owned components may still
be running from the previous subsystem instance.
Standalone zeroclaw gateway start has no daemon supervisor. Its reload
endpoint returns a restart-required response because there is no outer daemon
loop to signal.
Local reload is allowed from loopback. Remote reload requires both:
gateway.allow_remote_admin = trueOpting into remote admin while pairing is disabled is rejected rather than treated as anonymous remote reload access.
Security-critical malformed config sections are allowed to degrade only when the operator explicitly opts into degraded serving. Otherwise the process refuses to serve because reset-to-default security posture may be weaker than the file intended.
Config writes use an atomic temp-file replacement and owner-only permissions.
When replacing an existing file, the writer creates a same-directory
config.toml.bak during the replace and removes it after a successful write.
Gateway writes also snapshot the pre-write file and best-effort restore it if
persistence fails before swapping in-memory state.
There is no general transactional rollback for a valid but undesired config
change after it has been saved and applied. Restore the previous config.toml
from backup, edit the field back through the CLI or dashboard, then reload or
restart according to the runtime boundary above.
A field can be schema-visible before every runtime path consumes it. That is acceptable only when the docs and review notes say so plainly.
For example, knowledge_bundles is schema-visible and appears in config
section APIs. A PR that adds or changes such a surface must be precise about
whether it only stores configuration, wires the runtime behavior, or completes
both.
When reviewing a PR that touches a schema-visible but not yet runtime-consumed field, require the PR description to say whether runtime wiring is deferred, out of scope, or completed by the same change.
For config-schema, env-var, default, or reload changes, ask:
Config at use time?crates/zeroclaw-config/src/schema.rscrates/zeroclaw-config/src/env_overrides.rssrc/main.rscrates/zeroclaw-runtime/src/rpc/dispatch.rscrates/zeroclaw-runtime/src/quickstart/mod.rscrates/zeroclaw-gateway/src/api_quickstart.rscrates/zeroclaw-gateway/src/api_config.rscrates/zeroclaw-gateway/src/lib.rscrates/zeroclaw-gateway/src/api.rsxtask/src/cmd/mdbook/refs.rs