Skip to content

feat: Configuration system enhancements (hot-reload, JSON5/YAML support) #86

@ilblackdragon

Description

@ilblackdragon

Feature Parity: Configuration System Enhancements

Priority: P2-P3
Source: FEATURE_PARITY.md — Configuration System

IronClaw uses .env files with a type-safe Config struct. OpenClaw has richer configuration options.

Missing

  • Configuration hot-reload (P2) — Watch config files and apply changes without restart
  • JSON5 support (P3) — Comments, trailing commas in config files
  • YAML alternative (P3) — YAML as alternative config format

Notes

  • Hot-reload is the most impactful item here. The current Config struct is built once at startup.
  • Could use notify crate for file watching + channel to signal config changes
  • JSON5/YAML are lower priority since .env works well for the current use case

Design Considerations

Current Config Architecture

Config struct (src/config.rs:16-33): 14 sub-configs covering database, LLM, embeddings, channels, agent, safety, WASM, secrets, builder, heartbeat, routines, sandbox, and Claude Code.

Two loading methods:

  • Config::from_env() — Env vars only, used before DB connection
  • Config::from_db(store, user_id, bootstrap) — DB settings override env vars

Critical: Config is NOT Arc'd. The config is cloned into individual components at startup:

let agent = Agent::new(
    config.agent.clone(),      // AgentConfig is Clone
    deps,
    channels,
    Some(config.heartbeat.clone()),
    Some(config.routines.clone()),
    ...
);

Components hold their own copies. Changes to the source Config don't propagate.

Settings Storage (Already Implemented)

The Database trait provides 5 settings methods: set_setting(), get_setting(), delete_setting(), get_all_settings(), set_all_settings(). The CLI already has ironclaw config list/get/set/reset/path. Settings use flat dot notation: "agent.name", "heartbeat.interval_secs".

Resolution order: Environment variable → Database setting → Hardcoded default.

Hot-Reload Options

Option A: Component-scoped reload (recommended, minimal refactor)

Keep config cloned to components. Add a reload channel:

pub struct Agent {
    config: AgentConfig,
    config_rx: watch::Receiver<AgentConfig>,  // Tokio watch channel
}

// On config change:
config_tx.send(new_agent_config)?;

// In agent loop:
if self.config_rx.has_changed().unwrap_or(false) {
    self.config = self.config_rx.borrow_and_update().clone();
    tracing::info!("Agent config reloaded");
}

Pros: No Arc/RwLock overhead, each component reads on its own schedule, minimal code change.
Cons: Each component needs a watch channel, config push logic per sub-config.

Option B: Shared Arc<RwLock> (comprehensive, significant refactor)

Wrap Config in Arc<RwLock<Config>>, share across all components. Components call config.read() when they need values.

Pros: Single source of truth, any change immediately visible.
Cons: ~500 LOC changes across 10+ files, lock contention on hot paths, every config access becomes async.

Recommendation: Option A for initial implementation. Only the most dynamic settings need hot-reload (heartbeat interval, gateway port, routine schedules). Most config (database, LLM provider) requires restart anyway.

Config Change Sources

  1. CLI command: ironclaw config set heartbeat.interval_secs 900 → writes to DB → notifies running agent
  2. Web UI: Gateway /api/settings endpoint → same flow
  3. File watch: notify crate watches .env file → re-parse → push changes
  4. Signal: SIGHUP → reload all config from DB

Notification mechanism: Use a Unix domain socket or the existing DB settings table with a config_version counter. Running agent polls or watches for version changes.

What's Actually Hot-Reloadable

Setting Hot-Reloadable Reason
heartbeat.interval_secs Yes Background task reads interval each tick
heartbeat.enabled Yes Start/stop background task
routines.max_concurrent Yes Checked per-fire
agent.max_parallel_jobs Yes Checked before job spawn
safety.max_output_length Yes Checked per tool output
llm.backend No Requires provider re-instantiation
database.url No Requires connection pool rebuild
channels.gateway.port No Requires server rebind
embeddings.provider No Requires provider re-instantiation

JSON5/YAML Support

Lower priority since .env + DB settings cover most use cases. If implemented:

  • JSON5: Use json5 crate. Supports comments and trailing commas. Good for capabilities.json files (developer-facing).
  • YAML: Use serde_yaml crate. Better for complex nested config. Could replace .env for power users.

Recommended path: Support JSON5 for capabilities files and WASM tool manifests (developer experience). Keep .env for main config (simplicity).


Success Criteria

  1. Hot-reload works for dynamic settings: Changing heartbeat.interval_secs via config set takes effect within one heartbeat cycle without restart
  2. Non-reloadable settings warn: Attempting to hot-reload database.url or llm.backend prints "Restart required for this change to take effect"
  3. Config change audit trail: All config changes logged with timestamp, old value, new value, source (CLI/API/file)
  4. File watch optional: Hot-reload works via DB polling even without the notify crate (file watch is a nice-to-have)
  5. No performance regression: Config access on hot paths (tool output sanitization, approval checks) adds < 100ns overhead
  6. Backward compatible: Existing .env + Config::from_env() continues to work unchanged for users who don't use hot-reload

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions