feat(config): add show/get/set subcommands for runtime config inspection and modification

This commit is contained in:
argenis de la rosa 2026-02-28 13:49:36 -05:00 committed by Argenis
parent f6278373cb
commit 20ed60d2a0
5 changed files with 179 additions and 11 deletions

View File

@ -24,7 +24,7 @@ Last verified: **February 28, 2026**.
| `integrations` | Inspect integration details |
| `skills` | List/install/remove skills |
| `migrate` | Import from external runtimes (currently OpenClaw) |
| `config` | Export machine-readable config schema |
| `config` | Inspect, query, and modify runtime configuration |
| `completions` | Generate shell completion scripts to stdout |
| `hardware` | Discover and introspect USB hardware |
| `peripheral` | Configure and flash peripherals |
@ -267,8 +267,17 @@ Skill manifests (`SKILL.toml`) support `prompts` and `[[tools]]`; both are injec
### `config`
- `zeroclaw config show`
- `zeroclaw config get <key>`
- `zeroclaw config set <key> <value>`
- `zeroclaw config schema`
`config show` prints the full effective configuration as pretty JSON with secrets masked as `***REDACTED***`. Environment variable overrides are already applied.
`config get <key>` queries a single value by dot-separated path (e.g. `gateway.port`, `security.estop.enabled`). Scalars print raw values; objects and arrays print pretty JSON. Sensitive fields are masked.
`config set <key> <value>` updates a configuration value and persists it atomically to `config.toml`. Types are inferred automatically (`true`/`false` → bool, integers, floats, JSON syntax → object/array, otherwise string). Type mismatches are rejected before writing.
`config schema` prints a JSON Schema (draft 2020-12) for the full `config.toml` contract to stdout.
### `completions`

View File

@ -14,9 +14,12 @@ ZeroClaw logs the resolved config on startup at `INFO` level:
- `Config loaded` with fields: `path`, `workspace`, `source`, `initialized`
Schema export command:
CLI commands for config inspection and modification:
- `zeroclaw config schema` (prints JSON Schema draft 2020-12 to stdout)
- `zeroclaw config show` — print effective config as JSON (secrets masked)
- `zeroclaw config get <key>` — query a value by dot-path (e.g. `zeroclaw config get gateway.port`)
- `zeroclaw config set <key> <value>` — update a value and save to `config.toml`
- `zeroclaw config schema` — print JSON Schema (draft 2020-12) to stdout
## Core Keys

View File

@ -22,7 +22,7 @@ Xác minh lần cuối: **2026-02-28**.
| `integrations` | Kiểm tra chi tiết tích hợp |
| `skills` | Liệt kê/cài đặt/gỡ bỏ skills |
| `migrate` | Nhập dữ liệu từ runtime khác (hiện hỗ trợ OpenClaw) |
| `config` | Xuất schema cấu hình dạng máy đọc được |
| `config` | Kiểm tra, truy vấn và sửa đổi cấu hình runtime |
| `completions` | Tạo script tự hoàn thành cho shell ra stdout |
| `hardware` | Phát hiện và kiểm tra phần cứng USB |
| `peripheral` | Cấu hình và nạp firmware thiết bị ngoại vi |
@ -124,8 +124,17 @@ Skill manifest (`SKILL.toml`) hỗ trợ `prompts` và `[[tools]]`; cả hai đ
### `config`
- `zeroclaw config show`
- `zeroclaw config get <key>`
- `zeroclaw config set <key> <value>`
- `zeroclaw config schema`
`config show` xuất toàn bộ cấu hình hiệu lực dưới dạng JSON với các trường nhạy cảm được ẩn thành `***REDACTED***`. Các ghi đè từ biến môi trường đã được áp dụng.
`config get <key>` truy vấn một giá trị theo đường dẫn phân tách bằng dấu chấm (ví dụ: `gateway.port`, `security.estop.enabled`). Giá trị đơn in trực tiếp; đối tượng và mảng in dạng JSON.
`config set <key> <value>` cập nhật giá trị cấu hình và lưu nguyên tử vào `config.toml`. Kiểu dữ liệu được suy luận tự động (`true`/`false` → bool, số nguyên, số thực, cú pháp JSON → đối tượng/mảng, còn lại → chuỗi). Sai kiểu sẽ bị từ chối trước khi ghi.
`config schema` xuất JSON Schema (draft 2020-12) cho toàn bộ hợp đồng `config.toml` ra stdout.
### `completions`

View File

@ -14,9 +14,12 @@ ZeroClaw ghi log đường dẫn config đã giải quyết khi khởi động
- `Config loaded` với các trường: `path`, `workspace`, `source`, `initialized`
Lệnh xuất schema:
Lệnh CLI để kiểm tra và sửa đổi cấu hình:
- `zeroclaw config schema` (xuất JSON Schema draft 2020-12 ra stdout)
- `zeroclaw config show` — xuất cấu hình hiệu lực dạng JSON (ẩn secrets)
- `zeroclaw config get <key>` — truy vấn giá trị theo đường dẫn (ví dụ: `zeroclaw config get gateway.port`)
- `zeroclaw config set <key> <value>` — cập nhật giá trị và lưu vào `config.toml`
- `zeroclaw config schema` — xuất JSON Schema (draft 2020-12) ra stdout
## Khóa chính

View File

