- 3124
"heartbeat.quiet_hours '{raw}' is not 'HH:MM-HH:MM'; ignoring" - 3125
)); - 3126
None - 3127
} - 3128
}, - 3129
}; - 3130
match hb.max_findings { - 3131
Some(0) => { - 3132
cfg.warnings - 3133
.push("heartbeat.max_findings must be >= 1; using 1".into()); - 3134
cfg.heartbeat.max_findings = 1; - 3135
} - 3136
Some(n) => cfg.heartbeat.max_findings = n, - 3137
None => cfg.heartbeat.max_findings = 3, - 3138
} - 3139
let fs = &merged.feeds; - 3140
cfg.feeds.enabled = fs.enabled.unwrap_or(false); - 3141
cfg.feeds.config_path = fs.config_path.clone(); - 3142
cfg.feeds.db_path = fs.db_path.clone(); - 3143
cfg.feeds.default_check_interval = fs - 3144
.default_check_interval - 3145
.clone() - 3146
.unwrap_or_else(|| "30m".into()); - 3147
cfg.feeds.max_items_per_feed = fs.max_items_per_feed.unwrap_or(500); - 3148
cfg.feeds.dedup_window_days = fs.dedup_window_days.unwrap_or(90); - 3149
- 3150
// ---- [server] (docs/design/48-web-client.md §4.2) -------------------- - 3151
// - 3152
// Defaults reproduce the pre-web behaviour exactly: loopback only, no - 3153
// trusted hosts, no terminal over the web. Every step away from that is - 3154
// something an operator typed on purpose. - 3155
let sv = &merged.server; - 3156
cfg.server.bind = sv - 3157
.bind - 3158
.clone() - 3159
.map(|b| b.trim().to_string()) - 3160
.filter(|b| !b.is_empty()) - 3161
.unwrap_or_else(|| "127.0.0.1".into()); - 3162
cfg.server.trusted_hosts = sv - 3163
.trusted_hosts - 3164
.clone() - 3165
.unwrap_or_default() - 3166
.into_iter() - 3167
.map(|h| h.trim().to_ascii_lowercase()) - 3168
.filter(|h| !h.is_empty()) - 3169
.collect(); - 3170
if cfg.server.trusted_hosts.iter().any(|h| h.contains('*')) { - 3171
cfg.server.trusted_hosts.retain(|h| !h.contains('*')); - 3172
cfg.warnings.push( - 3173
"server.trusted_hosts entries containing '*' were ignored: a wildcard \ - 3174
defeats the DNS-rebinding check the list exists to enforce" - 3175
.into(), - 3176
); - 3177
} - 3178
cfg.server.public_url = sv - 3179
.public_url - 3180
.clone() - 3181
.map(|u| u.trim().trim_end_matches('/').to_string()) - 3182
.filter(|u| !u.is_empty()); - 3183
cfg.server.session_ttl_hours = sv.session_ttl_hours.filter(|h| *h > 0).unwrap_or(168); - 3184
cfg.server.loopback_auto_login = sv.loopback_auto_login.unwrap_or(true); - 3185
cfg.server.workspace_roots = sv - 3186
.workspace_roots - 3187
.clone() - 3188
.unwrap_or_default() - 3189
.into_iter() - 3190
.map(std::path::PathBuf::from) - 3191
.filter(|p| p.is_absolute()) - 3192
.collect(); - 3193
cfg.server.web_terminal = sv.web.terminal.unwrap_or(false); - 3194
cfg.server.web_terminal_requires_loopback = sv.web.terminal_requires_loopback.unwrap_or(true); - 3195
// Bus config: resolve the workspace encryption secret from the named env var. - 3196
// The env var name itself is never stored in the resolved config — only the - 3197
// secret bytes read from the environment at resolution time. - 3198
cfg.server.bus = BusResolved { - 3199
nats_url: sv.bus.nats_url.clone(), - 3200
workspace_secret: sv - 3201
.bus - 3202
.workspace_secret_env - 3203
.as_deref() - 3204
.and_then(|env_name| std::env::var(env_name).ok()) - 3205
.map(|s| s.into_bytes()), - 3206
}; - 3207
for (name, hook) in merged.gateway.outbound.webhooks { - 3208
if hook.url.trim().is_empty() { - 3209
cfg.warnings.push(format!( - 3210
"gateway.outbound.webhooks.{name}.url is empty; ignored" - 3211
)); - 3212
continue; - 3213
} - 3214
if let Some(env_name) = &hook.token_env - 3215
&& env_name.trim().is_empty() - 3216
{ - 3217
cfg.warnings.push(format!( - 3218
"gateway.outbound.webhooks.{name}.token_env is empty; delivery will fail closed" - 3219
)); - 3220
} - 3221
cfg.gateway.webhooks.insert( - 3222
name, - 3223
WebhookResolved { - 3224
url: hook.url, - 3225
token_env: hook.token_env, - 3226
}, - 3227
); - 3228
} - 3229
- 3230
cfg.plugins = PluginResolved { - 3231
enabled: merged.plugins.enabled, - 3232
disabled: merged.plugins.disabled, - 3233
allow: merged.plugins.allow, - 3234
deny: merged.plugins.deny, - 3235
network_allow: merged.plugins.network_allow, - 3236
network_deny: merged.plugins.network_deny, - 3237
}; - 3238
- 3239
if let Some(name) = &merged.profile { - 3240
if let Some(profile) = merged.profiles.get(name) { - 3241
if let Some(model) = &profile.model { - 3242
cfg.model = model.clone(); - 3243
} - 3244
if let Some(provider) = &profile.provider { - 3245
cfg.provider = provider.clone(); - 3246
} - 3247
if let Some(mode) = profile.permission_mode { - 3248
cfg.permission_mode = mode; - 3249
} - 3250
if let Some(turns) = profile.max_turns { - 3251
cfg.max_turns = turns; - 3252
} - 3253
} else { - 3254
cfg.warnings.push(format!("profile '{name}' not defined")); - 3255
} - 3256
} - 3257
- 3258
Ok(cfg) - 3259
} - 3260
- 3261
fn parse_file(path: &Path) -> Result<(FileConfig, Vec<String>), ConfigError> { - 3262
let text = std::fs::read_to_string(path).map_err(|source| ConfigError::Read { - 3263
path: path.to_path_buf(), - 3264
source, - 3265
})?; - 3266
let fc: FileConfig = toml::from_str(&text).map_err(|source| ConfigError::Parse { - 3267
path: path.to_path_buf(), - 3268
source, - 3269
})?; - 3270
let warnings = unknown_key_warnings(path, &text); - 3271
Ok((fc, warnings)) - 3272
} - 3273
- 3274
const KNOWN_TOP_KEYS: &[&str] = &[ - 3275
"provider", - 3276
"model", - 3277
"max_tokens", - 3278
"max_turns", - 3279
"permission_mode", - 3280
"approval_mode", - 3281
"profile", - 3282
"profiles", - 3283
"anthropic_base_url", - 3284
"allow", - 3285
"ask", - 3286
"deny", - 3287
"workers", - 3288
// Legacy alias for `workers`, kept so old configs don't trigger an - 3289
// "unknown key" warning. - 3290
"subagents", - 3291
"hooks", - 3292
"max_retries", - 3293
"retry_base_backoff_ms", - 3294
"request_timeout_secs", - 3295
"run_retry_attempts", - 3296
"run_retry_base_backoff_ms", - 3297
"circuit_breaker_threshold", - 3298
"circuit_breaker_cooldown_secs", - 3299
"context_window", - 3300
"mcp", - 3301
"capabilities", - 3302
"ui", - 3303
"stop_policy", - 3304
"work", - 3305
"gateway", - 3306
"memory", - 3307
"sandbox", - 3308
"finops", - 3309
"automation", - 3310
"update", - 3311
"tools", - 3312
"heartbeat", - 3313
"plugins", - 3314
"intent", - 3315
"voice", - 3316
"probe", - 3317
"providers", - 3318
]; - 3319
const KNOWN_PLUGINS_KEYS: &[&str] = &[ - 3320
"enabled", - 3321
"disabled", - 3322
"allow", - 3323
"deny", - 3324
"network_allow", - 3325
"network_deny", - 3326
]; - 3327
const KNOWN_FINOPS_KEYS: &[&str] = &["max_run_usd", "max_day_usd", "price_overrides"]; - 3328
const KNOWN_GOAL_KEYS: &[&str] = &["handoff_reset", "max_audit_blocks"]; - 3329
const KNOWN_WORK_KEYS: &[&str] = &[ - 3330
"enabled", - 3331
"default_mode", - 3332
"max_items", - 3333
"max_revisions", - 3334
"max_parallel", - 3335
"confirmation", - 3336
]; - 3337
const KNOWN_PROFILE_KEYS: &[&str] = &["model", "provider", "permission_mode", "max_turns"]; - 3338
const KNOWN_HOOK_KEYS: &[&str] = &[ - 3339
"event", - 3340
"match", - 3341
"command", - 3342
"timeout_ms", - 3343
"enabled", - 3344
"failure_mode", - 3345
]; - 3346
const KNOWN_MCP_SERVER_KEYS: &[&str] = &["command", "args", "env", "network"]; - 3347
const KNOWN_CAPABILITY_KEYS: &[&str] = &[ - 3348
"inherit_mcp", - 3349
"inherit_hooks", - 3350
"inherit_skills", - 3351
"inherit_commands", - 3352
"inherit_plugins", - 3353
]; - 3354
const KNOWN_UI_KEYS: &[&str] = &[ - 3355
"theme", - 3356
"bell", - 3357
"keymap", - 3358
"composer", - 3359
"osc52", - 3360
"accessibility", - 3361
"themes", - 3362
]; - 3363
const KNOWN_THEME_COLORS: &[&str] = &[ - 3364
"accent", - 3365
"dim", - 3366
"success", - 3367
"error", - 3368
"warning", - 3369
"heading", - 3370
"code", - 3371
"user", - 3372
"spinner", - 3373
"panel_bg", - 3374
"selected_bg", - 3375
]; - 3376
const KNOWN_ACCESSIBILITY_KEYS: &[&str] = &["plain", "reduced_motion", "screen_reader"]; - 3377
const KNOWN_STOP_POLICY_KEYS: &[&str] = &["enabled", "marker_gate", "verify_gate", "max_blocks"]; - 3378
const KNOWN_GATEWAY_KEYS: &[&str] = &[ - 3379
"enabled", - 3380
"approvals", - 3381
"approver", - 3382
"approval_timeout_secs", - 3383
"outbound", - 3384
"rate_limit", - 3385
"chat_allowlist", - 3386
"chat_allowlist_open", - 3387
"core_pool_max", - 3388
"core_pool_idle_secs", - 3389
"pending_expiry_days", - 3390
]; - 3391
const KNOWN_MEMORY_KEYS: &[&str] = &[ - 3392
"search_enabled", - 3393
"write_enabled", - 3394
"skill_proposals", - 3395
"reflection", - 3396
]; - 3397
const KNOWN_SANDBOX_KEYS: &[&str] = &["backend", "image"]; - 3398
const KNOWN_OUTBOUND_KEYS: &[&str] = &["webhooks"]; - 3399
const KNOWN_WEBHOOK_KEYS: &[&str] = &["url", "token_env"]; - 3400
const KNOWN_AUTOMATION_KEYS: &[&str] = &["catch_up_missed"]; - 3401
const KNOWN_UPDATE_KEYS: &[&str] = &["url", "interval_hours"]; - 3402
const KNOWN_TOOLS_KEYS: &[&str] = &["web_fetch", "browse"]; - 3403
const KNOWN_HEARTBEAT_KEYS: &[&str] = &[ - 3404
"enabled", - 3405
"interval_secs", - 3406
"model", - 3407
"quiet_hours", - 3408
"max_findings", - 3409
]; - 3410
const KNOWN_PROVIDERS_KEYS: &[&str] = &["ollama", "anthropic"]; - 3411
const KNOWN_OLLAMA_KEYS: &[&str] = &["keep_alive", "num_ctx"]; - 3412
const KNOWN_ANTHROPIC_PROVIDER_KEYS: &[&str] = &["fast_mode"]; - 3413
- 3414
/// A typo'd key must be visible, not silently dead: diff the raw TOML - 3415
/// against the known schema and surface every unrecognized key. - 3416
fn unknown_key_warnings(path: &Path, text: &str) -> Vec<String> { - 3417
let Ok(v) = toml::from_str::<toml::Value>(text) else { - 3418
return Vec::new(); - 3419
}; - 3420
let mut out = Vec::new(); - 3421
let Some(top) = v.as_table() else { - 3422
return out; - 3423
}; - 3424
for key in top.keys() { - 3425
if !KNOWN_TOP_KEYS.contains(&key.as_str()) { - 3426
out.push(format!( - 3427
"{}: unknown config key '{key}' (ignored)", - 3428
path.display() - 3429
)); - 3430
} - 3431
} - 3432
if let Some(profiles) = top.get("profiles").and_then(toml::Value::as_table) { - 3433
for (name, t) in profiles { - 3434
if let Some(t) = t.as_table() { - 3435
for key in t.keys() { - 3436
if !KNOWN_PROFILE_KEYS.contains(&key.as_str()) { - 3437
out.push(format!( - 3438
"{}: unknown profile key 'profiles.{name}.{key}' (ignored)", - 3439
path.display() - 3440
)); - 3441
} - 3442
} - 3443
} - 3444
} - 3445
} - 3446
if let Some(hooks) = top.get("hooks").and_then(toml::Value::as_array) { - 3447
for (i, h) in hooks.iter().enumerate() { - 3448
if let Some(h) = h.as_table() { - 3449
for key in h.keys() { - 3450
if !KNOWN_HOOK_KEYS.contains(&key.as_str()) { - 3451
out.push(format!( - 3452
"{}: unknown hook key 'hooks[{i}].{key}' (ignored)", - 3453
path.display() - 3454
)); - 3455
} - 3456
} - 3457
} - 3458
} - 3459
} - 3460
if let Some(servers) = top - 3461
.get("mcp") - 3462
.and_then(|m| m.get("servers")) - 3463
.and_then(toml::Value::as_table) - 3464
{ - 3465
for (name, t) in servers { - 3466
if let Some(t) = t.as_table() { - 3467
for key in t.keys() { - 3468
if !KNOWN_MCP_SERVER_KEYS.contains(&key.as_str()) { - 3469
out.push(format!( - 3470
"{}: unknown mcp server key 'mcp.servers.{name}.{key}' (ignored)", - 3471
path.display() - 3472
)); - 3473
} - 3474
} - 3475
} - 3476
} - 3477
} - 3478
if let Some(t) = top.get("capabilities").and_then(toml::Value::as_table) { - 3479
for key in t.keys() { - 3480
if !KNOWN_CAPABILITY_KEYS.contains(&key.as_str()) { - 3481
out.push(format!( - 3482
"{}: unknown capabilities key 'capabilities.{key}' (ignored)", - 3483
path.display() - 3484
)); - 3485
} - 3486
} - 3487
} - 3488
if let Some(ui) = top.get("ui").and_then(toml::Value::as_table) { - 3489
for key in ui.keys() { - 3490
if !KNOWN_UI_KEYS.contains(&key.as_str()) { - 3491
out.push(format!( - 3492
"{}: unknown ui key 'ui.{key}' (ignored)", - 3493
path.display() - 3494
)); - 3495
} - 3496
} - 3497
if let Some(acc) = ui.get("accessibility").and_then(toml::Value::as_table) { - 3498
for key in acc.keys() { - 3499
if !KNOWN_ACCESSIBILITY_KEYS.contains(&key.as_str()) { - 3500
out.push(format!( - 3501
"{}: unknown ui.accessibility key 'ui.accessibility.{key}' (ignored)", - 3502
path.display() - 3503
)); - 3504
} - 3505
} - 3506
} - 3507
if let Some(themes) = ui.get("themes").and_then(toml::Value::as_table) { - 3508
for (name, t) in themes { - 3509
if let Some(t) = t.as_table() { - 3510
for key in t.keys() { - 3511
if !KNOWN_THEME_COLORS.contains(&key.as_str()) { - 3512
out.push(format!( - 3513
"{}: unknown theme color 'ui.themes.{name}.{key}' (ignored)", - 3514
path.display() - 3515
)); - 3516
} - 3517
} - 3518
} - 3519
} - 3520
} - 3521
} - 3522
if let Some(sp) = top.get("stop_policy").and_then(toml::Value::as_table) { - 3523
for key in sp.keys() { - 3524
if !KNOWN_STOP_POLICY_KEYS.contains(&key.as_str()) { - 3525
out.push(format!( - 3526
"{}: unknown stop_policy key 'stop_policy.{key}' (ignored)", - 3527
path.display() - 3528
)); - 3529
} - 3530
} - 3531
} - 3532
if let Some(gl) = top.get("goal").and_then(toml::Value::as_table) { - 3533
for key in gl.keys() { - 3534
if !KNOWN_GOAL_KEYS.contains(&key.as_str()) { - 3535
out.push(format!( - 3536
"{}: unknown goal key 'goal.{key}' (ignored)", - 3537
path.display() - 3538
)); - 3539
} - 3540
} - 3541
} - 3542
if let Some(fo) = top.get("finops").and_then(toml::Value::as_table) { - 3543
for key in fo.keys() { - 3544
if !KNOWN_FINOPS_KEYS.contains(&key.as_str()) { - 3545
out.push(format!( - 3546
"{}: unknown finops key 'finops.{key}' (ignored)", - 3547
path.display() - 3548
)); - 3549
} - 3550
} - 3551
} - 3552
if let Some(work) = top.get("work").and_then(toml::Value::as_table) { - 3553
for key in work.keys() { - 3554
if !KNOWN_WORK_KEYS.contains(&key.as_str()) { - 3555
out.push(format!( - 3556
"{}: unknown work key 'work.{key}' (ignored)", - 3557
path.display() - 3558
)); - 3559
} - 3560
} - 3561
} - 3562
if let Some(gw) = top.get("gateway").and_then(toml::Value::as_table) { - 3563
for key in gw.keys() { - 3564
if !KNOWN_GATEWAY_KEYS.contains(&key.as_str()) { - 3565
out.push(format!( - 3566
"{}: unknown gateway key 'gateway.{key}' (ignored)", - 3567
path.display() - 3568
)); - 3569
} - 3570
} - 3571
if let Some(ob) = gw.get("outbound").and_then(toml::Value::as_table) { - 3572
for key in ob.keys() { - 3573
if !KNOWN_OUTBOUND_KEYS.contains(&key.as_str()) { - 3574
out.push(format!( - 3575
"{}: unknown gateway.outbound key 'gateway.outbound.{key}' (ignored)", - 3576
path.display() - 3577
)); - 3578
} - 3579
} - 3580
if let Some(hooks) = ob.get("webhooks").and_then(toml::Value::as_table) { - 3581
for (name, t) in hooks { - 3582
if let Some(t) = t.as_table() { - 3583
for key in t.keys() { - 3584
if !KNOWN_WEBHOOK_KEYS.contains(&key.as_str()) { - 3585
out.push(format!( - 3586
"{}: unknown webhook key 'gateway.outbound.webhooks.{name}.{key}' (ignored)", - 3587
path.display() - 3588
)); - 3589
} - 3590
} - 3591
} - 3592
} - 3593
} - 3594
} - 3595
} - 3596
if let Some(mem) = top.get("memory").and_then(toml::Value::as_table) { - 3597
for key in mem.keys() { - 3598
if !KNOWN_MEMORY_KEYS.contains(&key.as_str()) { - 3599
out.push(format!( - 3600
"{}: unknown memory key 'memory.{key}' (ignored)", - 3601
path.display() - 3602
)); - 3603
} - 3604
} - 3605
} - 3606
if let Some(sb) = top.get("sandbox").and_then(toml::Value::as_table) { - 3607
for key in sb.keys() { - 3608
if !KNOWN_SANDBOX_KEYS.contains(&key.as_str()) { - 3609
out.push(format!( - 3610
"{}: unknown sandbox key 'sandbox.{key}' (ignored)", - 3611
path.display() - 3612
)); - 3613
} - 3614
} - 3615
} - 3616
if let Some(t) = top.get("automation").and_then(toml::Value::as_table) { - 3617
for key in t.keys() { - 3618
if !KNOWN_AUTOMATION_KEYS.contains(&key.as_str()) { - 3619
out.push(format!( - 3620
"{}: unknown automation key 'automation.{key}' (ignored)", - 3621
path.display() - 3622
)); - 3623
} - 3624
} - 3625
} - 3626
if let Some(t) = top.get("update").and_then(toml::Value::as_table) { - 3627
for key in t.keys() { - 3628
if !KNOWN_UPDATE_KEYS.contains(&key.as_str()) { - 3629
out.push(format!( - 3630
"{}: unknown update key 'update.{key}' (ignored)", - 3631
path.display() - 3632
)); - 3633
} - 3634
} - 3635
} - 3636
if let Some(t) = top.get("tools").and_then(toml::Value::as_table) { - 3637
for key in t.keys() { - 3638
if !KNOWN_TOOLS_KEYS.contains(&key.as_str()) { - 3639
out.push(format!( - 3640
"{}: unknown tools key 'tools.{key}' (ignored)", - 3641
path.display() - 3642
)); - 3643
} - 3644
} - 3645
} - 3646
if let Some(t) = top.get("heartbeat").and_then(toml::Value::as_table) { - 3647
for key in t.keys() { - 3648
if !KNOWN_HEARTBEAT_KEYS.contains(&key.as_str()) { - 3649
out.push(format!( - 3650
"{}: unknown heartbeat key 'heartbeat.{key}' (ignored)", - 3651
path.display() - 3652
)); - 3653
} - 3654
} - 3655
} - 3656
if let Some(t) = top.get("plugins").and_then(toml::Value::as_table) { - 3657
for key in t.keys() { - 3658
if !KNOWN_PLUGINS_KEYS.contains(&key.as_str()) { - 3659
out.push(format!( - 3660
"{}: unknown plugins key 'plugins.{key}' (ignored)", - 3661
path.display() - 3662
)); - 3663
} - 3664
} - 3665
} - 3666
if let Some(t) = top.get("providers").and_then(toml::Value::as_table) { - 3667
for key in t.keys() { - 3668
if !KNOWN_PROVIDERS_KEYS.contains(&key.as_str()) { - 3669
out.push(format!( - 3670
"{}: unknown providers key 'providers.{key}' (ignored)", - 3671
path.display() - 3672
)); - 3673
} - 3674
} - 3675
if let Some(t) = t.get("ollama").and_then(toml::Value::as_table) { - 3676
for key in t.keys() { - 3677
if !KNOWN_OLLAMA_KEYS.contains(&key.as_str()) { - 3678
out.push(format!( - 3679
"{}: unknown providers key 'providers.ollama.{key}' (ignored)", - 3680
path.display() - 3681
)); - 3682
} - 3683
} - 3684
} - 3685
if let Some(t) = t.get("anthropic").and_then(toml::Value::as_table) { - 3686
for key in t.keys() { - 3687
if !KNOWN_ANTHROPIC_PROVIDER_KEYS.contains(&key.as_str()) { - 3688
out.push(format!( - 3689
"{}: unknown providers key 'providers.anthropic.{key}' (ignored)", - 3690
path.display() - 3691
)); - 3692
} - 3693
} - 3694
} - 3695
} - 3696
out - 3697
} - 3698
- 3699
fn merge_into(base: &mut FileConfig, over: FileConfig) { - 3700
if over.provider.is_some() { - 3701
base.provider = over.provider; - 3702
} - 3703
if over.model.is_some() { - 3704
base.model = over.model; - 3705
} - 3706
if over.max_tokens.is_some() { - 3707
base.max_tokens = over.max_tokens; - 3708
} - 3709
if over.max_turns.is_some() { - 3710
base.max_turns = over.max_turns; - 3711
} - 3712
if over.permission_mode.is_some() { - 3713
base.permission_mode = over.permission_mode; - 3714
} - 3715
if over.approval_mode.is_some() { - 3716
base.approval_mode = over.approval_mode; - 3717
} - 3718
if over.profile.is_some() { - 3719
base.profile = over.profile; - 3720
} - 3721
if over.anthropic_base_url.is_some() { - 3722
base.anthropic_base_url = over.anthropic_base_url; - 3723
} - 3724
for r in over.allow { - 3725
if !base.allow.contains(&r) { - 3726
base.allow.push(r); - 3727
} - 3728
} - 3729
for r in over.ask { - 3730
if !base.ask.contains(&r) { - 3731
base.ask.push(r); - 3732
} - 3733
} - 3734
for r in over.deny { - 3735
if !base.deny.contains(&r) { - 3736
base.deny.push(r); - 3737
} - 3738
} - 3739
if over.workers.is_some() { - 3740
base.workers = over.workers; - 3741
} - 3742
if over.max_retries.is_some() { - 3743
base.max_retries = over.max_retries; - 3744
} - 3745
if over.retry_base_backoff_ms.is_some() { - 3746
base.retry_base_backoff_ms = over.retry_base_backoff_ms; - 3747
} - 3748
if over.request_timeout_secs.is_some() { - 3749
base.request_timeout_secs = over.request_timeout_secs; - 3750
} - 3751
if over.run_retry_attempts.is_some() { - 3752
base.run_retry_attempts = over.run_retry_attempts; - 3753
} - 3754
if over.run_retry_base_backoff_ms.is_some() { - 3755
base.run_retry_base_backoff_ms = over.run_retry_base_backoff_ms; - 3756
} - 3757
if over.circuit_breaker_threshold.is_some() { - 3758
base.circuit_breaker_threshold = over.circuit_breaker_threshold; - 3759
} - 3760
if over.circuit_breaker_cooldown_secs.is_some() { - 3761
base.circuit_breaker_cooldown_secs = over.circuit_breaker_cooldown_secs; - 3762
} - 3763
if over.context_window.is_some() { - 3764
base.context_window = over.context_window; - 3765
} - 3766
// Voice settings are scalar overrides; keep the narrower layer's intent. - 3767
if over.voice.is_some() { - 3768
base.voice = over.voice; - 3769
} - 3770
if over.capabilities.inherit_hooks == Some(false) { - 3771
base.hooks.clear(); - 3772
} - 3773
if over.capabilities.inherit_mcp == Some(false) { - 3774
base.mcp.servers.clear(); - 3775
} - 3776
if over.capabilities.inherit_mcp.is_some() { - 3777
base.capabilities.inherit_mcp = over.capabilities.inherit_mcp; - 3778
} - 3779
if over.capabilities.inherit_hooks.is_some() { - 3780
base.capabilities.inherit_hooks = over.capabilities.inherit_hooks; - 3781
} - 3782
if over.capabilities.inherit_skills.is_some() { - 3783
base.capabilities.inherit_skills = over.capabilities.inherit_skills; - 3784
} - 3785
if over.capabilities.inherit_commands.is_some() { - 3786
base.capabilities.inherit_commands = over.capabilities.inherit_commands; - 3787
} - 3788
if over.capabilities.inherit_plugins.is_some() { - 3789
base.capabilities.inherit_plugins = over.capabilities.inherit_plugins; - 3790
} - 3791
// Unlike allow/ask/deny just above, this used to be a plain `.extend` - 3792
// with no dedup. `PUT /config/hooks` (vak-server) always resubmitted the - 3793
// full *effective* list it had just read — global layer included — so - 3794
// every hook edit re-wrote the project's own inherited copy of every - 3795
// global hook back into the project file, and the next load merged - 3796
// both: the same hook doubled, then tripled on the next edit, without - 3797
// bound. `get_hooks` no longer echoes inherited hooks for this reason, - 3798
// but a hand-edited config with a genuine duplicate should not compound - 3799
// either. - 3800
for h in over.hooks { - 3801
let key = |hook: &HookConfig| { - 3802
( - 3803
hook.event.clone(), - 3804
hook.matcher.clone(), - 3805
hook.command.clone(), - 3806
) - 3807
}; - 3808
let incoming = key(&h); - 3809
if let Some(existing) = base - 3810
.hooks - 3811
.iter_mut() - 3812
.find(|candidate| key(candidate) == incoming) - 3813
{ - 3814
*existing = h; - 3815
} else { - 3816
base.hooks.push(h); - 3817
} - 3818
} - 3819
for (name, srv) in over.mcp.servers { - 3820
base.mcp.servers.insert(name, srv); - 3821
} - 3822
if over.ui.theme.is_some() { - 3823
base.ui.theme = over.ui.theme; - 3824
} - 3825
if over.ui.bell.is_some() { - 3826
base.ui.bell = over.ui.bell; - 3827
} - 3828
for (k, v) in &over.ui.keymap { - 3829
base.ui.keymap.insert(k.clone(), v.clone()); - 3830
} - 3831
if over.ui.composer.is_some() { - 3832
base.ui.composer = over.ui.composer; - 3833
} - 3834
if over.ui.osc52.is_some() { - 3835
base.ui.osc52 = over.ui.osc52; - 3836
} - 3837
if let Some(acc) = over.ui.accessibility { - 3838
let entry = base - 3839
.ui - 3840
.accessibility - 3841
.get_or_insert_with(AccessibilitySettings::default); - 3842
if acc.plain.is_some() { - 3843
entry.plain = acc.plain; - 3844
} - 3845
if acc.reduced_motion.is_some() { - 3846
entry.reduced_motion = acc.reduced_motion; - 3847
} - 3848
if acc.screen_reader.is_some() { - 3849
entry.screen_reader = acc.screen_reader; - 3850
} - 3851
} - 3852
for (name, colors) in &over.ui.themes { - 3853
let entry = base.ui.themes.entry(name.clone()).or_default(); - 3854
for (k, v) in colors { - 3855
entry.insert(k.clone(), v.clone()); - 3856
} - 3857
} - 3858
if let Some(sp) = over.stop_policy { - 3859
base.stop_policy = Some(sp); - 3860
} - 3861
if over.gateway.enabled.is_some() { - 3862
base.gateway.enabled = over.gateway.enabled; - 3863
} - 3864
if over.gateway.approvals.is_some() { - 3865
base.gateway.approvals = over.gateway.approvals; - 3866
} - 3867
if over.gateway.approver.is_some() { - 3868
base.gateway.approver = over.gateway.approver; - 3869
} - 3870
if over.gateway.approval_timeout_secs.is_some() { - 3871
base.gateway.approval_timeout_secs = over.gateway.approval_timeout_secs; - 3872
} - 3873
if over.gateway.rate_limit.is_some() { - 3874
base.gateway.rate_limit = over.gateway.rate_limit; - 3875
} - 3876
if !over.gateway.chat_allowlist.is_empty() { - 3877
base.gateway.chat_allowlist = over.gateway.chat_allowlist; - 3878
} - 3879
if over.gateway.chat_allowlist_open.is_some() { - 3880
base.gateway.chat_allowlist_open = over.gateway.chat_allowlist_open; - 3881
} - 3882
if over.gateway.core_pool_max.is_some() { - 3883
base.gateway.core_pool_max = over.gateway.core_pool_max; - 3884
} - 3885
if over.gateway.core_pool_idle_secs.is_some() { - 3886
base.gateway.core_pool_idle_secs = over.gateway.core_pool_idle_secs; - 3887
} - 3888
if over.gateway.pending_expiry_days.is_some() { - 3889
base.gateway.pending_expiry_days = over.gateway.pending_expiry_days; - 3890
} - 3891
if over.memory.search_enabled.is_some() { - 3892
base.memory.search_enabled = over.memory.search_enabled; - 3893
} - 3894
if over.memory.write_enabled.is_some() { - 3895
base.memory.write_enabled = over.memory.write_enabled; - 3896
} - 3897
if over.memory.skill_proposals.is_some() { - 3898
base.memory.skill_proposals = over.memory.skill_proposals; - 3899
} - 3900
if over.memory.reflection.is_some() { - 3901
base.memory.reflection = over.memory.reflection; - 3902
} - 3903
if over.sandbox.backend.is_some() { - 3904
base.sandbox.backend = over.sandbox.backend; - 3905
} - 3906
if over.sandbox.image.is_some() { - 3907
base.sandbox.image = over.sandbox.image; - 3908
} - 3909
if over.finops.max_run_usd.is_some() { - 3910
base.finops.max_run_usd = over.finops.max_run_usd; - 3911
} - 3912
if over.finops.max_day_usd.is_some() { - 3913
base.finops.max_day_usd = over.finops.max_day_usd; - 3914
} - 3915
for (k, v) in over.finops.price_overrides { - 3916
base.finops.price_overrides.insert(k, v); - 3917
} - 3918
if over.goal.handoff_reset.is_some() { - 3919
base.goal.handoff_reset = over.goal.handoff_reset; - 3920
} - 3921
if over.goal.max_audit_blocks.is_some() { - 3922
base.goal.max_audit_blocks = over.goal.max_audit_blocks; - 3923
} - 3924
if over.work.enabled.is_some() { - 3925
base.work.enabled = over.work.enabled; - 3926
} - 3927
if over.work.default_mode.is_some() { - 3928
base.work.default_mode = over.work.default_mode; - 3929
} - 3930
if over.work.max_items.is_some() { - 3931
base.work.max_items = over.work.max_items; - 3932
} - 3933
if over.work.max_revisions.is_some() { - 3934
base.work.max_revisions = over.work.max_revisions; - 3935
} - 3936
if over.work.max_parallel.is_some() { - 3937
base.work.max_parallel = over.work.max_parallel; - 3938
} - 3939
if over.work.confirmation.is_some() { - 3940
base.work.confirmation = over.work.confirmation; - 3941
} - 3942
if over.route.objective.is_some() { - 3943
base.route.objective = over.route.objective; - 3944
} - 3945
for m in over.route.fallback_models { - 3946
if !base.route.fallback_models.contains(&m) { - 3947
base.route.fallback_models.push(m); - 3948
} - 3949
} - 3950
if over.route.max_fallbacks.is_some() { - 3951
base.route.max_fallbacks = over.route.max_fallbacks; - 3952
} - 3953
if over.probe.hosted.is_some() { - 3954
base.probe.hosted = over.probe.hosted; - 3955
} - 3956
if over.providers.ollama.keep_alive.is_some() { - 3957
base.providers.ollama.keep_alive = over.providers.ollama.keep_alive; - 3958
} - 3959
if over.providers.ollama.num_ctx.is_some() { - 3960
base.providers.ollama.num_ctx = over.providers.ollama.num_ctx; - 3961
} - 3962
if over.providers.anthropic.fast_mode.is_some() { - 3963
base.providers.anthropic.fast_mode = over.providers.anthropic.fast_mode; - 3964
} - 3965
if over.intent.enabled.is_some() { - 3966
base.intent.enabled = over.intent.enabled; - 3967
} - 3968
if over.intent.accept_confidence.is_some() { - 3969
base.intent.accept_confidence = over.intent.accept_confidence; - 3970
} - 3971
if over.intent.provisional_confidence.is_some() { - 3972
base.intent.provisional_confidence = over.intent.provisional_confidence; - 3973
} - 3974
if over.intent.slice_capabilities.is_some() { - 3975
base.intent.slice_capabilities = over.intent.slice_capabilities; - 3976
} - 3977
if over.intent.posture.is_some() { - 3978
base.intent.posture = over.intent.posture; - 3979
} - 3980
if over.intent.escalate.is_some() { - 3981
base.intent.escalate = over.intent.escalate; - 3982
} - 3983
if over.intent.classify_model.is_some() { - 3984
base.intent.classify_model = over.intent.classify_model; - 3985
} - 3986
if over.intent.max_classify_usd.is_some() { - 3987
base.intent.max_classify_usd = over.intent.max_classify_usd; - 3988
} - 3989
if over.intent.classify_timeout_secs.is_some() { - 3990
base.intent.classify_timeout_secs = over.intent.classify_timeout_secs; - 3991
} - 3992
if over.intent.autonomy.is_some() { - 3993
base.intent.autonomy = over.intent.autonomy; - 3994
} - 3995
if over.intent.evidence_max_age_secs.is_some() { - 3996
base.intent.evidence_max_age_secs = over.intent.evidence_max_age_secs; - 3997
} - 3998
if over.commitment.enabled.is_some() { - 3999
base.commitment.enabled = over.commitment.enabled; - 4000
} - 4001
if over.commitment.lifetime_budget_usd.is_some() { - 4002
base.commitment.lifetime_budget_usd = over.commitment.lifetime_budget_usd; - 4003
} - 4004
if over.commitment.stall_limit.is_some() { - 4005
base.commitment.stall_limit = over.commitment.stall_limit; - 4006
} - 4007
if over.commitment.review_every_hours.is_some() { - 4008
base.commitment.review_every_hours = over.commitment.review_every_hours; - 4009
} - 4010
if over.commitment.default_ttl_days.is_some() { - 4011
base.commitment.default_ttl_days = over.commitment.default_ttl_days; - 4012
} - 4013
for h in over.route.quality_hints { - 4014
if !base.route.quality_hints.contains(&h) { - 4015
base.route.quality_hints.push(h); - 4016
} - 4017
} - 4018
for h in over.route.modality_hints { - 4019
if !base.route.modality_hints.contains(&h) { - 4020
base.route.modality_hints.push(h); - 4021
} - 4022
} - 4023
if over.automation.catch_up_missed.is_some() { - 4024
base.automation.catch_up_missed = over.automation.catch_up_missed; - 4025
} - 4026
if over.update.url.is_some() { - 4027
base.update.url = over.update.url; - 4028
} - 4029
if over.update.interval_hours.is_some() { - 4030
base.update.interval_hours = over.update.interval_hours; - 4031
} - 4032
if over.tools.web_fetch.is_some() { - 4033
base.tools.web_fetch = over.tools.web_fetch; - 4034
} - 4035
if over.tools.browse.is_some() { - 4036
base.tools.browse = over.tools.browse; - 4037
} - 4038
if over.heartbeat.enabled.is_some() { - 4039
base.heartbeat.enabled = over.heartbeat.enabled; - 4040
} - 4041
if over.heartbeat.interval_secs.is_some() { - 4042
base.heartbeat.interval_secs = over.heartbeat.interval_secs; - 4043
} - 4044
if over.heartbeat.model.is_some() { - 4045
base.heartbeat.model = over.heartbeat.model; - 4046
} - 4047
if over.heartbeat.quiet_hours.is_some() { - 4048
base.heartbeat.quiet_hours = over.heartbeat.quiet_hours; - 4049
} - 4050
if over.heartbeat.max_findings.is_some() { - 4051
base.heartbeat.max_findings = over.heartbeat.max_findings; - 4052
} - 4053
for (name, hook) in over.gateway.outbound.webhooks { - 4054
base.gateway.outbound.webhooks.insert(name, hook); - 4055
} - 4056
if over.feeds.enabled.is_some() { - 4057
base.feeds.enabled = over.feeds.enabled; - 4058
} - 4059
if over.feeds.config_path.is_some() { - 4060
base.feeds.config_path = over.feeds.config_path; - 4061
} - 4062
if over.feeds.db_path.is_some() { - 4063
base.feeds.db_path = over.feeds.db_path; - 4064
} - 4065
if over.feeds.default_check_interval.is_some() { - 4066
base.feeds.default_check_interval = over.feeds.default_check_interval; - 4067
} - 4068
if over.feeds.max_items_per_feed.is_some() { - 4069
base.feeds.max_items_per_feed = over.feeds.max_items_per_feed; - 4070
} - 4071
if over.feeds.dedup_window_days.is_some() { - 4072
base.feeds.dedup_window_days = over.feeds.dedup_window_days; - 4073
} - 4074
for p in over.plugins.enabled { - 4075
if !base.plugins.enabled.contains(&p) { - 4076
base.plugins.enabled.push(p); - 4077
} - 4078
} - 4079
for p in over.plugins.disabled { - 4080
if !base.plugins.disabled.contains(&p) { - 4081
base.plugins.disabled.push(p); - 4082
} - 4083
} - 4084
if over.plugins.allow.is_some() { - 4085
base.plugins.allow = over.plugins.allow; - 4086
} - 4087
for p in over.plugins.deny { - 4088
if !base.plugins.deny.contains(&p) { - 4089
base.plugins.deny.push(p); - 4090
} - 4091
} - 4092
if over.plugins.network_allow.is_some() { - 4093
base.plugins.network_allow = over.plugins.network_allow; - 4094
} - 4095
for p in over.plugins.network_deny { - 4096
if !base.plugins.network_deny.contains(&p) { - 4097
base.plugins.network_deny.push(p); - 4098
} - 4099
} - 4100
for (k, v) in over.profiles { - 4101
base.profiles.insert(k, v); - 4102
} - 4103
} - 4104
- 4105
/// Process-wide extra environment sourced from .env files. Real - 4106
/// environment variables always take precedence. - 4107
type ExtraMap = std::collections::BTreeMap<String, String>; - 4108
- 4109
fn dotenv_extra() -> std::sync::MutexGuard<'static, ExtraMap> { - 4110
static EXTRA: std::sync::OnceLock<std::sync::Mutex<ExtraMap>> = std::sync::OnceLock::new(); - 4111
EXTRA - 4112
.get_or_init(|| std::sync::Mutex::new(ExtraMap::new())) - 4113
.lock() - 4114
.unwrap_or_else(std::sync::PoisonError::into_inner) - 4115
} - 4116
- 4117
/// Loads a scope's stored secrets (docs/design/44-shared-config.md, - 4118
/// "Secrets Chain") into the extra-env table. `path` is a scope hint — the - 4119
/// directory that used to hold a literal `.env` file — not a file read - 4120
/// directly; see [`credentials`]. Existing real environment variables are - 4121
/// never overridden. Only the encrypted-file credential backend can - 4122
/// enumerate a scope's contents; an OS-native secret service is reached by - 4123
/// point lookup only, so bulk-seeding this cache has no effect there (see
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.