- 2769
if let Some(c) = merged.circuit_breaker_cooldown_secs { - 2770
cfg.circuit_breaker_cooldown_secs = c; - 2771
} - 2772
if let Some(w) = merged.context_window { - 2773
if w < 16_384 { - 2774
cfg.warnings.push(format!( - 2775
"context_window {w} too small; using default {}", - 2776
cfg.context_window - 2777
)); - 2778
} else { - 2779
cfg.context_window = w; - 2780
} - 2781
} - 2782
cfg.voice = merged.voice.unwrap_or_default(); - 2783
cfg.hooks = merged.hooks; - 2784
for (name, srv) in merged.mcp.servers { - 2785
cfg.mcp.servers.insert(name, srv); - 2786
} - 2787
cfg.capabilities = CapabilityInheritanceResolved { - 2788
inherit_mcp: merged.capabilities.inherit_mcp.unwrap_or(true), - 2789
inherit_hooks: merged.capabilities.inherit_hooks.unwrap_or(true), - 2790
inherit_skills: merged.capabilities.inherit_skills.unwrap_or(true), - 2791
inherit_commands: merged.capabilities.inherit_commands.unwrap_or(true), - 2792
inherit_plugins: merged.capabilities.inherit_plugins.unwrap_or(true), - 2793
}; - 2794
cfg.ui.theme = merged.ui.theme.clone().unwrap_or_else(|| "dark".into()); - 2795
let builtin = matches!( - 2796
cfg.ui.theme.as_str(), - 2797
"dark" - 2798
| "light" - 2799
| "neo" - 2800
| "rich" - 2801
| "teenage" - 2802
| "plain" - 2803
| "midnight" - 2804
| "synthwave" - 2805
| "forest" - 2806
); - 2807
if !builtin && !merged.ui.themes.contains_key(&cfg.ui.theme) { - 2808
cfg.warnings - 2809
.push(format!("unknown ui.theme '{}'; using 'dark'", cfg.ui.theme)); - 2810
cfg.ui.theme = "dark".into(); - 2811
} - 2812
cfg.ui.bell = merged.ui.bell.unwrap_or(true); - 2813
cfg.ui.keymap = merged.ui.keymap; - 2814
for (name, colors) in merged.ui.themes { - 2815
let entry = cfg.ui.themes.entry(name.clone()).or_default(); - 2816
for (key, value) in colors { - 2817
match value.as_str() { - 2818
Some(v) => { - 2819
entry.insert(key, v.to_string()); - 2820
} - 2821
None => cfg.warnings.push(format!( - 2822
"ui.themes.{name}.{key} must be a string color (ignored)" - 2823
)), - 2824
} - 2825
} - 2826
} - 2827
cfg.ui.composer = match merged.ui.composer.as_deref() { - 2828
Some("vim") => "vim".into(), - 2829
Some("emacs") | None => "emacs".into(), - 2830
Some(other) => { - 2831
cfg.warnings - 2832
.push(format!("unknown ui.composer '{other}'; using 'emacs'")); - 2833
"emacs".into() - 2834
} - 2835
}; - 2836
cfg.ui.osc52 = merged.ui.osc52.unwrap_or(false); - 2837
let acc = merged.ui.accessibility.unwrap_or_default(); - 2838
cfg.ui.accessibility = AccessibilityResolved { - 2839
plain: acc.plain.unwrap_or(false), - 2840
reduced_motion: acc.reduced_motion.unwrap_or(false), - 2841
screen_reader: acc.screen_reader.unwrap_or(false), - 2842
}; - 2843
- 2844
let sp = merged.stop_policy.unwrap_or_default(); - 2845
cfg.stop_policy = StopPolicyResolved { - 2846
enabled: sp.enabled.unwrap_or(true), - 2847
marker_gate: sp.marker_gate.unwrap_or(true), - 2848
verify_gate: sp.verify_gate.unwrap_or(true), - 2849
max_blocks: sp.max_blocks.unwrap_or(2), - 2850
}; - 2851
- 2852
cfg.finops = FinopsResolved { - 2853
max_run_usd: merged.finops.max_run_usd, - 2854
max_day_usd: merged.finops.max_day_usd, - 2855
price_overrides: merged.finops.price_overrides.clone(), - 2856
}; - 2857
- 2858
cfg.goal = GoalResolved { - 2859
handoff_reset: merged.goal.handoff_reset.unwrap_or(true), - 2860
max_audit_blocks: merged.goal.max_audit_blocks.unwrap_or(2), - 2861
}; - 2862
- 2863
cfg.work = WorkResolved { - 2864
enabled: merged.work.enabled.unwrap_or(true), - 2865
default_mode: match merged.work.default_mode.as_deref() { - 2866
None | Some("direct") => "direct".into(), - 2867
Some("managed") => "managed".into(), - 2868
Some("auto") => "auto".into(), - 2869
Some(other) => { - 2870
cfg.warnings.push(format!( - 2871
"unknown work.default_mode '{other}'; using 'direct'" - 2872
)); - 2873
"direct".into() - 2874
} - 2875
}, - 2876
max_items: merged.work.max_items.unwrap_or(20).clamp(1, 128), - 2877
max_revisions: merged.work.max_revisions.unwrap_or(8).clamp(1, 64), - 2878
max_parallel: merged.work.max_parallel.unwrap_or(4).clamp(1, 32), - 2879
confirmation: match merged.work.confirmation.as_deref() { - 2880
None | Some("risk-based") => "risk-based".into(), - 2881
Some("always") => "always".into(), - 2882
Some("never") => "never".into(), - 2883
Some(other) => { - 2884
cfg.warnings.push(format!( - 2885
"unknown work.confirmation '{other}'; using 'risk-based'" - 2886
)); - 2887
"risk-based".into() - 2888
} - 2889
}, - 2890
}; - 2891
- 2892
cfg.route.objective = match merged.route.objective.as_deref() { - 2893
None | Some("auto") => "auto".into(), - 2894
Some("utility") => "utility".into(), - 2895
Some("balanced") => "balanced".into(), - 2896
Some("quality-critical") | Some("quality_critical") => "quality-critical".into(), - 2897
Some(other) => { - 2898
cfg.warnings.push(format!( - 2899
"unknown route.objective '{other}'; using 'auto' \ - 2900
(valid: auto | utility | balanced | quality-critical)" - 2901
)); - 2902
"auto".into() - 2903
} - 2904
}; - 2905
cfg.route.fallback_models = merged.route.fallback_models.clone(); - 2906
cfg.route.max_fallbacks = merged.route.max_fallbacks.unwrap_or(4).clamp(1, 16); - 2907
cfg.route.quality_hints = merged - 2908
.route - 2909
.quality_hints - 2910
.iter() - 2911
.map(|h| h.to_ascii_lowercase()) - 2912
.collect(); - 2913
cfg.route.modality_hints = merged - 2914
.route - 2915
.modality_hints - 2916
.iter() - 2917
.map(|h| h.to_ascii_lowercase()) - 2918
.collect(); - 2919
- 2920
cfg.probe.hosted = match merged.probe.hosted.as_deref() { - 2921
None | Some("none") => "none".into(), - 2922
Some("full") => "full".into(), - 2923
Some(other) => { - 2924
cfg.warnings.push(format!( - 2925
"unknown probe.hosted '{other}'; using 'none' (valid: none | full)" - 2926
)); - 2927
"none".into() - 2928
} - 2929
}; - 2930
- 2931
// --- providers.ollama (docs/design/68-context-engine.md §8) --- - 2932
match merged.providers.ollama.validate() { - 2933
Ok(()) => { - 2934
if let Some(keep_alive) = merged.providers.ollama.keep_alive.clone() { - 2935
cfg.ollama.keep_alive = keep_alive; - 2936
} - 2937
cfg.ollama.num_ctx = merged.providers.ollama.num_ctx; - 2938
} - 2939
Err(msg) => cfg.warnings.push(msg), - 2940
} - 2941
- 2942
// --- providers.anthropic (docs/design/68-context-engine.md §11) --- - 2943
cfg.anthropic.fast_mode = merged.providers.anthropic.fast_mode.unwrap_or(false); - 2944
- 2945
// --- intent kernel (docs/design/47-commitment-kernel.md) --- - 2946
cfg.intent.enabled = merged.intent.enabled.unwrap_or(true); - 2947
// Clamped rather than rejected: a nonsensical threshold should not stop - 2948
// the runtime, and the clamp keeps the ordering invariant that - 2949
// `provisional <= accept` even if an operator inverts them. TOML can - 2950
// spell `nan` and `inf`, and `clamp` passes NaN straight through, where - 2951
// every comparison against it is false — so a non-finite value is - 2952
// replaced by the default before it is clamped. - 2953
let mut finite = |value: Option<f64>, default: f64, key: &str| match value { - 2954
Some(value) if !value.is_finite() => { - 2955
cfg.warnings.push(format!( - 2956
"{key} = {value} is not a finite number; using {default}" - 2957
)); - 2958
default - 2959
} - 2960
other => other.unwrap_or(default), - 2961
}; - 2962
let accept = finite( - 2963
merged.intent.accept_confidence, - 2964
0.75, - 2965
"intent.accept_confidence", - 2966
); - 2967
let provisional = finite( - 2968
merged.intent.provisional_confidence, - 2969
0.45, - 2970
"intent.provisional_confidence", - 2971
); - 2972
let max_classify = finite( - 2973
merged.intent.max_classify_usd, - 2974
0.01, - 2975
"intent.max_classify_usd", - 2976
); - 2977
let lifetime_budget = merged - 2978
.commitment - 2979
.lifetime_budget_usd - 2980
.map(|budget| finite(Some(budget), 0.0, "commitment.lifetime_budget_usd")); - 2981
cfg.intent.accept_confidence = accept.clamp(0.0, 1.0); - 2982
cfg.intent.provisional_confidence = provisional.clamp(0.0, cfg.intent.accept_confidence); - 2983
cfg.intent.slice_capabilities = merged.intent.slice_capabilities.unwrap_or(true); - 2984
cfg.intent.posture = merged.intent.posture.unwrap_or(true); - 2985
cfg.intent.escalate = match merged.intent.escalate.as_deref() { - 2986
Some("none") | None => "none".into(), - 2987
Some("local") => "local".into(), - 2988
Some("cloud") => "cloud".into(), - 2989
Some(other) => { - 2990
cfg.warnings.push(format!( - 2991
"unknown intent.escalate '{other}'; using 'none' \ - 2992
(valid: none, local, cloud)" - 2993
)); - 2994
"none".into() - 2995
} - 2996
}; - 2997
cfg.intent.classify_model = merged - 2998
.intent - 2999
.classify_model - 3000
.clone() - 3001
.filter(|model| !model.trim().is_empty()); - 3002
cfg.intent.max_classify_usd = max_classify.clamp(0.0, 1.0); - 3003
cfg.intent.classify_timeout_secs = merged - 3004
.intent - 3005
.classify_timeout_secs - 3006
.unwrap_or(10) - 3007
.clamp(1, 120); - 3008
cfg.intent.autonomy = match merged.intent.autonomy.as_deref() { - 3009
Some("manual") => "manual".into(), - 3010
Some("assisted") | None => "assisted".into(), - 3011
Some("delegated") => "delegated".into(), - 3012
Some("autonomous") => "autonomous".into(), - 3013
Some(other) => { - 3014
cfg.warnings.push(format!( - 3015
"unknown intent.autonomy '{other}'; using 'assisted' \ - 3016
(valid: manual, assisted, delegated, autonomous)" - 3017
)); - 3018
"assisted".into() - 3019
} - 3020
}; - 3021
cfg.intent.evidence_max_age_secs = merged.intent.evidence_max_age_secs.unwrap_or(86_400).max(0); - 3022
- 3023
// --- durable commitments --- - 3024
cfg.commitment.enabled = merged.commitment.enabled.unwrap_or(true); - 3025
cfg.commitment.lifetime_budget_usd = lifetime_budget.filter(|budget| *budget > 0.0); - 3026
cfg.commitment.stall_limit = merged.commitment.stall_limit.unwrap_or(3).clamp(1, 100); - 3027
cfg.commitment.review_every_hours = merged - 3028
.commitment - 3029
.review_every_hours - 3030
.filter(|hours| *hours > 0); - 3031
cfg.commitment.default_ttl_days = merged.commitment.default_ttl_days.filter(|days| *days > 0); - 3032
- 3033
cfg.gateway.enabled = merged.gateway.enabled.unwrap_or(false); - 3034
cfg.gateway.approvals = match merged.gateway.approvals.as_deref() { - 3035
Some("deny") | None => "deny".into(), - 3036
Some("forward") => "forward".into(), - 3037
Some(other) => { - 3038
cfg.warnings - 3039
.push(format!("unknown gateway.approvals '{other}'; using 'deny'")); - 3040
"deny".into() - 3041
} - 3042
}; - 3043
cfg.gateway.approver = merged.gateway.approver.clone(); - 3044
if cfg.gateway.approvals == "forward" { - 3045
let ok = cfg - 3046
.gateway - 3047
.approver - 3048
.as_deref() - 3049
.is_some_and(|t| t.contains(':') && !t.trim().is_empty()); - 3050
if !ok { - 3051
cfg.warnings.push( - 3052
"gateway.approvals = 'forward' requires gateway.approver = '<surface>:<chat>'; \ - 3053
falling back to 'deny'" - 3054
.into(), - 3055
); - 3056
cfg.gateway.approvals = "deny".into(); - 3057
cfg.gateway.approver = None; - 3058
} - 3059
} - 3060
match merged.gateway.approval_timeout_secs { - 3061
Some(t) if t < 5 => { - 3062
cfg.warnings.push(format!( - 3063
"gateway.approval_timeout_secs {t} below minimum; using 5" - 3064
)); - 3065
cfg.gateway.approval_timeout_secs = 5; - 3066
} - 3067
Some(t) => cfg.gateway.approval_timeout_secs = t, - 3068
None => {} - 3069
} - 3070
cfg.gateway.rate_limit = merged.gateway.rate_limit.clone(); - 3071
cfg.gateway.chat_allowlist = merged.gateway.chat_allowlist.clone(); - 3072
cfg.gateway.chat_allowlist_open = merged.gateway.chat_allowlist_open.unwrap_or(false); - 3073
cfg.gateway.core_pool_max = merged.gateway.core_pool_max.unwrap_or(8).max(1); - 3074
cfg.gateway.core_pool_idle_secs = merged.gateway.core_pool_idle_secs.unwrap_or(1800).max(60); - 3075
cfg.gateway.pending_expiry_days = merged.gateway.pending_expiry_days.unwrap_or(7).max(1); - 3076
if cfg.gateway.enabled - 3077
&& cfg.gateway.chat_allowlist.is_empty() - 3078
&& cfg.gateway.chat_allowlist_open - 3079
{ - 3080
cfg.warnings.push( - 3081
"gateway.chat_allowlist is empty and gateway.chat_allowlist_open = true: \ - 3082
every inbound chat is accepted. Set gateway.chat_allowlist to restrict access." - 3083
.into(), - 3084
); - 3085
} - 3086
cfg.memory.search_enabled = merged.memory.search_enabled.unwrap_or(true); - 3087
cfg.memory.write_enabled = merged.memory.write_enabled.unwrap_or(true); - 3088
cfg.memory.skill_proposals = merged.memory.skill_proposals.unwrap_or(true); - 3089
cfg.memory.reflection = merged.memory.reflection.unwrap_or(false); - 3090
cfg.sandbox.backend = match merged.sandbox.backend.as_deref() { - 3091
None => "auto".into(), - 3092
Some(b @ ("auto" | "seatbelt" | "landlock" | "docker")) => b.into(), - 3093
Some(other) => { - 3094
cfg.warnings - 3095
.push(format!("unknown sandbox.backend '{other}'; using 'auto'")); - 3096
"auto".into() - 3097
} - 3098
}; - 3099
cfg.sandbox.image = merged.sandbox.image.clone(); - 3100
cfg.automation.catch_up_missed = merged.automation.catch_up_missed.unwrap_or(true); - 3101
cfg.update.url = merged.update.url.clone(); - 3102
cfg.update.interval_hours = merged.update.interval_hours.unwrap_or(24); - 3103
cfg.tools.web_fetch = merged.tools.web_fetch.unwrap_or(true); - 3104
cfg.tools.browse = merged.tools.browse.unwrap_or(true); - 3105
let hb = &merged.heartbeat; - 3106
cfg.heartbeat.interval_secs = match hb.interval_secs { - 3107
Some(v) if v < 300 => { - 3108
cfg.warnings.push(format!( - 3109
"heartbeat.interval_secs {v} below minimum; using 300" - 3110
)); - 3111
300 - 3112
} - 3113
Some(v) => v, - 3114
None => 1800, - 3115
}; - 3116
cfg.heartbeat.enabled = hb.enabled.unwrap_or(false); - 3117
cfg.heartbeat.model = hb.model.clone(); - 3118
cfg.heartbeat.quiet_hours = match hb.quiet_hours.as_deref() { - 3119
None => None, - 3120
Some(raw) => match QuietWindow::parse(raw) { - 3121
Some(w) => Some(w), - 3122
None => { - 3123
cfg.warnings.push(format!( - 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;
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.