Skip to content

Commit bcdc52d

Browse files
committed
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<String> 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).
1 parent dd97c49 commit bcdc52d

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

rust/crates/runtime/src/config.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub struct RuntimeFeatureConfig {
8585
permission_rules: RuntimePermissionRuleConfig,
8686
sandbox: SandboxConfig,
8787
provider_fallbacks: ProviderFallbackConfig,
88+
trusted_roots: Vec<String>,
8889
}
8990

9091
/// Ordered chain of fallback model identifiers used when the primary
@@ -334,6 +335,7 @@ impl ConfigLoader {
334335
permission_rules: parse_optional_permission_rules(&merged_value)?,
335336
sandbox: parse_optional_sandbox_config(&merged_value)?,
336337
provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?,
338+
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
337339
};
338340

339341
Ok(RuntimeConfig {
@@ -428,6 +430,11 @@ impl RuntimeConfig {
428430
pub fn provider_fallbacks(&self) -> &ProviderFallbackConfig {
429431
&self.feature_config.provider_fallbacks
430432
}
433+
434+
#[must_use]
435+
pub fn trusted_roots(&self) -> &[String] {
436+
&self.feature_config.trusted_roots
437+
}
431438
}
432439

433440
impl RuntimeFeatureConfig {
@@ -492,6 +499,11 @@ impl RuntimeFeatureConfig {
492499
pub fn provider_fallbacks(&self) -> &ProviderFallbackConfig {
493500
&self.provider_fallbacks
494501
}
502+
503+
#[must_use]
504+
pub fn trusted_roots(&self) -> &[String] {
505+
&self.trusted_roots
506+
}
495507
}
496508

497509
impl ProviderFallbackConfig {
@@ -913,6 +925,14 @@ fn parse_optional_provider_fallbacks(
913925
Ok(ProviderFallbackConfig { primary, fallbacks })
914926
}
915927

928+
fn parse_optional_trusted_roots(root: &JsonValue) -> Result<Vec<String>, ConfigError> {
929+
let Some(object) = root.as_object() else {
930+
return Ok(Vec::new());
931+
};
932+
Ok(optional_string_array(object, "trustedRoots", "merged settings.trustedRoots")?
933+
.unwrap_or_default())
934+
}
935+
916936
fn parse_filesystem_mode_label(value: &str) -> Result<FilesystemIsolationMode, ConfigError> {
917937
match value {
918938
"off" => Ok(FilesystemIsolationMode::Off),
@@ -1465,6 +1485,53 @@ mod tests {
14651485
fs::remove_dir_all(root).expect("cleanup temp dir");
14661486
}
14671487

1488+
#[test]
1489+
fn parses_trusted_roots_from_settings() {
1490+
// given
1491+
let root = temp_dir();
1492+
let cwd = root.join("project");
1493+
let home = root.join("home").join(".claw");
1494+
fs::create_dir_all(&home).expect("home config dir");
1495+
fs::create_dir_all(&cwd).expect("project dir");
1496+
fs::write(
1497+
home.join("settings.json"),
1498+
r#"{"trustedRoots": ["/tmp/worktrees", "/home/user/projects"]}"#,
1499+
)
1500+
.expect("write settings");
1501+
1502+
// when
1503+
let loaded = ConfigLoader::new(&cwd, &home)
1504+
.load()
1505+
.expect("config should load");
1506+
1507+
// then
1508+
let roots = loaded.trusted_roots();
1509+
assert_eq!(roots, ["/tmp/worktrees", "/home/user/projects"]);
1510+
1511+
fs::remove_dir_all(root).expect("cleanup temp dir");
1512+
}
1513+
1514+
#[test]
1515+
fn trusted_roots_default_is_empty_when_unset() {
1516+
// given
1517+
let root = temp_dir();
1518+
let cwd = root.join("project");
1519+
let home = root.join("home").join(".claw");
1520+
fs::create_dir_all(&home).expect("home config dir");
1521+
fs::create_dir_all(&cwd).expect("project dir");
1522+
fs::write(home.join("settings.json"), "{}").expect("write empty settings");
1523+
1524+
// when
1525+
let loaded = ConfigLoader::new(&cwd, &home)
1526+
.load()
1527+
.expect("config should load");
1528+
1529+
// then
1530+
assert!(loaded.trusted_roots().is_empty());
1531+
1532+
fs::remove_dir_all(root).expect("cleanup temp dir");
1533+
}
1534+
14681535
#[test]
14691536
fn parses_typed_mcp_and_oauth_config() {
14701537
let root = temp_dir();

rust/crates/runtime/src/config_validate.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
193193
name: "providerFallbacks",
194194
expected: FieldType::Object,
195195
},
196+
FieldSpec {
197+
name: "trustedRoots",
198+
expected: FieldType::StringArray,
199+
},
196200
];
197201

198202
const HOOKS_FIELDS: &[FieldSpec] = &[

0 commit comments

Comments
 (0)