- 842
pub default_check_interval: String, - 843
pub max_items_per_feed: u32, - 844
pub dedup_window_days: u32, - 845
} - 846
- 847
#[derive(Debug, Clone, Deserialize, PartialEq)] - 848
pub struct PriceEntry { - 849
pub input: f64, - 850
pub output: f64, - 851
} - 852
- 853
#[derive(Debug, Clone, Deserialize, Default)] - 854
pub struct McpConfig { - 855
#[serde(default)] - 856
pub servers: std::collections::BTreeMap<String, McpServerConfig>, - 857
} - 858
- 859
#[derive(Debug, Clone, Serialize, Deserialize)] - 860
pub struct McpServerConfig { - 861
pub command: String, - 862
#[serde(default)] - 863
pub args: Vec<String>, - 864
#[serde(default)] - 865
pub env: std::collections::BTreeMap<String, String>, - 866
/// Allow outbound network for this MCP server. Privileged (mcp.servers - 867
/// is stripped from untrusted projects). - 868
#[serde(default)] - 869
pub network: bool, - 870
/// What this server is for, in its own words: `serves = ["live-data"]`. - 871
/// - 872
/// Optional, and deliberately so. It decides whether a call to this - 873
/// server counts as retrieval that an answer must be grounded in; an - 874
/// empty list means *undeclared*, and the server inherits the `mcp` - 875
/// broker's web/live-data claim. It never hides the server from a turn. - 876
/// The alternative, guessing a domain from the server's tool names, - 877
/// would put a keyword table back in the harness and reintroduce the - 878
/// coupling this field exists to remove. - 879
/// - 880
/// Skipped when empty so the config file and the management API keep - 881
/// exactly the shape they had before this field existed — a server that - 882
/// declares nothing should look no different from one written last year. - 883
#[serde(default, skip_serializing_if = "Vec::is_empty")] - 884
pub serves: Vec<String>, - 885
} - 886
- 887
/// Project-layer switches for severing one inherited capability category. - 888
/// Missing means inherit. User-layer values are accepted but only become - 889
/// meaningful when a narrower layer is merged over them. - 890
#[derive(Debug, Clone, Deserialize, Default)] - 891
#[serde(default)] - 892
pub struct CapabilityInheritanceSettings { - 893
pub inherit_mcp: Option<bool>, - 894
pub inherit_hooks: Option<bool>, - 895
pub inherit_skills: Option<bool>, - 896
pub inherit_commands: Option<bool>, - 897
pub inherit_plugins: Option<bool>, - 898
} - 899
- 900
#[derive(Debug, Clone)] - 901
pub struct CapabilityInheritanceResolved { - 902
pub inherit_mcp: bool, - 903
pub inherit_hooks: bool, - 904
pub inherit_skills: bool, - 905
pub inherit_commands: bool, - 906
pub inherit_plugins: bool, - 907
} - 908
- 909
/// Global and workspace settings governing capability plugins. - 910
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)] - 911
#[serde(default)] - 912
pub struct PluginSettings { - 913
/// Explicitly enabled plugin names. - 914
pub enabled: Vec<String>, - 915
/// Explicitly disabled plugin names. - 916
pub disabled: Vec<String>, - 917
/// Optional allowlist of permitted plugin names. If specified, only matching plugins may be enabled. - 918
pub allow: Option<Vec<String>>, - 919
/// Denylist of forbidden plugin names. Deny always takes precedence over allow. - 920
pub deny: Vec<String>, - 921
/// Plugins permitted outbound network access. Privileged. - 922
pub network_allow: Option<Vec<String>>, - 923
/// Plugins forbidden outbound network access. Precedence: an entry - 924
/// here always beats `network_allow` (channel `plugins_network_deny` - 925
/// layers on top of both and can only take egress away). - 926
pub network_deny: Vec<String>, - 927
} - 928
- 929
/// Resolved plugin policy across global and workspace layers. - 930
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] - 931
pub struct PluginResolved { - 932
pub enabled: Vec<String>, - 933
pub disabled: Vec<String>, - 934
pub allow: Option<Vec<String>>, - 935
pub deny: Vec<String>, - 936
pub network_allow: Option<Vec<String>>, - 937
pub network_deny: Vec<String>, - 938
} - 939
- 940
impl PluginResolved { - 941
pub fn is_enabled(&self, name: &str) -> bool { - 942
// Deny always wins - 943
if self.deny.iter().any(|d| d == name || d == "*") { - 944
return false; - 945
} - 946
if self.disabled.iter().any(|d| d == name) { - 947
return false; - 948
} - 949
if let Some(allow) = &self.allow { - 950
return allow.iter().any(|a| a == name || a == "*"); - 951
} - 952
if !self.enabled.is_empty() { - 953
return self.enabled.iter().any(|e| e == name || e == "*"); - 954
} - 955
true - 956
} - 957
- 958
pub fn is_network_allowed(&self, name: &str) -> bool { - 959
if !self.is_enabled(name) { - 960
return false; - 961
} - 962
if self.network_deny.iter().any(|d| d == name || d == "*") { - 963
return false; - 964
} - 965
if let Some(allow) = &self.network_allow { - 966
return allow.iter().any(|a| a == name || a == "*"); - 967
} - 968
false - 969
} - 970
} - 971
- 972
/// Restrictive capability overlay for a gateway channel. `None` means inherit - 973
/// the workspace policy; `Some([])` means deny everything in that category. - 974
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] - 975
#[serde(default)] - 976
pub struct ChannelPolicy { - 977
pub tools_allow: Option<Vec<String>>, - 978
pub tools_deny: Vec<String>, - 979
pub mcp_allow: Option<Vec<String>>, - 980
pub mcp_deny: Vec<String>, - 981
pub skills_allow: Option<Vec<String>>, - 982
pub skills_deny: Vec<String>, - 983
pub hooks_allow: Option<Vec<String>>, - 984
pub hooks_deny: Vec<String>, - 985
/// Server-name patterns (matched the same way as `mcp_allow`/`mcp_deny`) - 986
/// for which this channel forces outbound network off, even when the - 987
/// server's own `McpServerConfig.network` is `true`. Restrictive only — - 988
/// there is deliberately no matching "network_allow": a channel can - 989
/// only take network access away from a server it can already reach, - 990
/// never grant it to one the server config itself denies. - 991
pub mcp_network_deny: Vec<String>, - 992
/// Plugin-name patterns for which this channel forces outbound network off. - 993
pub plugins_network_deny: Vec<String>, - 994
/// Optional allowlist of plugin names permitted on this channel. - 995
pub plugins_allow: Option<Vec<String>>, - 996
/// Denylist of plugin names forbidden on this channel. - 997
pub plugins_deny: Vec<String>, - 998
/// Autonomy ceiling for this channel (docs/design/47-commitment-kernel.md). - 999
/// - 1000
/// **Restrictive only**, like everything else on this type: a channel may - 1001
/// cap delegation below what the workspace granted, never raise it. That - 1002
/// asymmetry is the point — a Telegram chat should be able to say "propose - 1003
/// only, in here", and must never be able to say "act freely" on a - 1004
/// workspace whose operator did not. - 1005
/// - 1006
/// `None` inherits. Values: `manual` | `assisted` | `delegated` | - 1007
/// `autonomous`. - 1008
pub autonomy_ceiling: Option<String>, - 1009
} - 1010
- 1011
/// Rank an autonomy name, mirroring `vak_intent::Autonomy::rank`. - 1012
/// - 1013
/// Duplicated rather than imported because `vak-config` deliberately does not - 1014
/// depend on the intent kernel; the ranking is asserted equal by a test in - 1015
/// `vak-core`, which sees both. - 1016
fn autonomy_rank(name: &str) -> u8 { - 1017
match name { - 1018
"manual" => 0, - 1019
"assisted" => 1, - 1020
"delegated" => 2, - 1021
"autonomous" => 3, - 1022
_ => 1, - 1023
} - 1024
} - 1025
- 1026
impl ChannelPolicy { - 1027
/// The least-delegated of two autonomy ceilings. `None` on either side - 1028
/// means "says nothing", not "allows everything". - 1029
pub fn cap_autonomy(lower: Option<&str>, higher: Option<&str>) -> Option<String> { - 1030
match (lower, higher) { - 1031
(None, None) => None, - 1032
(Some(one), None) | (None, Some(one)) => Some(one.to_string()), - 1033
(Some(a), Some(b)) => Some( - 1034
if autonomy_rank(b) < autonomy_rank(a) { - 1035
b - 1036
} else { - 1037
a - 1038
} - 1039
.to_string(), - 1040
), - 1041
} - 1042
} - 1043
- 1044
/// Fold a lower tier (e.g. bot) and a higher tier (e.g. chat) into the - 1045
/// single effective policy applied at dispatch. Restrictive-only: an - 1046
/// `_allow` list from the higher tier wins outright when present (it is - 1047
/// itself already capped against whatever it's allowed to name), a - 1048
/// missing `_allow` falls back to the lower tier's, and `_deny` lists - 1049
/// concatenate across tiers since denies only ever remove, never add, - 1050
/// access. `lower` is the more permissive default (bot), `higher` is - 1051
/// the more specific override (chat). - 1052
pub fn merge(lower: &ChannelPolicy, higher: &ChannelPolicy) -> ChannelPolicy { - 1053
fn merge_allow( - 1054
lower: &Option<Vec<String>>, - 1055
higher: &Option<Vec<String>>, - 1056
) -> Option<Vec<String>> { - 1057
higher.clone().or_else(|| lower.clone()) - 1058
} - 1059
fn merge_deny(lower: &[String], higher: &[String]) -> Vec<String> { - 1060
let mut out = lower.to_vec(); - 1061
for item in higher { - 1062
if !out.contains(item) { - 1063
out.push(item.clone()); - 1064
} - 1065
} - 1066
out - 1067
} - 1068
ChannelPolicy { - 1069
tools_allow: merge_allow(&lower.tools_allow, &higher.tools_allow), - 1070
tools_deny: merge_deny(&lower.tools_deny, &higher.tools_deny), - 1071
mcp_allow: merge_allow(&lower.mcp_allow, &higher.mcp_allow), - 1072
mcp_deny: merge_deny(&lower.mcp_deny, &higher.mcp_deny), - 1073
skills_allow: merge_allow(&lower.skills_allow, &higher.skills_allow), - 1074
skills_deny: merge_deny(&lower.skills_deny, &higher.skills_deny), - 1075
hooks_allow: merge_allow(&lower.hooks_allow, &higher.hooks_allow), - 1076
hooks_deny: merge_deny(&lower.hooks_deny, &higher.hooks_deny), - 1077
mcp_network_deny: merge_deny(&lower.mcp_network_deny, &higher.mcp_network_deny), - 1078
plugins_network_deny: merge_deny( - 1079
&lower.plugins_network_deny, - 1080
&higher.plugins_network_deny, - 1081
), - 1082
plugins_allow: merge_allow(&lower.plugins_allow, &higher.plugins_allow), - 1083
plugins_deny: merge_deny(&lower.plugins_deny, &higher.plugins_deny), - 1084
autonomy_ceiling: Self::cap_autonomy( - 1085
lower.autonomy_ceiling.as_deref(), - 1086
higher.autonomy_ceiling.as_deref(), - 1087
), - 1088
} - 1089
} - 1090
} - 1091
- 1092
/// Optional spoken voice + persona for a bot/chat, resolved through the - 1093
/// same bot→chat inheritance idiom as `route`/`permission_mode` (see - 1094
/// `GatewayState::core_for_entry` in vak-server::gateway). `None` on a - 1095
/// field means "no override for that piece"; the whole `VoiceConfig` being - 1096
/// `None` on the entity means "inherit the parent tier's voice entirely". - 1097
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] - 1098
pub struct VoiceConfig { - 1099
/// Provider override for voice operations at this scope. - 1100
#[serde(default, skip_serializing_if = "Option::is_none")] - 1101
pub provider: Option<String>, - 1102
/// Live API prebuilt voice name, e.g. "Kore", "Puck", "Zephyr". - 1103
#[serde(default, skip_serializing_if = "Option::is_none")] - 1104
pub voice_name: Option<String>, - 1105
/// Provider model override for transcription at this scope. - 1106
#[serde(default, skip_serializing_if = "Option::is_none")] - 1107
pub transcription_model: Option<String>, - 1108
/// Provider model override for synthesis at this scope. - 1109
#[serde(default, skip_serializing_if = "Option::is_none")] - 1110
pub synthesis_model: Option<String>, - 1111
/// **Deprecated** (docs/design/45-prompt-layers.md): the bot/chat - 1112
/// `identity` prompt block is the persona now, so a bot's spoken and - 1113
/// written selves cannot drift apart. Still read as a fallback when no - 1114
/// prompt tier sets an identity, and still honoured as an explicit - 1115
/// per-request override, so existing configs keep working. New writes - 1116
/// should set the `identity` block instead. - 1117
#[serde(default, skip_serializing_if = "Option::is_none")] - 1118
pub persona: Option<String>, - 1119
} - 1120
- 1121
impl VoiceConfig { - 1122
/// Overlay a narrower scope onto its parent. Missing fields inherit. - 1123
pub fn overlay(parent: Option<&Self>, child: &Self) -> Self { - 1124
Self { - 1125
provider: child - 1126
.provider - 1127
.clone() - 1128
.or_else(|| parent.and_then(|v| v.provider.clone())), - 1129
voice_name: child - 1130
.voice_name - 1131
.clone() - 1132
.or_else(|| parent.and_then(|v| v.voice_name.clone())), - 1133
transcription_model: child - 1134
.transcription_model - 1135
.clone() - 1136
.or_else(|| parent.and_then(|v| v.transcription_model.clone())), - 1137
synthesis_model: child - 1138
.synthesis_model - 1139
.clone() - 1140
.or_else(|| parent.and_then(|v| v.synthesis_model.clone())), - 1141
persona: child - 1142
.persona - 1143
.clone() - 1144
.or_else(|| parent.and_then(|v| v.persona.clone())), - 1145
} - 1146
} - 1147
} - 1148
- 1149
// Note: the `Bot` entity itself (id/surface/label/token_env/policy/ - 1150
// permission_mode/route/workspace) lives in `vak-server::gateway` next to - 1151
// `AllowlistEntry` and `AllowlistRoute`, since it needs `AllowlistRoute` and - 1152
// vak-config must not depend on vak-server. It reuses `ChannelPolicy::merge` - 1153
// and `PermissionMode::capped_by` from here for its slot in the bot → chat → - 1154
// workspace resolution chain. - 1155
- 1156
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - 1157
pub struct HookConfig { - 1158
pub event: String, - 1159
#[serde(rename = "match")] - 1160
pub matcher: Option<String>, - 1161
pub command: String, - 1162
pub timeout_ms: Option<u64>, - 1163
/// Missing in an existing `config.toml` means enabled — this field was - 1164
/// added after `[[hooks]]` shipped without one, and every hook written - 1165
/// before it must keep firing. - 1166
#[serde(default = "default_hook_config_enabled")] - 1167
pub enabled: bool, - 1168
#[serde(default)] - 1169
pub failure_mode: Option<String>, - 1170
} - 1171
- 1172
/// Add the built-in disabled automation templates to the Shared layer once. - 1173
/// Existing operator hooks are preserved byte-for-byte in the same atomic - 1174
/// rewrite used by every other persisted configuration mutation. - 1175
pub fn seed_global_hooks_if_empty(hooks: &[HookConfig]) -> Result<bool, ConfigError> { - 1176
let path = global_path().ok_or_else(|| ConfigError::Write { - 1177
path: PathBuf::from("<shared>"), - 1178
source: std::io::Error::other("shared workspace unavailable"), - 1179
})?; - 1180
update_config_file(&path, |document| { - 1181
if document - 1182
.get("hooks") - 1183
.and_then(toml::Value::as_array) - 1184
.is_some_and(|existing| !existing.is_empty()) - 1185
{ - 1186
return Ok(false); - 1187
} - 1188
document.insert("hooks".into(), encode_hooks(&path, hooks)?); - 1189
Ok(true) - 1190
}) - 1191
} - 1192
- 1193
/// Replace the `[[hooks]]` list in the configuration file at `path`, - 1194
/// leaving every other key alone. A disabled hook is written with - 1195
/// `enabled = false`, never dropped (invariant 21). - 1196
pub fn persist_hooks(path: &Path, hooks: &[HookConfig]) -> Result<(), ConfigError> { - 1197
update_config_file(path, |document| { - 1198
document.insert("hooks".into(), encode_hooks(path, hooks)?); - 1199
Ok(()) - 1200
}) - 1201
} - 1202
- 1203
fn encode_hooks(path: &Path, hooks: &[HookConfig]) -> Result<toml::Value, ConfigError> { - 1204
hooks - 1205
.iter() - 1206
.map(toml::Value::try_from) - 1207
.collect::<Result<Vec<_>, _>>() - 1208
.map(toml::Value::Array) - 1209
.map_err(|error| ConfigError::Write { - 1210
path: path.to_path_buf(), - 1211
source: std::io::Error::other(error.to_string()), - 1212
}) - 1213
} - 1214
- 1215
/// Seed the default execution plugin policy into the given config file's - 1216
/// `[plugins] network_allow` table if unconfigured. - 1217
pub fn seed_plugins_network_allow_if_empty(path: &Path) -> Result<bool, ConfigError> { - 1218
update_config_file(path, |document| { - 1219
let plugins = child_table(document, "plugins", path)?; - 1220
if plugins.contains_key("network_allow") { - 1221
return Ok(false); - 1222
} - 1223
plugins.insert("network_allow".into(), toml::Value::Array(Vec::new())); - 1224
Ok(true) - 1225
}) - 1226
} - 1227
- 1228
/// Seed the Shared layer's `[plugins] network_allow` table once if unconfigured. - 1229
pub fn seed_global_plugins_network_allow_if_empty() -> Result<bool, ConfigError> { - 1230
let path = global_path().ok_or_else(|| ConfigError::Write { - 1231
path: PathBuf::from("<shared>"), - 1232
source: std::io::Error::other("shared workspace unavailable"), - 1233
})?; - 1234
seed_plugins_network_allow_if_empty(&path) - 1235
} - 1236
- 1237
/// Remove a plugin name from `[plugins] network_allow` in the config at `path`. - 1238
/// Called during retired-plugin cleanup so the allowlist stays consistent - 1239
/// with the on-disk plugin store. Silently succeeds if the entry, or the - 1240
/// file, was not present. - 1241
pub fn prune_plugins_network_allow(path: &Path, plugin_name: &str) -> Result<bool, ConfigError> { - 1242
update_config_file(path, |document| { - 1243
let Some(allow) = document - 1244
.get_mut("plugins") - 1245
.and_then(toml::Value::as_table_mut) - 1246
.and_then(|plugins| plugins.get_mut("network_allow")) - 1247
.and_then(toml::Value::as_array_mut) - 1248
else { - 1249
return Ok(false); - 1250
}; - 1251
let before = allow.len(); - 1252
allow.retain(|name| name.as_str() != Some(plugin_name)); - 1253
Ok(allow.len() != before) - 1254
}) - 1255
} - 1256
- 1257
fn default_hook_config_enabled() -> bool { - 1258
true - 1259
} - 1260
- 1261
#[derive(Debug, Clone)] - 1262
pub struct Config { - 1263
pub provider: String, - 1264
pub model: String, - 1265
pub max_tokens: u32, - 1266
pub max_turns: usize, - 1267
pub permission_mode: PermissionMode, - 1268
pub approval_mode: ApprovalMode, - 1269
pub anthropic_base_url: Option<String>, - 1270
pub allow: Vec<String>, - 1271
pub ask: Vec<String>, - 1272
pub deny: Vec<String>, - 1273
pub workers: bool, - 1274
pub hooks: Vec<HookConfig>, - 1275
pub max_retries: u32, - 1276
pub retry_base_backoff_ms: u64, - 1277
pub request_timeout_secs: u64, - 1278
pub run_retry_attempts: u32, - 1279
pub run_retry_base_backoff_ms: u64, - 1280
pub circuit_breaker_threshold: u32, - 1281
pub circuit_breaker_cooldown_secs: u64, - 1282
pub context_window: u64, - 1283
pub mcp: McpConfig, - 1284
pub capabilities: CapabilityInheritanceResolved, - 1285
pub ui: UiResolved, - 1286
pub stop_policy: StopPolicyResolved, - 1287
pub gateway: GatewayResolved, - 1288
pub memory: MemoryResolved, - 1289
pub sandbox: SandboxResolved, - 1290
pub finops: FinopsResolved, - 1291
pub goal: GoalResolved, - 1292
pub work: WorkResolved, - 1293
pub route: RouteResolved, - 1294
pub probe: ProbeResolved, - 1295
pub intent: IntentResolved, - 1296
pub commitment: CommitmentResolved, - 1297
pub automation: AutomationResolved, - 1298
pub update: UpdateResolved, - 1299
pub tools: ToolsResolved, - 1300
pub heartbeat: HeartbeatResolved, - 1301
pub feeds: FeedResolved, - 1302
pub server: ServerResolved, - 1303
pub plugins: PluginResolved, - 1304
pub voice: VoiceSettings, - 1305
pub ollama: OllamaResolved, - 1306
pub anthropic: AnthropicResolved, - 1307
pub warnings: Vec<String>, - 1308
} - 1309
- 1310
/// Resolved heartbeat policy (docs/design/29-personal-os.md P7). - 1311
#[derive(Debug, Clone, PartialEq, Eq)] - 1312
pub struct HeartbeatResolved { - 1313
pub enabled: bool, - 1314
pub interval_secs: u64, - 1315
/// Model pin; None keeps the provider's current model. - 1316
pub model: Option<String>, - 1317
/// Parsed quiet window; None means cycles may fire any time. - 1318
pub quiet_hours: Option<QuietWindow>, - 1319
pub max_findings: usize, - 1320
} - 1321
- 1322
/// Local-time quiet window parsed from "HH:MM-HH:MM". `start_min` is - 1323
/// inclusive, `end_min` exclusive, and the window may wrap midnight. - 1324
#[derive(Debug, Clone, Copy, PartialEq, Eq)] - 1325
pub struct QuietWindow { - 1326
pub start_min: u32, - 1327
pub end_min: u32, - 1328
} - 1329
- 1330
impl QuietWindow { - 1331
/// Parses "HH:MM-HH:MM". A zero-length window is rejected as - 1332
/// meaningless rather than treated as empty or full-day. - 1333
pub fn parse(s: &str) -> Option<Self> { - 1334
let (a, b) = s.split_once('-')?; - 1335
let start_min = parse_hhmm(a.trim())?; - 1336
let end_min = parse_hhmm(b.trim())?; - 1337
if start_min == end_min { - 1338
return None; - 1339
} - 1340
Some(QuietWindow { start_min, end_min }) - 1341
} - 1342
- 1343
/// True when `minutes_from_midnight` falls inside the window. The - 1344
/// start bound is inclusive, the end exclusive. - 1345
pub fn contains(&self, minutes_from_midnight: u32) -> bool { - 1346
let t = minutes_from_midnight % (24 * 60); - 1347
if self.start_min < self.end_min { - 1348
t >= self.start_min && t < self.end_min - 1349
} else { - 1350
t >= self.start_min || t < self.end_min - 1351
} - 1352
} - 1353
} - 1354
- 1355
fn parse_hhmm(s: &str) -> Option<u32> { - 1356
let (h, m) = s.split_once(':')?; - 1357
let h: u32 = h.trim().parse().ok()?; - 1358
let m: u32 = m.trim().parse().ok()?; - 1359
if h > 23 || m > 59 { - 1360
return None; - 1361
} - 1362
Some(h * 60 + m) - 1363
} - 1364
- 1365
/// Resolved goal-mode policy (docs/design/42-managed-work-contracts.md). - 1366
#[derive(Debug, Clone, PartialEq, Eq)] - 1367
pub struct GoalResolved { - 1368
/// Reset-with-handoff rescue on still-over contexts. - 1369
pub handoff_reset: bool, - 1370
/// Audit blocks per goal before degrading to Unverified. - 1371
pub max_audit_blocks: u32, - 1372
} - 1373
- 1374
#[derive(Debug, Clone, PartialEq, Eq)] - 1375
pub struct WorkResolved { - 1376
pub enabled: bool, - 1377
pub default_mode: String, - 1378
pub max_items: usize, - 1379
pub max_revisions: u32, - 1380
pub max_parallel: usize, - 1381
pub confirmation: String, - 1382
} - 1383
- 1384
/// Resolved spend-admission policy (docs/design/15-reliability.md). - 1385
#[derive(Debug, Clone, Default, PartialEq)] - 1386
pub struct FinopsResolved { - 1387
pub max_run_usd: Option<f64>, - 1388
pub max_day_usd: Option<f64>, - 1389
pub price_overrides: std::collections::BTreeMap<String, PriceEntry>, - 1390
} - 1391
- 1392
/// Resolved routing policy (Phase R). - 1393
#[derive(Debug, Clone, PartialEq, Eq)] - 1394
pub struct RouteResolved { - 1395
/// "auto" | "utility" | "balanced" | "quality-critical". - 1396
pub objective: String, - 1397
/// Cross-model fallback allowlist (exact model ids). - 1398
pub fallback_models: Vec<String>, - 1399
/// Total ladder length cap including the primary leg. - 1400
pub max_fallbacks: usize, - 1401
/// Frontier-tier model-id substrings (lowercased for matching). - 1402
pub quality_hints: Vec<String>, - 1403
/// Model-id substrings that can serve non-text modalities (lowercased). - 1404
pub modality_hints: Vec<String>, - 1405
} - 1406
- 1407
/// Resolved intent-kernel policy. - 1408
#[derive(Debug, Clone, PartialEq)] - 1409
pub struct IntentResolved { - 1410
pub enabled: bool, - 1411
pub accept_confidence: f64, - 1412
pub provisional_confidence: f64, - 1413
pub slice_capabilities: bool, - 1414
pub posture: bool, - 1415
/// "none" | "local" | "cloud". - 1416
pub escalate: String, - 1417
pub classify_model: Option<String>, - 1418
pub max_classify_usd: f64, - 1419
pub classify_timeout_secs: u64, - 1420
/// Standing delegation for this workspace. - 1421
pub autonomy: String, - 1422
pub evidence_max_age_secs: i64, - 1423
} - 1424
- 1425
/// Resolved durable-commitment policy. - 1426
#[derive(Debug, Clone, PartialEq)] - 1427
pub struct CommitmentResolved { - 1428
pub enabled: bool, - 1429
pub lifetime_budget_usd: Option<f64>, - 1430
pub stall_limit: u32, - 1431
pub review_every_hours: Option<u32>, - 1432
pub default_ttl_days: Option<u32>, - 1433
} - 1434
- 1435
#[derive(Debug, Clone, PartialEq, Eq)] - 1436
pub struct UiResolved { - 1437
/// Built-in theme name, or any name defined in `ui.themes`; unknown - 1438
/// values normalize to "dark". - 1439
pub theme: String, - 1440
pub bell: bool, - 1441
/// Keymap overrides merged project-over-user. - 1442
pub keymap: std::collections::BTreeMap<String, String>, - 1443
pub composer: String, - 1444
pub osc52: bool, - 1445
pub accessibility: AccessibilityResolved, - 1446
/// Custom theme definitions passed through to the UI layer. - 1447
pub themes: std::collections::BTreeMap<String, std::collections::BTreeMap<String, String>>, - 1448
} - 1449
- 1450
#[derive(Debug, Clone, PartialEq, Eq)] - 1451
pub struct AccessibilityResolved { - 1452
pub plain: bool, - 1453
pub reduced_motion: bool, - 1454
pub screen_reader: bool, - 1455
} - 1456
- 1457
/// Built-in premature-completion gate. On by default; conservative. - 1458
#[derive(Debug, Clone, PartialEq, Eq)] - 1459
pub struct StopPolicyResolved { - 1460
pub enabled: bool, - 1461
pub marker_gate: bool, - 1462
pub verify_gate: bool, - 1463
pub max_blocks: u32, - 1464
} - 1465
- 1466
/// Resolved gateway policy (docs/design/22-gateway.md). - 1467
#[derive(Debug, Clone, PartialEq, Eq)] - 1468
pub struct GatewayResolved { - 1469
pub enabled: bool, - 1470
pub approvals: String, - 1471
pub approver: Option<String>, - 1472
pub approval_timeout_secs: u64, - 1473
pub webhooks: std::collections::BTreeMap<String, WebhookResolved>, - 1474
pub rate_limit: Option<RateLimitSettings>, - 1475
/// Allowed inbound chat keys. Empty fails closed unless `chat_allowlist_open`. - 1476
pub chat_allowlist: Vec<String>, - 1477
/// Empty `chat_allowlist` was explicitly opted into staying open. - 1478
pub chat_allowlist_open: bool, - 1479
/// Process-wide cap on concurrently pooled per-workspace `Core` - 1480
/// instances (docs/design/34 Phase 2). Default 8. - 1481
pub core_pool_max: usize, - 1482
/// Idle duration after which a pooled non-default-workspace `Core` is - 1483
/// evicted. Default 1800s (30 minutes). - 1484
pub core_pool_idle_secs: u64, - 1485
/// Days a `pending` allowlist entry may sit unreviewed before it is - 1486
/// flagged by doctor and auto-denied by `--repair`. Default 7. - 1487
pub pending_expiry_days: u64, - 1488
} - 1489
- 1490
#[derive(Debug, Clone, PartialEq, Eq)] - 1491
pub struct SandboxResolved { - 1492
pub backend: String, - 1493
pub image: Option<String>, - 1494
} - 1495
- 1496
#[derive(Debug, Clone, PartialEq, Eq)] - 1497
pub struct WebhookResolved { - 1498
pub url: String, - 1499
pub token_env: Option<String>, - 1500
} - 1501
- 1502
/// Resolved memory policy (docs/design/23-memory.md). - 1503
#[derive(Debug, Clone, PartialEq, Eq)] - 1504
pub struct MemoryResolved { - 1505
pub search_enabled: bool, - 1506
pub write_enabled: bool, - 1507
pub skill_proposals: bool, - 1508
pub reflection: bool, - 1509
} - 1510
- 1511
/// Resolved scheduled-task behavior (docs/design/29-personal-os.md P2). - 1512
#[derive(Debug, Clone, PartialEq, Eq)] - 1513
pub struct AutomationResolved { - 1514
pub catch_up_missed: bool, - 1515
} - 1516
- 1517
/// Resolved update-check policy (docs/design/29-personal-os.md P3). - 1518
#[derive(Debug, Clone, PartialEq, Eq)] - 1519
pub struct UpdateResolved { - 1520
pub url: Option<String>, - 1521
pub interval_hours: u64, - 1522
} - 1523
- 1524
/// Resolved optional-tool registration policy. - 1525
#[derive(Debug, Clone, PartialEq, Eq)] - 1526
pub struct ToolsResolved { - 1527
pub web_fetch: bool, - 1528
pub browse: bool, - 1529
} - 1530
- 1531
/// Resolved native-Ollama tuning (docs/design/68-context-engine.md §8). - 1532
#[derive(Debug, Clone, PartialEq, Eq)] - 1533
pub struct OllamaResolved { - 1534
pub keep_alive: String, - 1535
pub num_ctx: Option<u64>, - 1536
} - 1537
- 1538
/// Resolved Anthropic tuning (docs/design/68-context-engine.md §11). - 1539
#[derive(Debug, Clone, PartialEq, Eq)] - 1540
pub struct AnthropicResolved { - 1541
pub fast_mode: bool, - 1542
} - 1543
- 1544
impl Default for Config { - 1545
fn default() -> Self { - 1546
Config { - 1547
provider: "anthropic".into(), - 1548
model: "claude-sonnet-4-5".into(), - 1549
max_tokens: 8192, - 1550
max_turns: 40, - 1551
permission_mode: PermissionMode::WorkspaceWrite, - 1552
approval_mode: ApprovalMode::Ask, - 1553
anthropic_base_url: None, - 1554
allow: Vec::new(), - 1555
ask: Vec::new(), - 1556
deny: Vec::new(), - 1557
workers: true, - 1558
hooks: Vec::new(), - 1559
max_retries: 3, - 1560
retry_base_backoff_ms: 500, - 1561
request_timeout_secs: 600, - 1562
run_retry_attempts: 6, - 1563
run_retry_base_backoff_ms: 2_000, - 1564
circuit_breaker_threshold: 5, - 1565
circuit_breaker_cooldown_secs: 60, - 1566
context_window: 128_000, - 1567
mcp: McpConfig::default(), - 1568
capabilities: CapabilityInheritanceResolved { - 1569
inherit_mcp: true, - 1570
inherit_hooks: true, - 1571
inherit_skills: true, - 1572
inherit_commands: true, - 1573
inherit_plugins: true, - 1574
}, - 1575
ui: UiResolved { - 1576
theme: "dark".into(), - 1577
bell: true, - 1578
keymap: std::collections::BTreeMap::new(), - 1579
composer: "emacs".into(), - 1580
osc52: false, - 1581
accessibility: AccessibilityResolved { - 1582
plain: false, - 1583
reduced_motion: false, - 1584
screen_reader: false, - 1585
}, - 1586
themes: std::collections::BTreeMap::new(), - 1587
}, - 1588
stop_policy: StopPolicyResolved { - 1589
enabled: true, - 1590
marker_gate: true, - 1591
verify_gate: true, - 1592
max_blocks: 2, - 1593
}, - 1594
finops: FinopsResolved::default(), - 1595
goal: GoalResolved { - 1596
handoff_reset: true, - 1597
max_audit_blocks: 2, - 1598
}, - 1599
work: WorkResolved { - 1600
enabled: true, - 1601
default_mode: "direct".into(), - 1602
max_items: 20, - 1603
max_revisions: 8, - 1604
max_parallel: 4, - 1605
confirmation: "risk-based".into(), - 1606
}, - 1607
route: RouteResolved { - 1608
objective: "auto".into(), - 1609
fallback_models: Vec::new(), - 1610
max_fallbacks: 4, - 1611
quality_hints: Vec::new(), - 1612
modality_hints: Vec::new(), - 1613
}, - 1614
probe: ProbeResolved { - 1615
hosted: "none".into(), - 1616
}, - 1617
intent: IntentResolved { - 1618
enabled: true, - 1619
accept_confidence: 0.75, - 1620
provisional_confidence: 0.45, - 1621
slice_capabilities: true, - 1622
posture: true, - 1623
// Deterministic tiers only by default. A paid classification - 1624
// before the run the user actually asked for is a real cost - 1625
// and a real latency, so it is opt-in. - 1626
escalate: "none".into(), - 1627
classify_model: None, - 1628
max_classify_usd: 0.01, - 1629
classify_timeout_secs: 10, - 1630
autonomy: "assisted".into(), - 1631
evidence_max_age_secs: 86_400, - 1632
}, - 1633
commitment: CommitmentResolved { - 1634
enabled: true, - 1635
lifetime_budget_usd: None, - 1636
stall_limit: 3, - 1637
review_every_hours: None, - 1638
default_ttl_days: None, - 1639
}, - 1640
automation: AutomationResolved { - 1641
catch_up_missed: true, - 1642
}, - 1643
update: UpdateResolved { - 1644
url: None, - 1645
interval_hours: 24, - 1646
}, - 1647
tools: ToolsResolved { - 1648
web_fetch: true, - 1649
browse: true, - 1650
}, - 1651
heartbeat: HeartbeatResolved { - 1652
enabled: false, - 1653
interval_secs: 1800, - 1654
model: None, - 1655
quiet_hours: None, - 1656
max_findings: 3, - 1657
}, - 1658
gateway: GatewayResolved { - 1659
enabled: false, - 1660
approvals: "deny".into(), - 1661
approver: None, - 1662
approval_timeout_secs: 300, - 1663
webhooks: std::collections::BTreeMap::new(), - 1664
rate_limit: None, - 1665
chat_allowlist: Vec::new(), - 1666
chat_allowlist_open: false, - 1667
core_pool_max: 8, - 1668
core_pool_idle_secs: 1800, - 1669
pending_expiry_days: 7, - 1670
}, - 1671
memory: MemoryResolved { - 1672
search_enabled: true, - 1673
write_enabled: true, - 1674
skill_proposals: true, - 1675
reflection: false, - 1676
}, - 1677
sandbox: SandboxResolved { - 1678
backend: "auto".into(), - 1679
image: None, - 1680
}, - 1681
feeds: FeedResolved { - 1682
enabled: false, - 1683
config_path: None, - 1684
db_path: None, - 1685
default_check_interval: "30m".into(), - 1686
max_items_per_feed: 500, - 1687
dedup_window_days: 90, - 1688
}, - 1689
server: ServerResolved { - 1690
// Loopback, no trusted hosts, no terminal over the web: a - 1691
// default install is reachable only from the machine it runs - 1692
// on, and every step away from that is deliberate. - 1693
bind: "127.0.0.1".into(), - 1694
trusted_hosts: Vec::new(), - 1695
public_url: None, - 1696
session_ttl_hours: 168, - 1697
loopback_auto_login: true, - 1698
workspace_roots: Vec::new(), - 1699
web_terminal: false, - 1700
web_terminal_requires_loopback: true, - 1701
bus: BusResolved::default(), - 1702
}, - 1703
plugins: PluginResolved::default(), - 1704
voice: VoiceSettings::default(), - 1705
ollama: OllamaResolved { - 1706
keep_alive: "30m".into(), - 1707
num_ctx: None, - 1708
}, - 1709
anthropic: AnthropicResolved { fast_mode: false }, - 1710
warnings: Vec::new(), - 1711
} - 1712
} - 1713
} - 1714
- 1715
#[derive(Debug, thiserror::Error)] - 1716
pub enum ConfigError { - 1717
#[error("cannot read config file {path}: {source}")] - 1718
Read { - 1719
path: PathBuf, - 1720
source: std::io::Error, - 1721
}, - 1722
#[error("cannot parse config file {path}: {source}")] - 1723
Parse { - 1724
path: PathBuf, - 1725
source: toml::de::Error, - 1726
}, - 1727
#[error("cannot write config file {path}: {source}")] - 1728
Write { - 1729
path: PathBuf, - 1730
source: std::io::Error, - 1731
}, - 1732
} - 1733
- 1734
/// The topmost editable configuration layer. `vak-home` is deliberately a - 1735
/// normal workspace people can inspect, and every other workspace inherits - 1736
/// this file without copying it. - 1737
pub fn global_path() -> Option<PathBuf> { - 1738
Some(crate::paths::default_workspace().join(".vak/config.toml")) - 1739
} - 1740
- 1741
pub fn project_path(cwd: &Path) -> PathBuf { - 1742
cwd.join(".vak/config.toml") - 1743
} - 1744
- 1745
/// Cheap, stat-only "has anything changed" signal for the global + project - 1746
/// config layers. Callers that hold a resolved value derived from these - 1747
/// files (e.g. `Core`'s cached `RouteSelection`) can compare fingerprints on - 1748
/// every access instead of re-parsing TOML, and only pay for a full re-load - 1749
/// when this value actually moves (docs/design/44-shared-config.md, - 1750
/// "Liveness"). - 1751
pub fn config_fingerprint(cwd: &Path) -> u64 { - 1752
let mut hash = 0xcbf29ce484222325_u64; - 1753
for path in [global_path(), Some(project_path(cwd))] - 1754
.into_iter() - 1755
.flatten() - 1756
{ - 1757
let (mtime_nanos, len) = std::fs::metadata(&path) - 1758
.and_then(|m| m.modified().map(|t| (t, m.len()))) - 1759
.map(|(t, len)| { - 1760
let nanos = t - 1761
.duration_since(std::time::UNIX_EPOCH) - 1762
.map(|d| d.as_nanos() as u64) - 1763
.unwrap_or(0); - 1764
(nanos, len) - 1765
}) - 1766
.unwrap_or((0, 0)); - 1767
for byte in mtime_nanos - 1768
.to_le_bytes() - 1769
.into_iter() - 1770
.chain(len.to_le_bytes()) - 1771
{ - 1772
hash ^= u64::from(byte); - 1773
hash = hash.wrapping_mul(0x100000001b3); - 1774
} - 1775
} - 1776
hash - 1777
} - 1778
- 1779
/// Initialize the project layer used by interactive clients. - 1780
/// - 1781
/// The file intentionally contains no copied global values. An empty project - 1782
/// layer inherits the user's global configuration through [`load_with_trust`], - 1783
/// so later changes to shared defaults reach projects that have not opted into - 1784
/// a local override. `create_new` also keeps two desktop launches from - 1785
/// overwriting a project config created by the other launch. - 1786
pub fn ensure_project_config(cwd: &Path) -> Result<PathBuf, ConfigError> { - 1787
let dir = cwd.join(".vak"); - 1788
std::fs::create_dir_all(&dir).map_err(|source| ConfigError::Write { - 1789
path: dir.clone(), - 1790
source, - 1791
})?; - 1792
let path = project_path(cwd); - 1793
let _guard = lock_config_files(); - 1794
match std::fs::OpenOptions::new() - 1795
.write(true) - 1796
.create_new(true) - 1797
.open(&path) - 1798
{ - 1799
Ok(mut file) => { - 1800
use std::io::Write; - 1801
file.write_all( - 1802
b"# Project-local overrides. Unset values inherit from the user config.\n", - 1803
) - 1804
.map_err(|source| ConfigError::Write { - 1805
path: path.clone(), - 1806
source, - 1807
})?; - 1808
} - 1809
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} - 1810
Err(source) => { - 1811
return Err(ConfigError::Write { - 1812
path: path.clone(), - 1813
source, - 1814
}); - 1815
} - 1816
} - 1817
Ok(path) - 1818
} - 1819
- 1820
/// Atomically replace the set of MCP servers at one explicit configuration - 1821
/// scope. The caller selects either [`global_path`] or [`project_path`]; no - 1822
/// values are inferred from the process directory. A server missing from - 1823
/// `servers` is removed; a server that stays keeps every key the management - 1824
/// API does not own, and every other key in the file is preserved. - 1825
pub fn persist_mcp_servers( - 1826
path: &Path, - 1827
servers: &std::collections::BTreeMap<String, McpServerConfig>, - 1828
) -> Result<(), ConfigError> { - 1829
update_config_file(path, |document| { - 1830
let mcp = child_table(document, "mcp", path)?; - 1831
let mut existing = match mcp.remove("servers") { - 1832
Some(toml::Value::Table(existing)) => existing, - 1833
_ => toml::Table::new(), - 1834
}; - 1835
let entries = servers - 1836
.iter() - 1837
.map(|(name, server)| { - 1838
let mut entry = match existing.remove(name) { - 1839
Some(toml::Value::Table(entry)) => entry, - 1840
_ => toml::Table::new(), - 1841
};
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.