Skip to content

hot-reload path bypasses Config::validate(), skipping all 7 newly-wired invariant checks #6063

Description

@bug-ops

Description

PR #6058 (commit de900e0, closes #5932/#6018) wired 7 previously-dead subsystem validate() functions into Config::validate() (provider pool, STT, trajectory sentinel, gateway, utility scoring, memory fidelity, ACON thresholds). The commit message states the goal was to make these reachable "from the production config-load path."

The primary startup path is fixed: Bootstrap::new (src/bootstrap/mod.rs:202-203) calls Config::load(&config_path)? immediately followed by config.validate()?.

However, the hot-reload path never calls Config::validate() at all. Agent::reload_config (crates/zeph-core/src/agent/config_reload.rs:17) calls self.load_config_with_overlay(&path) (line 21), which itself only calls Config::load(path) (line 163) — no .validate() anywhere in either function. reload_config then applies dozens of fields straight from the unvalidated config onto the live Agent runtime (memory compression/routing thresholds, skill matching weights, context budget, ACON thresholds, etc.), and is invoked from the live config-watcher event handler at crates/zeph-core/src/agent/mod.rs:422.

Net effect: a config file edited while the agent is running (e.g. introducing an empty [[llm.providers]] pool, duplicate provider names, multiple default = true entries, inverted ACON/fidelity/trajectory thresholds) is silently applied at runtime with none of the 7 invariant checks the #6058 fix just wired in — the exact same "unreachable validate()" defect class the fix was meant to close, just at a second call site that PR missed.

Two smaller, lower-priority instances of the same gap exist in one-off CLI commands that load Config but never validate it: src/commands/worktree.rs:22 and src/commands/gonka.rs:385. These are lower severity since they only read subsystem fields (config.worktree.*) untouched by the 7 newly-wired validators, and gonka.rs's doctor-style diagnostics arguably wants granular per-check reporting rather than a hard validate() bail — worth a look but not blocking.

Reproduction Steps

  1. Start the agent with a valid config (e.g. .local/config/testing.toml) and an active config-watcher (config_path set in runtime.lifecycle).
  2. While the agent is running, edit the config file on disk to set memory.fidelity.full_threshold above compressed_threshold (or empty [[llm.providers]], or duplicate provider names).
  3. Trigger a reload (file-watch event fires Agent::reload_config).
  4. Observe: the invalid config is applied to the live runtime with no validation error logged or surfaced.

Expected Behavior

reload_config/load_config_with_overlay should call Config::validate() on the freshly-loaded config before applying any field to the running agent, and reject the reload (keeping prior runtime state, matching the existing "keep previous state on error" pattern already used for overlay-merge failures) when validation fails.

Actual Behavior

No validation runs on the hot-reload path; invalid configs are applied directly to the live agent.

Environment

Logs / Evidence

  • crates/zeph-core/src/agent/config_reload.rs:17-23 (reload_config) and :159-197 (load_config_with_overlay) — no .validate() call in either.
  • crates/zeph-core/src/agent/mod.rs:422self.reload_config(); invoked from the config-watcher event handler.
  • Contrast with src/bootstrap/mod.rs:202-203 and src/tui_remote.rs:49-51, which both correctly call .validate() immediately after Config::load.

Metadata

Metadata

Assignees

Labels

P1High ROI, low complexity — do next sprintbugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions