- 1342
pub fn effective_voice(&self) -> vak_config::VoiceSettings { - 1343
Self::read_override(&self.inner.voice_override) - 1344
.unwrap_or_else(|| self.inner.config.voice.clone()) - 1345
} - 1346
- 1347
pub fn apply_persisted_voice(&self, voice: vak_config::VoiceSettings) { - 1348
Self::write_override(&self.inner.voice_override, Some(voice)); - 1349
} - 1350
- 1351
pub fn effective_plugins(&self) -> vak_config::PluginResolved { - 1352
Self::read_override(&self.inner.plugins_override).unwrap_or_else(|| { - 1353
let resolved: &vak_config::PluginResolved = &self.inner.config.plugins; - 1354
resolved.clone() - 1355
}) - 1356
} - 1357
- 1358
pub fn apply_persisted_plugins(&self, plugins: vak_config::PluginResolved) { - 1359
Self::write_override(&self.inner.plugins_override, Some(plugins)); - 1360
} - 1361
- 1362
/// Session-scoped routing beliefs (Phase R): domain-weighted doubt - 1363
/// that demotes flaky legs until one success clears them. - 1364
pub fn beliefs(&self) -> &Arc<routing::BeliefState> { - 1365
&self.inner.beliefs - 1366
} - 1367
- 1368
/// Read a session-scoped override, releasing the lock before returning. - 1369
/// - 1370
/// Every `Option`-shaped override on `Inner` is read through here. The - 1371
/// idiom this replaces — `if let Ok(g) = self.inner.slot.lock() && …` - 1372
/// — keeps the guard alive for the whole body, so a `self.` call - 1373
/// inside that body which locks the same slot deadlocks the thread: - 1374
/// `std::sync::Mutex` is not reentrant. `cache_home` did exactly that - 1375
/// against `sessions_home`, wedging any process that set the override. - 1376
/// Cloning out under a minimal scope makes the hazard unreachable. - 1377
fn read_override<T: Clone>(slot: &std::sync::Mutex<Option<T>>) -> Option<T> { - 1378
slot.lock() - 1379
.unwrap_or_else(std::sync::PoisonError::into_inner) - 1380
.clone() - 1381
} - 1382
- 1383
/// Set a session-scoped override. Poisoning is recovered rather than - 1384
/// propagated: an override is a preference, and losing one to an - 1385
/// unrelated panic elsewhere should not take down this call. - 1386
fn write_override<T>(slot: &std::sync::Mutex<Option<T>>, value: Option<T>) { - 1387
*slot - 1388
.lock() - 1389
.unwrap_or_else(std::sync::PoisonError::into_inner) = value; - 1390
} - 1391
- 1392
pub fn set_model(&self, model: String) { - 1393
let route = self.effective_route(); - 1394
self.set_route(route.provider, model); - 1395
} - 1396
- 1397
pub fn effective_model(&self) -> String { - 1398
self.effective_route().model - 1399
} - 1400
- 1401
pub fn model_source(&self) -> String { - 1402
self.effective_route().model_source - 1403
} - 1404
- 1405
pub fn set_provider(&self, provider: String) { - 1406
let route = self.effective_route(); - 1407
self.set_route(provider, route.model); - 1408
} - 1409
- 1410
/// Apply an explicit scoped route override atomically. CLI flags, task - 1411
/// pins, heartbeat pins, and test seams use this path; persisted admin - 1412
/// changes use `apply_persisted_route` instead. - 1413
pub fn set_route(&self, provider: String, model: String) { - 1414
self.replace_route(route_selection( - 1415
provider, - 1416
model, - 1417
"runtime_override", - 1418
"runtime_override", - 1419
true, - 1420
)); - 1421
} - 1422
- 1423
/// Re-read the layered provider/model defaults. Runtime-pinned cores are - 1424
/// deliberately excluded so a global admin edit cannot rewrite a scoped - 1425
/// CLI, task, heartbeat, or worker contract. - 1426
pub fn refresh_persisted_route(&self) -> Result<RouteSelection, CoreError> { - 1427
let current = self.effective_route(); - 1428
if current.runtime_pinned { - 1429
return Ok(current); - 1430
} - 1431
let config = vak_config::load_with_trust(&self.inner.cwd, self.inner.trust_project_config)?; - 1432
let route = route_from_config(&self.inner.cwd, &config, false); - 1433
if route != current { - 1434
self.replace_route(route.clone()); - 1435
} - 1436
Ok(route) - 1437
} - 1438
- 1439
/// Hot-apply a route that has already been committed atomically to the - 1440
/// workspace config by the authenticated administration surface. - 1441
pub fn apply_persisted_route(&self, provider: String, model: String) { - 1442
let provider_source = route_source(&self.inner.cwd, "provider"); - 1443
let model_source = route_source(&self.inner.cwd, "model"); - 1444
self.replace_route(route_selection( - 1445
provider, - 1446
model, - 1447
&provider_source, - 1448
&model_source, - 1449
false, - 1450
)); - 1451
} - 1452
- 1453
pub fn effective_route(&self) -> RouteSelection { - 1454
self.refresh_route_if_stale(); - 1455
self.inner - 1456
.route - 1457
.lock() - 1458
.unwrap_or_else(std::sync::PoisonError::into_inner) - 1459
.clone() - 1460
} - 1461
- 1462
/// Cheap (stat-only) check for whether the config files the cached - 1463
/// route was derived from have changed since — e.g. another process - 1464
/// ran `vak setup` while this `Core` was already resolved and pooled. - 1465
/// Only pays for a full re-parse + re-derivation when the fingerprint - 1466
/// actually moved (docs/design/44-shared-config.md, "Liveness"). - 1467
fn refresh_route_if_stale(&self) { - 1468
let current_fp = vak_config::config_fingerprint(&self.inner.cwd); - 1469
{ - 1470
let mut last_fp = self - 1471
.inner - 1472
.route_fingerprint - 1473
.lock() - 1474
.unwrap_or_else(std::sync::PoisonError::into_inner); - 1475
if *last_fp == current_fp { - 1476
return; - 1477
} - 1478
*last_fp = current_fp; - 1479
} - 1480
let _ = self.refresh_persisted_route(); - 1481
} - 1482
- 1483
fn replace_route(&self, route: RouteSelection) { - 1484
let provider = route.provider.clone(); - 1485
*self - 1486
.inner - 1487
.route - 1488
.lock() - 1489
.unwrap_or_else(std::sync::PoisonError::into_inner) = route; - 1490
if let Ok(mut injected) = self.inner.provider_instance.lock() - 1491
&& injected - 1492
.as_ref() - 1493
.is_some_and(|current| current.name() != provider) - 1494
{ - 1495
*injected = None; - 1496
} - 1497
} - 1498
- 1499
pub fn effective_provider(&self) -> String { - 1500
self.effective_route().provider - 1501
} - 1502
- 1503
pub fn provider_source(&self) -> String { - 1504
self.effective_route().provider_source - 1505
} - 1506
- 1507
/// Whether project-owned privileged configuration was admitted when this - 1508
/// core was created. Presentation files use the same trust boundary. - 1509
pub fn project_config_trusted(&self) -> bool { - 1510
self.inner.trust_project_config - 1511
} - 1512
- 1513
pub fn provider_names(&self) -> Vec<String> { - 1514
self.inner.registry.names() - 1515
} - 1516
- 1517
pub fn set_max_turns(&self, max_turns: usize) { - 1518
self.inner - 1519
.max_turns_runtime_pinned - 1520
.store(true, std::sync::atomic::Ordering::Release); - 1521
if let Ok(mut c) = self.inner.max_turns_override.lock() { - 1522
*c = Some(max_turns); - 1523
} - 1524
} - 1525
- 1526
pub fn apply_persisted_max_turns(&self, max_turns: usize) { - 1527
Self::write_override(&self.inner.max_turns_override, Some(max_turns)); - 1528
self.inner - 1529
.max_turns_runtime_pinned - 1530
.store(false, std::sync::atomic::Ordering::Release); - 1531
} - 1532
- 1533
pub fn set_tool_worker_exe(&self, executable: PathBuf) { - 1534
if let Ok(mut worker) = self.inner.tool_worker_exe.lock() { - 1535
*worker = executable; - 1536
} - 1537
} - 1538
- 1539
/// Carry the pinned, version-matched worker into an isolated child Core. - 1540
pub fn tool_worker_exe(&self) -> PathBuf { - 1541
self.inner - 1542
.tool_worker_exe - 1543
.lock() - 1544
.map(|worker| worker.clone()) - 1545
.unwrap_or_else(|_| PathBuf::from("__vak_tool_worker_unavailable__")) - 1546
} - 1547
- 1548
pub fn agent_tools(&self) -> Vec<Arc<dyn vak_tools::Tool>> { - 1549
let worker = self - 1550
.inner - 1551
.tool_worker_exe - 1552
.lock() - 1553
.ok() - 1554
.map(|worker| worker.clone()) - 1555
.unwrap_or_else(|| PathBuf::from("__vak_tool_worker_unavailable__")); - 1556
let tools = vak_tools::brokered_tools(worker, &self.new_documents); - 1557
self.filter_builtin_tools(tools) - 1558
} - 1559
- 1560
/// Prepare the current effective capability surface for a standalone - 1561
/// flow. The prompt and both tool views are derived together so callers - 1562
/// cannot accidentally advertise one surface while executing another. - 1563
pub async fn prepare_turn(&self) -> PreparedTurn { - 1564
let descriptors = self.admitted_capabilities().await; - 1565
let admitted: std::collections::BTreeSet<String> = descriptors - 1566
.iter() - 1567
.filter(|descriptor| descriptor.kind == CapabilityKind::Tool) - 1568
.map(|descriptor| descriptor.name.clone()) - 1569
.collect(); - 1570
let tools: Vec<_> = self - 1571
.agent_tools() - 1572
.into_iter() - 1573
.filter(|tool| admitted.contains(tool.name())) - 1574
.collect(); - 1575
let read_only_tools: Vec<_> = self - 1576
.agent_read_only_tools() - 1577
.into_iter() - 1578
.filter(|tool| admitted.contains(tool.name())) - 1579
.collect(); - 1580
PreparedTurn::from_parts( - 1581
self.resolve_prompt(&descriptors).text, - 1582
tools, - 1583
read_only_tools, - 1584
) - 1585
} - 1586
- 1587
pub fn agent_read_only_tools(&self) -> Vec<Arc<dyn vak_tools::Tool>> { - 1588
let worker = self - 1589
.inner - 1590
.tool_worker_exe - 1591
.lock() - 1592
.ok() - 1593
.map(|worker| worker.clone()) - 1594
.unwrap_or_else(|| PathBuf::from("__vak_tool_worker_unavailable__")); - 1595
let tools = vak_tools::brokered_read_only_tools(worker); - 1596
self.filter_builtin_tools(tools) - 1597
} - 1598
- 1599
fn filter_builtin_tools( - 1600
&self, - 1601
tools: Vec<Arc<dyn vak_tools::Tool>>, - 1602
) -> Vec<Arc<dyn vak_tools::Tool>> { - 1603
let Some(policy) = self.channel_policy() else { - 1604
return tools; - 1605
}; - 1606
tools - 1607
.into_iter() - 1608
.filter(|tool| Self::allowed_by(&policy.tools_allow, &policy.tools_deny, tool.name())) - 1609
.collect() - 1610
} - 1611
- 1612
pub fn agent_sandbox(&self) -> Option<Arc<dyn vak_tools::sandbox::Sandbox>> { - 1613
self.build_execution_sandbox() - 1614
} - 1615
- 1616
/// Returns the broker for this Core's isolated agent environment. The - 1617
/// broker is capability-based and does not grant network access by itself. - 1618
pub fn agent_network_broker(&self) -> agent_network::AgentNetworkBroker { - 1619
self.inner - 1620
.agent_network - 1621
.lock() - 1622
.unwrap_or_else(std::sync::PoisonError::into_inner) - 1623
.clone() - 1624
} - 1625
- 1626
pub fn set_agent_network_broker(&self, broker: agent_network::AgentNetworkBroker) { - 1627
*self - 1628
.inner - 1629
.agent_network - 1630
.lock() - 1631
.unwrap_or_else(std::sync::PoisonError::into_inner) = broker; - 1632
} - 1633
- 1634
pub fn effective_max_turns(&self) -> usize { - 1635
Self::read_override(&self.inner.max_turns_override).unwrap_or(self.inner.config.max_turns) - 1636
} - 1637
- 1638
pub fn set_permission_mode(&self, mode: vak_config::PermissionMode) { - 1639
self.replace_permission_mode(mode, true); - 1640
} - 1641
- 1642
fn replace_permission_mode(&self, mode: vak_config::PermissionMode, pinned: bool) { - 1643
let mut lease = self - 1644
.inner - 1645
.permission_lease - 1646
.lock() - 1647
.unwrap_or_else(std::sync::PoisonError::into_inner); - 1648
if self.effective_permission_mode() != mode { - 1649
lease.cancel(); - 1650
*lease = CancellationToken::new(); - 1651
} - 1652
self.inner - 1653
.mode_runtime_pinned - 1654
.store(pinned, std::sync::atomic::Ordering::Release); - 1655
Self::write_override(&self.inner.mode_override, Some(mode)); - 1656
} - 1657
- 1658
pub fn apply_persisted_permission_mode(&self, mode: vak_config::PermissionMode) { - 1659
self.replace_permission_mode(mode, false); - 1660
} - 1661
- 1662
/// A run's authority lease. A mode change cancels old leases before - 1663
/// publishing its new mode; a caller cannot revive one by resetting its - 1664
/// own cancellation token. - 1665
pub fn permission_lease(&self) -> CancellationToken { - 1666
self.inner - 1667
.permission_lease - 1668
.lock() - 1669
.unwrap_or_else(std::sync::PoisonError::into_inner) - 1670
.child_token() - 1671
} - 1672
- 1673
/// Read only the persisted trust-resolved ceiling. Unlike preference - 1674
/// refresh this never touches live MCP clients or capability caches. - 1675
pub fn persisted_permission_ceiling(&self) -> Result<vak_config::PermissionMode, CoreError> { - 1676
Ok( - 1677
vak_config::load_with_trust(&self.inner.cwd, self.inner.trust_project_config)? - 1678
.permission_mode, - 1679
) - 1680
} - 1681
- 1682
/// Revoke a resident scoped mode when the workspace's persisted ceiling - 1683
/// has narrowed. A narrower channel pin remains intact; only a mode above - 1684
/// the new ceiling is replaced, and replacement cancels its old lease. - 1685
pub fn enforce_persisted_permission_ceiling(&self) -> Result<bool, CoreError> { - 1686
let ceiling = self.persisted_permission_ceiling()?; - 1687
if self.effective_permission_mode().rank() > ceiling.rank() { - 1688
self.replace_permission_mode(ceiling, false); - 1689
Ok(true) - 1690
} else { - 1691
Ok(false) - 1692
} - 1693
} - 1694
- 1695
pub fn permission_mode_runtime_pinned(&self) -> bool { - 1696
self.inner - 1697
.mode_runtime_pinned - 1698
.load(std::sync::atomic::Ordering::Acquire) - 1699
} - 1700
- 1701
/// The runtime-pinned permission mode, if one is active (see - 1702
/// `permission_mode_runtime_pinned`), so it can be carried over to a - 1703
/// freshly-resolved `Core` for another workspace/agent — otherwise a - 1704
/// user-pinned restriction (e.g. read-only) silently would not apply the - 1705
/// moment a different Agent's Core is resolved from disk config. - 1706
pub fn permission_mode_override_value(&self) -> Option<vak_config::PermissionMode> { - 1707
if self.permission_mode_runtime_pinned() { - 1708
Self::read_override(&self.inner.mode_override) - 1709
} else { - 1710
None - 1711
} - 1712
} - 1713
- 1714
pub fn effective_approval_mode(&self) -> vak_config::ApprovalMode { - 1715
Self::read_override(&self.inner.approval_mode_override) - 1716
.unwrap_or(self.inner.config.approval_mode) - 1717
} - 1718
- 1719
pub fn set_approval_mode(&self, mode: vak_config::ApprovalMode) { - 1720
Self::write_override(&self.inner.approval_mode_override, Some(mode)); - 1721
} - 1722
- 1723
pub fn apply_persisted_approval_mode(&self, mode: vak_config::ApprovalMode) { - 1724
Self::write_override(&self.inner.approval_mode_override, Some(mode)); - 1725
} - 1726
- 1727
/// The effective `(allow, ask, deny)` lists — the runtime override when - 1728
/// one has been applied, the loaded config otherwise. - 1729
/// - 1730
/// Everything that builds a permission engine reads rules through here, - 1731
/// so an edit persisted by `PUT /config/permissions` takes effect on the - 1732
/// next turn rather than the next process. - 1733
pub fn effective_permission_rules(&self) -> PermissionRuleLists { - 1734
Self::read_override(&self.inner.rules_override).unwrap_or_else(|| { - 1735
( - 1736
self.inner.config.allow.clone(), - 1737
self.inner.config.ask.clone(), - 1738
self.inner.config.deny.clone(), - 1739
) - 1740
}) - 1741
} - 1742
- 1743
pub fn apply_persisted_permission_rules( - 1744
&self, - 1745
allow: Vec<String>, - 1746
ask: Vec<String>, - 1747
deny: Vec<String>, - 1748
) { - 1749
Self::write_override(&self.inner.rules_override, Some((allow, ask, deny))); - 1750
} - 1751
- 1752
/// Build a permission engine from this `Core`'s effective rules. - 1753
/// - 1754
/// The single entry point every surface uses. Callers used to reach for - 1755
/// `build_engine_with(core.config(), …)` directly, which read the - 1756
/// immutable loaded config and so could not see a runtime rule change; - 1757
/// routing through the `Core` is what keeps "what the engine evaluates" - 1758
/// and "what the operator last set" the same answer. - 1759
pub fn build_permission_engine( - 1760
&self, - 1761
extra: &[String], - 1762
) -> Result<vak_permission::PermissionEngine, CoreError> { - 1763
let (allow, ask, deny) = self.effective_permission_rules(); - 1764
vak_permission::PermissionEngine::from_rule_strings(&rule_specs_from( - 1765
&allow, &ask, &deny, extra, - 1766
)) - 1767
.map(|engine| engine.with_presenting_tools(presentation_tools::presenting_tool_names())) - 1768
.map(|engine| { - 1769
if self.task_copy_boundary { - 1770
engine.restrict_tools(TASK_COPY_TOOLS) - 1771
} else { - 1772
engine - 1773
} - 1774
}) - 1775
.map_err(CoreError::Rule) - 1776
} - 1777
- 1778
/// Runtime sandbox-backend selection ("os", "docker", or config default - 1779
/// via None). Session-scoped like every other override; never persisted. - 1780
pub fn set_sandbox_backend(&self, backend: Option<String>) { - 1781
Self::write_override(&self.inner.sandbox_backend_override, backend); - 1782
} - 1783
- 1784
/// The runtime-pinned sandbox backend, if one is active, so it can be - 1785
/// carried over to a freshly-resolved `Core` for another workspace/agent - 1786
/// — otherwise a user-pinned backend (e.g. forcing "docker" for a - 1787
/// hardened run) silently would not apply the moment a different - 1788
/// Agent's Core is resolved from disk config. - 1789
pub fn sandbox_backend_override_value(&self) -> Option<String> { - 1790
Self::read_override(&self.inner.sandbox_backend_override) - 1791
} - 1792
- 1793
pub fn effective_sandbox_backend(&self) -> String { - 1794
Self::read_override(&self.inner.sandbox_backend_override) - 1795
.unwrap_or_else(|| self.inner.config.sandbox.backend.clone()) - 1796
} - 1797
- 1798
pub fn breaker(&self) -> Arc<vak_agent::CircuitBreaker> { - 1799
self.inner.breaker.clone() - 1800
} - 1801
- 1802
/// Runtime MCP server table replacement (trusted surfaces only). Takes - 1803
/// effect on the next turn; the cached capability section is dropped so - 1804
/// the next run re-discovers the new inventory. - 1805
pub fn set_mcp_servers(&self, config: vak_config::McpConfig) { - 1806
self.inner - 1807
.mcp_runtime_pinned - 1808
.store(true, std::sync::atomic::Ordering::Release); - 1809
self.replace_mcp(config); - 1810
} - 1811
- 1812
pub fn apply_persisted_mcp_servers(&self, config: vak_config::McpConfig) { - 1813
self.invalidate_mcp_cache(); - 1814
self.replace_mcp(config.clone()); - 1815
self.inner - 1816
.mcp_runtime_pinned - 1817
.store(false, std::sync::atomic::Ordering::Release); - 1818
self.capability_registry() - 1819
.hint(capability::Hint::ConfigChanged); - 1820
} - 1821
- 1822
pub fn invalidate_mcp_cache(&self) { - 1823
*self - 1824
.inner - 1825
.mcp_cache - 1826
.lock() - 1827
.unwrap_or_else(std::sync::PoisonError::into_inner) = None; - 1828
} - 1829
- 1830
fn replace_mcp(&self, config: vak_config::McpConfig) { - 1831
if let Ok(mut c) = self.inner.mcp_override.lock() { - 1832
*c = Some(config); - 1833
} - 1834
} - 1835
- 1836
/// **The** capability packet for a turn, for every surface. - 1837
/// - 1838
/// Every surface asks here, so the same question always gets the same - 1839
/// packet. - 1840
/// - 1841
/// One canonical way (AGENTS.md invariant 30): surfaces do not decide - 1842
/// this, admission does. A pass is offline — declarations plus what the - 1843
/// MCP pool has already observed — so admission reconciles when anything - 1844
/// changed and never waits on an integration (invariant 25). - 1845
pub async fn admitted_capabilities(&self) -> Vec<CapabilityDescriptor> { - 1846
let registry = self.capability_registry(); - 1847
if registry.current().await.epoch == 0 || registry.has_pending_changes().await { - 1848
registry.reconcile().await; - 1849
} - 1850
registry.current().await.descriptors() - 1851
} - 1852
- 1853
/// Fast live revocation check for presentation and other non-async - 1854
/// projections. Availability still follows the published epoch; this - 1855
/// check only answers whether a capability is forbidden right now. - 1856
pub fn capability_revoked(&self, kind: vak_session::types::CapabilityKind, name: &str) -> bool { - 1857
self.capability_registry() - 1858
.revoked_now(&capability::CapabilityId::new(kind, name)) - 1859
} - 1860
- 1861
/// A live session's admitted set, re-rendered against the current - 1862
/// registry. - 1863
/// - 1864
/// **Admission is unchanged**: the contract still decides what may be - 1865
/// called, and a capability the registry has since gained does not - 1866
/// appear here. What is refreshed is the *description* of something the - 1867
/// contract already admits — most visibly an MCP server's discovered - 1868
/// catalog, but equally a skill's summary or a command's template. - 1869
/// - 1870
/// This is the epoch re-bind from doc 41 invariant 6: capability changes - 1871
/// take effect at the next turn boundary of every live session, with no - 1872
/// restart and no rotation. Without it a session admitted while - 1873
/// discovery was still in flight carries the name-only MCP line for its - 1874
/// entire life, and under those two constraints "its entire life" has no - 1875
/// end. Matching is on the typed `(kind, name)` identity rather than on - 1876
/// rendered text, so it holds for every kind rather than the one whose - 1877
/// wording someone thought to grep for. - 1878
async fn rebound_capabilities( - 1879
&self, - 1880
_contract: &vak_session::types::FrozenContract, - 1881
) -> Vec<CapabilityDescriptor> { - 1882
self.admitted_capabilities().await - 1883
} - 1884
- 1885
pub fn effective_mcp(&self) -> vak_config::McpConfig { - 1886
let mut config = self - 1887
.inner - 1888
.mcp_override - 1889
.lock() - 1890
.ok() - 1891
.and_then(|c| c.clone()) - 1892
.unwrap_or_else(|| self.inner.config.mcp.clone()); - 1893
self.extend_enabled_plugin_mcp(&mut config); - 1894
self.filter_mcp(config) - 1895
} - 1896
- 1897
pub fn effective_capability_inheritance(&self) -> vak_config::CapabilityInheritanceResolved { - 1898
self.inner - 1899
.capabilities_override - 1900
.lock() - 1901
.ok() - 1902
.and_then(|value| value.clone()) - 1903
.unwrap_or_else(|| self.inner.config.capabilities.clone()) - 1904
} - 1905
- 1906
pub fn apply_persisted_capability_inheritance( - 1907
&self, - 1908
capabilities: vak_config::CapabilityInheritanceResolved, - 1909
) { - 1910
if let Ok(mut current) = self.inner.capabilities_override.lock() { - 1911
*current = Some(capabilities); - 1912
} - 1913
if let Ok(mut cache) = self.inner.mcp_cache.lock() { - 1914
*cache = None; - 1915
} - 1916
} - 1917
- 1918
fn shared_capability_root(&self) -> std::path::PathBuf { - 1919
vak_config::paths::default_workspace().join(".vak") - 1920
} - 1921
- 1922
/// Plugin package roots contributing skills and commands, tagged with the - 1923
/// provenance string inspection surfaces render. - 1924
fn enabled_plugin_skill_roots(&self) -> Vec<(std::path::PathBuf, String)> { - 1925
let mut out = Vec::new(); - 1926
for root in self.capability_roots() { - 1927
let Ok(enabled) = vak_plugin::PluginStore::new(&root.path).enabled() else { - 1928
continue; - 1929
}; - 1930
out.extend(enabled.into_iter().map(|plugin| { - 1931
( - 1932
plugin.package_path, - 1933
format!( - 1934
"plugin:{}:{}:{}", - 1935
root.scope.label(), - 1936
plugin.name, - 1937
plugin.trace_id - 1938
), - 1939
) - 1940
})); - 1941
} - 1942
out - 1943
} - 1944
- 1945
/// The capability roots this Core reads, workspace-local first so a - 1946
/// workspace-scoped skill, command, plugin, or hook shadows a shared one - 1947
/// of the same name. - 1948
/// - 1949
/// The shared root is dropped when `capabilities.inherit_plugins = false`, - 1950
/// and collapsed when the workspace IS the shared workspace. Every - 1951
/// capability lookup goes through here: this resolution was copied into - 1952
/// six call sites, one of which had already drifted to the opposite - 1953
/// ordering, and a shared-scope bug in one copy is invisible in the rest. - 1954
pub fn capability_roots(&self) -> Vec<CapabilityRoot> { - 1955
let shared = self.shared_capability_root(); - 1956
let workspace = self.inner.cwd.join(".vak"); - 1957
let mut roots = vec![CapabilityRoot { - 1958
path: workspace.clone(), - 1959
scope: CapabilityScope::Workspace, - 1960
}]; - 1961
if workspace != shared && self.effective_capability_inheritance().inherit_plugins { - 1962
roots.push(CapabilityRoot { - 1963
path: shared, - 1964
scope: CapabilityScope::Shared, - 1965
}); - 1966
} - 1967
roots - 1968
} - 1969
- 1970
/// Scan all plugin stores for packages whose skill descriptions reference - 1971
/// retired tool names (e.g. `python_eval`, `react_preview`). Returns each - 1972
/// `(plugin_name, retired_tool_names)` pair found. - 1973
/// - 1974
/// This is a **read-only** check: it never removes or disables plugins. - 1975
/// Removal is the job of `seed::cleanup_retired_plugins` during setup - 1976
/// (`vak setup seed` / `vak self update`). The check exists so a running - 1977
/// server or desktop session can surface a prominent warning and refuse - 1978
/// to advertise skills from retired-tool plugins in the capability - 1979
/// contract sent to the model (AGNS invariant 9: model catalogues are - 1980
/// discovered, never hardcoded; and invariant 29: pre-baseline or - 1981
/// retired state is refused, not partially read). - 1982
pub fn check_retired_plugins(&self) -> Vec<(String, Vec<String>)> { - 1983
let mut flagged = Vec::new(); - 1984
for root in self.capability_roots() { - 1985
if let Ok(store) = vak_plugin::PluginStore::new(&root.path).retired_plugins() { - 1986
flagged.extend(store); - 1987
} - 1988
} - 1989
flagged - 1990
} - 1991
- 1992
fn extend_enabled_plugin_mcp(&self, config: &mut vak_config::McpConfig) { - 1993
for root in self.capability_roots() { - 1994
let Ok(plugins) = vak_plugin::PluginStore::new(root.path).enabled() else { - 1995
continue; - 1996
}; - 1997
for plugin in plugins { - 1998
for relative in plugin.capabilities.mcp_manifests { - 1999
let path = plugin.package_path.join(&relative); - 2000
let Ok(bytes) = std::fs::read(&path) else { - 2001
continue; - 2002
}; - 2003
let Ok(value) = serde_json::from_slice::<serde_json::Value>(&bytes) else { - 2004
continue; - 2005
}; - 2006
let Some(servers) = value - 2007
.get("mcpServers") - 2008
.and_then(serde_json::Value::as_object) - 2009
else { - 2010
continue; - 2011
}; - 2012
for (name, raw) in servers { - 2013
let Some(command) = raw.get("command").and_then(serde_json::Value::as_str) - 2014
else { - 2015
continue; - 2016
}; - 2017
let args = raw - 2018
.get("args") - 2019
.and_then(serde_json::Value::as_array) - 2020
.map(|values| { - 2021
values - 2022
.iter() - 2023
.filter_map(serde_json::Value::as_str) - 2024
.map(str::to_string) - 2025
.collect() - 2026
}) - 2027
.unwrap_or_default(); - 2028
let env = raw - 2029
.get("env") - 2030
.and_then(serde_json::Value::as_object) - 2031
.map(|values| { - 2032
values - 2033
.iter() - 2034
.filter_map(|(key, value)| { - 2035
value.as_str().map(|v| (key.clone(), v.to_string())) - 2036
}) - 2037
.collect() - 2038
}) - 2039
.unwrap_or_default(); - 2040
let key = format!("plugin.{}.{}", plugin.name, name); - 2041
// Same effective-policy seam the runtime tools read: - 2042
// the persisted `network_allow`/`network_deny` override - 2043
// refreshes `effective_plugins()` live, so a Settings - 2044
// toggle reaches a plugin-contributed server exactly - 2045
// like it reaches its runner tool. Reading the base - 2046
// config instead would keep this server stale for the - 2047
// whole process after an override lands. - 2048
let net_allowed = self.effective_plugins().is_network_allowed(&plugin.name) - 2049
&& self - 2050
.channel_policy() - 2051
.as_ref() - 2052
.map(|p| !p.plugins_network_deny.contains(&plugin.name)) - 2053
.unwrap_or(true); - 2054
config - 2055
.servers - 2056
.entry(key) - 2057
.or_insert(vak_config::McpServerConfig { - 2058
command: command.to_string(), - 2059
args, - 2060
env, - 2061
network: net_allowed, - 2062
// Plugin-contributed servers declare nothing - 2063
// by default, which keeps them reachable: - 2064
// undeclared is never sliced away. - 2065
serves: Vec::new(), - 2066
}); - 2067
} - 2068
} - 2069
} - 2070
} - 2071
} - 2072
- 2073
fn plugin_mcp_invocation_context(&self) -> Vec<(vak_plugin::PluginStore, String, String)> { - 2074
let mut context = Vec::new(); - 2075
for root in self.capability_roots() { - 2076
let store = vak_plugin::PluginStore::new(root.path); - 2077
let Ok(plugins) = store.enabled() else { - 2078
continue; - 2079
}; - 2080
context.extend( - 2081
plugins - 2082
.into_iter() - 2083
.map(|plugin| (store.clone(), plugin.name, plugin.trace_id)), - 2084
); - 2085
} - 2086
context - 2087
} - 2088
- 2089
pub fn apply_channel_policy(&self, policy: vak_config::ChannelPolicy) { - 2090
if let Ok(mut current) = self.inner.channel_policy.lock() { - 2091
*current = Some(policy); - 2092
} - 2093
} - 2094
- 2095
pub fn channel_policy(&self) -> Option<vak_config::ChannelPolicy> { - 2096
self.inner - 2097
.channel_policy - 2098
.lock() - 2099
.ok() - 2100
.and_then(|p| p.clone()) - 2101
} - 2102
- 2103
/// Whether a channel overlay permits a named capability. Inheritance is - 2104
/// represented by no policy and therefore permits the capability here; - 2105
/// the ordinary permission engine still decides whether execution is - 2106
/// allowed for the current mode. - 2107
pub fn channel_tool_allowed(&self, tool: &str) -> bool { - 2108
self.channel_policy() - 2109
.is_none_or(|policy| Self::allowed_by(&policy.tools_allow, &policy.tools_deny, tool)) - 2110
} - 2111
- 2112
/// Compile this turn's channel overlay into permission rules. - 2113
/// - 2114
/// **Restrictive only** (AGENTS.md invariant 20), and that is the whole - 2115
/// point of it living in one place. An overlay's `_allow` list is a - 2116
/// *visibility* narrowing — "this chat may reach these and nothing - 2117
/// else" — already enforced by dropping everything unlisted from the - 2118
/// tool registry (`channel_tool_allowed`) and, for MCP, by `McpTool`'s - 2119
/// own `server/tool` glob at call time. - 2120
/// - 2121
/// It must never become `+` allow rules. Both call sites used to do - 2122
/// exactly that, and a blanket `+bash` / `+mcp` outranks the mode - 2123
/// default that would otherwise have raised an approval gate — so - 2124
/// `tools_allow = ["bash"]`, written to *narrow* a chat to one tool, - 2125
/// silently handed that chat unattended shell execution, and - 2126
/// `mcp_allow = ["tavily/tavily_search"]` removed the approval gate - 2127
/// from every MCP call the glob still admitted. Adding a restriction - 2128
/// must never remove one. - 2129
fn channel_permission_rules(&self) -> Vec<String> { - 2130
let mut rules = self.extra_allow_snapshot(); - 2131
let Some(policy) = self.channel_policy() else { - 2132
return rules; - 2133
}; - 2134
// `Some([])` is "block this category outright"; `Some([..])` is a - 2135
// narrowing enforced by visibility, and contributes no rule here. - 2136
if policy.tools_allow.as_ref().is_some_and(|a| a.is_empty()) { - 2137
rules.extend( - 2138
CHANNEL_BLOCKABLE_TOOLS - 2139
.iter() - 2140
.map(|tool| format!("-{tool}")), - 2141
); - 2142
} - 2143
rules.extend( - 2144
policy - 2145
.tools_deny - 2146
.iter() - 2147
.map(|pattern| format!("-{pattern}")), - 2148
); - 2149
if policy.mcp_allow.as_ref().is_some_and(|a| a.is_empty()) { - 2150
rules.push("-mcp".into()); - 2151
} - 2152
rules.extend( - 2153
policy - 2154
.mcp_deny - 2155
.iter() - 2156
.map(|pattern| format!("-mcp({pattern})")), - 2157
); - 2158
rules - 2159
} - 2160
- 2161
pub fn memory_write_allowed(&self) -> bool { - 2162
if !self.channel_tool_allowed("remember") { - 2163
return false; - 2164
} - 2165
let rules = self.channel_permission_rules(); - 2166
let Ok(engine) = self.build_permission_engine(&rules) else { - 2167
return false; - 2168
}; - 2169
let mode = match self.effective_permission_mode() { - 2170
vak_config::PermissionMode::ReadOnly => vak_permission::Mode::ReadOnly, - 2171
vak_config::PermissionMode::WorkspaceWrite => vak_permission::Mode::WorkspaceWrite, - 2172
vak_config::PermissionMode::FullAccess => vak_permission::Mode::FullAccess, - 2173
}; - 2174
matches!( - 2175
engine.evaluate("remember", &serde_json::json!({}), mode, &self.inner.cwd), - 2176
vak_permission::Decision::Allow - 2177
) - 2178
} - 2179
- 2180
fn policy_matches(patterns: &[String], value: &str) -> bool { - 2181
patterns.iter().any(|pattern| { - 2182
globset::Glob::new(pattern) - 2183
.ok() - 2184
.is_some_and(|glob| glob.compile_matcher().is_match(value)) - 2185
}) - 2186
} - 2187
- 2188
fn allowed_by(allow: &Option<Vec<String>>, deny: &[String], value: &str) -> bool { - 2189
!Self::policy_matches(deny, value) - 2190
&& allow - 2191
.as_ref() - 2192
.is_none_or(|patterns| Self::policy_matches(patterns, value)) - 2193
} - 2194
- 2195
fn filter_mcp(&self, mut config: vak_config::McpConfig) -> vak_config::McpConfig { - 2196
let Some(policy) = self.channel_policy() else { - 2197
return config; - 2198
}; - 2199
config.servers.retain(|name, _| { - 2200
let server_pattern = format!("{name}/*"); - 2201
if Self::policy_matches(&policy.mcp_deny, &server_pattern) { - 2202
return false; - 2203
} - 2204
policy.mcp_allow.as_ref().is_none_or(|allow| { - 2205
Self::policy_matches(allow, &server_pattern) - 2206
|| allow - 2207
.iter() - 2208
.any(|pattern| pattern.starts_with(&format!("{name}/"))) - 2209
}) - 2210
}); - 2211
// Restrictive only: a channel can force a server's network off, but - 2212
// there is no matching grant — a server the config itself denies - 2213
// network to stays denied no matter what a channel policy says. - 2214
// Same pattern shape as mcp_allow/mcp_deny above (`name/*`). - 2215
for (name, server) in config.servers.iter_mut() { - 2216
let server_pattern = format!("{name}/*"); - 2217
if server.network && Self::policy_matches(&policy.mcp_network_deny, &server_pattern) { - 2218
server.network = false; - 2219
} - 2220
} - 2221
config - 2222
} - 2223
- 2224
/// Resolve `effective_mcp()` into the shape `vak_mcp::McpManager` wants: - 2225
/// `${VAR}` in env values expanded through the standard secret path, - 2226
/// fail-closed per server (an unresolved reference drops that server - 2227
/// rather than starting it half-configured). Shared by the per-turn - 2228
/// tool list and `mcp_manager()` so the two never resolve servers two - 2229
/// different ways. - 2230
fn resolved_mcp_servers(&self) -> Vec<(String, vak_mcp::ServerConfig)> { - 2231
self.effective_mcp() - 2232
.servers - 2233
.into_iter() - 2234
.filter_map(|(name, s)| { - 2235
let mut env = Vec::with_capacity(s.env.len()); - 2236
for (k, v) in &s.env { - 2237
match interpolate_env_var_with(v, |key| self.mcp_secret(key)) { - 2238
Some(resolved) => env.push((k.clone(), resolved)), - 2239
None => { - 2240
eprintln!("[mcp] server '{name}' skipped: unresolved environment variable in '{v}' (define it in .env)"); - 2241
return None; - 2242
} - 2243
} - 2244
} - 2245
Some(( - 2246
name, - 2247
vak_mcp::ServerConfig { - 2248
command: s.command, - 2249
args: s.args, - 2250
env, - 2251
network: s.network, - 2252
}, - 2253
)) - 2254
}) - 2255
.collect() - 2256
} - 2257
- 2258
/// Long-lived `McpManager` for this Core, reused across turns instead - 2259
/// of rebuilt per turn — `McpManager::get` caches one live connection - 2260
/// per server, so a manager rebuilt every turn meant every turn that - 2261
/// touched MCP respawned every configured server's process from - 2262
/// scratch. Returns `None` when no servers are configured. - 2263
/// - 2264
/// Keyed by a fingerprint of the resolved server set: a runtime - 2265
/// `set_mcp_servers` call or a plugin being enabled/disabled changes - 2266
/// what `effective_mcp()` returns, and the fingerprint mismatch swaps - 2267
/// in a fresh manager (dropping the old one, which shuts its clients - 2268
/// down on drop) rather than serving stale servers indefinitely. - 2269
/// - 2270
/// This only constructs the pool — no I/O, no spawn, no warm-up. A server - 2271
/// starts when a model's `mcp` call first needs it, and the pool shuts it - 2272
/// down again after `vak_mcp::IDLE_TTL` unused (AGENTS.md invariant 25). - 2273
fn mcp_manager(&self) -> Option<Arc<vak_mcp::McpManager>> { - 2274
let servers = self.resolved_mcp_servers(); - 2275
if servers.is_empty() { - 2276
*self - 2277
.inner - 2278
.mcp_cache - 2279
.lock() - 2280
.unwrap_or_else(std::sync::PoisonError::into_inner) = None; - 2281
return None; - 2282
} - 2283
let fp = mcp_fingerprint(&servers); - 2284
{ - 2285
let cache = self - 2286
.inner - 2287
.mcp_cache - 2288
.lock() - 2289
.unwrap_or_else(std::sync::PoisonError::into_inner); - 2290
if let Some(c) = cache.as_ref() - 2291
&& c.fingerprint == fp - 2292
{ - 2293
return Some(c.manager.clone()); - 2294
} - 2295
} - 2296
// The pool reports what demand taught it (a catalog learned, a - 2297
// failure recorded or cleared) and forwards a live server's - 2298
// `notifications/tools/list_changed`; both become registry hints, so - 2299
// the next turn sees the change. The loop is level-triggered, so a - 2300
// lost hint costs one tick of latency, never correctness. - 2301
let manager = vak_mcp::McpManager::new_sandboxed( - 2302
servers.into_iter().collect(), - 2303
self.inner.cwd.clone(), - 2304
self.build_sandbox(), - 2305
); - 2306
// Without a reactor (synchronous prompt assembly, one-shot tooling) - 2307
// nothing can be spawned or observed, so there is nothing to wire. - 2308
let (manager, wiring) = match tokio::runtime::Handle::try_current() { - 2309
Ok(_) => { - 2310
let (observed_tx, observed_rx) = tokio::sync::mpsc::unbounded_channel(); - 2311
let (notify_tx, notify_rx) = tokio::sync::mpsc::unbounded_channel(); - 2312
( - 2313
manager - 2314
.with_observer(observed_tx) - 2315
.with_notifications(notify_tx), - 2316
Some((observed_rx, notify_rx)), - 2317
) - 2318
} - 2319
Err(_) => (manager, None), - 2320
}; - 2321
let manager = Arc::new(manager); - 2322
if let Some((mut observed_rx, mut notify_rx)) = wiring { - 2323
let registry = self.capability_registry(); - 2324
let pool = Arc::downgrade(&manager); - 2325
tokio::spawn(async move { - 2326
loop { - 2327
let server = tokio::select! { - 2328
Some(server) = observed_rx.recv() => server, - 2329
Some((server, notification)) = notify_rx.recv() => { - 2330
if !notification.invalidates_tools() { - 2331
continue; - 2332
} - 2333
match pool.upgrade() { - 2334
// Forgetting announces, which arrives on - 2335
// `observed_rx` and hints from there. - 2336
Some(pool) => pool.forget_catalog(&server), - 2337
None => return, - 2338
} - 2339
continue; - 2340
} - 2341
else => return,
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.