@ -520,13 +520,13 @@ Examples:
#[command(long_about = "\
Manage ZeroClaw configuration.
Inspect and export configuration settings. Use 'schema' to dump \
the full JSON Schema for the config file, which documents every \
available key, type, and default value.
Inspect, query, and modify configuration settings.
Examples:
zeroclaw config schema # print JSON Schema to stdout
zeroclaw config schema > schema.json")]
zeroclaw config show # show effective config (secrets masked)
zeroclaw config get gateway.port # query a specific value by dot-path
zeroclaw config set gateway.port 8080 # update a value and save to config.toml
zeroclaw config schema # print full JSON Schema to stdout")]
Config {
#[command(subcommand)]
config_command: ConfigCommands,
@ -551,6 +551,20 @@ Examples:
#[derive(Subcommand, Debug)]
enum ConfigCommands {
/// Show the current effective configuration (secrets masked)
Show,
/// Get a specific configuration value by dot-path (e.g. "gateway.port")
Get {
/// Dot-separated config path, e.g. "security.estop.enabled"
key: String,
},
/// Set a configuration value and save to config.toml
Set {
/// Dot-separated config path, e.g. "gateway.port"
key: String,
/// New value (string, number, boolean, or JSON for objects/arrays)
value: String,
},
/// Dump the full configuration JSON Schema to stdout
Schema,
}
@ -1182,6 +1196,94 @@ async fn main() -> Result<()> {
}
Commands::Config { config_command } => match config_command {
ConfigCommands::Show => {
let mut json = serde_json::to_value(&config)
.context("Failed to serialize config")?;
redact_config_secrets(&mut json);
println!("{}", serde_json::to_string_pretty(&json)?);
Ok(())
}
ConfigCommands::Get { key } => {
let mut json = serde_json::to_value(&config)
.context("Failed to serialize config")?;
redact_config_secrets(&mut json);
let mut current = &json;
for segment in key.split('.') {
current = current
.get(segment)
.with_context(|| format!("Config path not found: {key}"))?;
}
match current {
serde_json::Value::String(s) => println!("{s}"),
serde_json::Value::Bool(b) => println!("{b}"),
serde_json::Value::Number(n) => println!("{n}"),
serde_json::Value::Null => println!("null"),
_ => println!("{}", serde_json::to_string_pretty(current)?),
}
Ok(())
}
ConfigCommands::Set { key, value } => {
let mut json = serde_json::to_value(&config)
.context("Failed to serialize config")?;
// Parse the new value: try bool, then integer, then float, then JSON, then string
let new_value = if value == "true" {
serde_json::Value::Bool(true)
} else if value == "false" {
serde_json::Value::Bool(false)
} else if value == "null" {
serde_json::Value::Null
} else if let Ok(n) = value.parse::<i64>() {
serde_json::json!(n)
} else if let Ok(n) = value.parse::<f64>() {
serde_json::json!(n)
} else if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&value) {
// JSON object/array (e.g. '["a","b"]' or '{"key":"val"}')
parsed
} else {
serde_json::Value::String(value.clone())
};
// Navigate to the parent and set the leaf
let segments: Vec<&str> = key.split('.').collect();
if segments.is_empty() {
bail!("Config key cannot be empty");
}
let (parents, leaf) = segments.split_at(segments.len() - 1);
let mut target = &mut json;
for segment in parents {
target = target
.get_mut(*segment)
.with_context(|| format!("Config path not found: {key}"))?;
}
let leaf_key = leaf[0];
if target.get(leaf_key).is_none() {
bail!("Config path not found: {key}");
}
target[leaf_key] = new_value.clone();
// Deserialize back to Config and save.
// Preserve runtime-only fields lost during JSON round-trip (#[serde(skip)]).
let config_path = config.config_path.clone();
let workspace_dir = config.workspace_dir.clone();
config = serde_json::from_value(json)
.context("Invalid value for this config key — type mismatch")?;
config.config_path = config_path;
config.workspace_dir = workspace_dir;
config.save().await?;
// Show the saved value
let display = match &new_value {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
println!("Set {key} = {display}");
Ok(())
}
ConfigCommands::Schema => {
let schema = schemars::schema_for!(config::Config);
println!(
@ -1194,6 +1296,48 @@ async fn main() -> Result<()> {
}
}
/// Keys whose values are masked in `config show` / `config get` output.
const REDACTED_CONFIG_KEYS: &[&str] = &[
"api_key",
"api_keys",
"bot_token",
"paired_tokens",
"db_url",
"http_proxy",
"https_proxy",
"all_proxy",
"secret_key",
"webhook_secret",
];
fn redact_config_secrets(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
for (k, v) in map.iter_mut() {
if REDACTED_CONFIG_KEYS.contains(&k.as_str()) {
match v {
serde_json::Value::String(s) if !s.is_empty() => {
*v = serde_json::Value::String("***REDACTED***".to_string());
}
serde_json::Value::Array(arr) if !arr.is_empty() => {
*v = serde_json::json!(["***REDACTED***"]);
}
_ => {}
}
} else {
redact_config_secrets(v);
}
}
}
serde_json::Value::Array(arr) => {
for item in arr.iter_mut() {
redact_config_secrets(item);
}
}
_ => {}
}
}
fn handle_estop_command(
config: &Config,
estop_command: Option<EstopSubcommands>,