From bcdc52d72c0da77e88b7c21f7708acc939b9e145 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 8 Apr 2026 02:35:19 +0900 Subject: [PATCH] feat(config): add trustedRoots to RuntimeConfig Closes the startup-friction gap filed in ROADMAP (dd97c49). WorkerCreate required trusted_roots on every call with no config-level default. Any batch script that omitted the field stalled all workers at TrustRequired with no auto-recovery path. Changes: - RuntimeFeatureConfig: add trusted_roots: Vec field - ConfigLoader: wire parse_optional_trusted_roots() for 'trustedRoots' key - RuntimeConfig / RuntimeFeatureConfig: expose trusted_roots() accessor - config_validate: add trustedRoots to TOP_LEVEL_FIELDS schema (StringArray) - Tests: parses_trusted_roots_from_settings + trusted_roots_default_is_empty_when_unset Callers can now set trusted_roots in .claw/settings.json: { "trustedRoots": ["/tmp/worktrees"] } WorkerRegistry::spawn_worker() callers should merge config.trusted_roots() with any per-call overrides (wiring left for follow-up). --- rust/crates/runtime/src/config.rs | 67 ++++++++++++++++++++++ rust/crates/runtime/src/config_validate.rs | 4 ++ 2 files changed, 71 insertions(+) diff --git a/rust/crates/runtime/src/config.rs b/rust/crates/runtime/src/config.rs index b034a28..c4e1b6b 100644 --- a/rust/crates/runtime/src/config.rs +++ b/rust/crates/runtime/src/config.rs @@ -85,6 +85,7 @@ pub struct RuntimeFeatureConfig { permission_rules: RuntimePermissionRuleConfig, sandbox: SandboxConfig, provider_fallbacks: ProviderFallbackConfig, + trusted_roots: Vec, } /// Ordered chain of fallback model identifiers used when the primary @@ -334,6 +335,7 @@ impl ConfigLoader { permission_rules: parse_optional_permission_rules(&merged_value)?, sandbox: parse_optional_sandbox_config(&merged_value)?, provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?, + trusted_roots: parse_optional_trusted_roots(&merged_value)?, }; Ok(RuntimeConfig { @@ -428,6 +430,11 @@ impl RuntimeConfig { pub fn provider_fallbacks(&self) -> &ProviderFallbackConfig { &self.feature_config.provider_fallbacks } + + #[must_use] + pub fn trusted_roots(&self) -> &[String] { + &self.feature_config.trusted_roots + } } impl RuntimeFeatureConfig { @@ -492,6 +499,11 @@ impl RuntimeFeatureConfig { pub fn provider_fallbacks(&self) -> &ProviderFallbackConfig { &self.provider_fallbacks } + + #[must_use] + pub fn trusted_roots(&self) -> &[String] { + &self.trusted_roots + } } impl ProviderFallbackConfig { @@ -913,6 +925,14 @@ fn parse_optional_provider_fallbacks( Ok(ProviderFallbackConfig { primary, fallbacks }) } +fn parse_optional_trusted_roots(root: &JsonValue) -> Result, ConfigError> { + let Some(object) = root.as_object() else { + return Ok(Vec::new()); + }; + Ok(optional_string_array(object, "trustedRoots", "merged settings.trustedRoots")? + .unwrap_or_default()) +} + fn parse_filesystem_mode_label(value: &str) -> Result { match value { "off" => Ok(FilesystemIsolationMode::Off), @@ -1465,6 +1485,53 @@ mod tests { fs::remove_dir_all(root).expect("cleanup temp dir"); } + #[test] + fn parses_trusted_roots_from_settings() { + // given + let root = temp_dir(); + let cwd = root.join("project"); + let home = root.join("home").join(".claw"); + fs::create_dir_all(&home).expect("home config dir"); + fs::create_dir_all(&cwd).expect("project dir"); + fs::write( + home.join("settings.json"), + r#"{"trustedRoots": ["/tmp/worktrees", "/home/user/projects"]}"#, + ) + .expect("write settings"); + + // when + let loaded = ConfigLoader::new(&cwd, &home) + .load() + .expect("config should load"); + + // then + let roots = loaded.trusted_roots(); + assert_eq!(roots, ["/tmp/worktrees", "/home/user/projects"]); + + fs::remove_dir_all(root).expect("cleanup temp dir"); + } + + #[test] + fn trusted_roots_default_is_empty_when_unset() { + // given + let root = temp_dir(); + let cwd = root.join("project"); + let home = root.join("home").join(".claw"); + fs::create_dir_all(&home).expect("home config dir"); + fs::create_dir_all(&cwd).expect("project dir"); + fs::write(home.join("settings.json"), "{}").expect("write empty settings"); + + // when + let loaded = ConfigLoader::new(&cwd, &home) + .load() + .expect("config should load"); + + // then + assert!(loaded.trusted_roots().is_empty()); + + fs::remove_dir_all(root).expect("cleanup temp dir"); + } + #[test] fn parses_typed_mcp_and_oauth_config() { let root = temp_dir(); diff --git a/rust/crates/runtime/src/config_validate.rs b/rust/crates/runtime/src/config_validate.rs index 141b34c..7a9c1c4 100644 --- a/rust/crates/runtime/src/config_validate.rs +++ b/rust/crates/runtime/src/config_validate.rs @@ -193,6 +193,10 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[ name: "providerFallbacks", expected: FieldType::Object, }, + FieldSpec { + name: "trustedRoots", + expected: FieldType::StringArray, + }, ]; const HOOKS_FIELDS: &[FieldSpec] = &[