Skip to main content

zagens_tools/
tool_manifest.rs

1//! Tool capability manifests (kernel-v2 M2).
2//!
3//! Static footprint declarations replace string heuristics in M3+ policy
4//! code. Built-in tools derive conservative manifests from existing
5//! `ToolCapability` metadata until each family is refined.
6
7use serde::{Deserialize, Serialize};
8
9use crate::ToolCapability;
10
11/// Where a footprint declaration came from (proposal §8.1.2).
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum FootprintProvenance {
15    /// Built-in tool: code is the declaration.
16    BuiltIn,
17    /// User explicitly trusted via config / allow flow.
18    UserConfig,
19    /// MCP server self-declaration — untrusted; may only tighten policy.
20    McpSelfDeclared,
21}
22
23/// Process / privilege class for tools that spawn subprocesses.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum SpawnClass {
27    #[default]
28    None,
29    Sandboxed,
30    Privileged,
31}
32
33/// Resource access summary (M2 minimal form; M3+ may split Fs/Net/Proc).
34#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
35pub struct ResourceSet {
36    pub workspace_read: bool,
37    pub network_read: bool,
38    pub workspace_write: bool,
39    pub network_write: bool,
40}
41
42impl ResourceSet {
43    /// True when this set declares no write-side effects (proposal §8.1).
44    #[must_use]
45    pub fn is_empty(&self) -> bool {
46        !self.workspace_write && !self.network_write
47    }
48}
49
50/// Declared resource footprint for scheduling / policy.
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct Footprint {
53    pub reads: ResourceSet,
54    pub writes: ResourceSet,
55    pub spawns: SpawnClass,
56}
57
58impl Footprint {
59    /// Whether the tool may mutate durable workspace state.
60    #[must_use]
61    pub fn writes_workspace_state(&self) -> bool {
62        self.writes.workspace_write
63    }
64}
65
66/// Per-tool manifest consumed by PolicyEngine (M3) and DAG scheduler (M4).
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct ToolManifest {
69    pub name: String,
70    pub footprint: Footprint,
71    pub provenance: FootprintProvenance,
72}
73
74impl ToolManifest {
75    /// Conservative manifest from legacy capability metadata + M1 union predicate.
76    ///
77    /// `unified_writes_state` should be `zagens_core::engine::tool_writes_state(name)`
78    /// for built-in tools so loop-guard and policy stay aligned until footprints
79    /// are hand-refined per family.
80    #[must_use]
81    pub fn derive_conservative(
82        name: &str,
83        caps: &[ToolCapability],
84        unified_writes_state: bool,
85        provenance: FootprintProvenance,
86    ) -> Self {
87        Self {
88            name: name.to_string(),
89            footprint: derive_conservative_footprint(caps, unified_writes_state),
90            provenance,
91        }
92    }
93}
94
95/// Derive a conservative footprint from capability flags and the M1 union bit.
96#[must_use]
97pub fn derive_conservative_footprint(
98    caps: &[ToolCapability],
99    unified_writes_state: bool,
100) -> Footprint {
101    let read_only = caps.contains(&ToolCapability::ReadOnly);
102    let writes_files = caps.contains(&ToolCapability::WritesFiles);
103    let executes = caps.contains(&ToolCapability::ExecutesCode);
104    let network = caps.contains(&ToolCapability::Network);
105
106    let writes = ResourceSet {
107        workspace_write: unified_writes_state || writes_files || executes,
108        network_write: network && !read_only,
109        ..Default::default()
110    };
111
112    let reads = ResourceSet {
113        workspace_read: read_only || writes_files || executes || !network,
114        network_read: network,
115        ..Default::default()
116    };
117
118    let spawns = if executes {
119        SpawnClass::Sandboxed
120    } else {
121        SpawnClass::None
122    };
123
124    Footprint {
125        reads,
126        writes,
127        spawns,
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn read_only_file_tool_has_empty_writes() {
137        let fp = derive_conservative_footprint(&[ToolCapability::ReadOnly], false);
138        assert!(fp.writes.is_empty());
139        assert!(fp.reads.workspace_read);
140        assert!(!fp.writes_workspace_state());
141    }
142
143    #[test]
144    fn unified_writes_state_union_covers_exec_shell() {
145        let caps = vec![ToolCapability::ExecutesCode, ToolCapability::Sandboxable];
146        let without = derive_conservative_footprint(&caps, false);
147        assert!(without.writes_workspace_state());
148        let with_union = derive_conservative_footprint(&caps, true);
149        assert!(with_union.writes_workspace_state());
150        assert_eq!(with_union.spawns, SpawnClass::Sandboxed);
151    }
152
153    #[test]
154    fn write_file_footprint_is_workspace_write() {
155        let fp = derive_conservative_footprint(&[ToolCapability::WritesFiles], true);
156        assert!(fp.writes.workspace_write);
157        assert!(!fp.writes.is_empty());
158    }
159
160    #[test]
161    fn mcp_network_tool_defaults_network_write_when_not_read_only() {
162        let fp = derive_conservative_footprint(
163            &[ToolCapability::Network, ToolCapability::RequiresApproval],
164            false,
165        );
166        assert!(fp.reads.network_read);
167        assert!(fp.writes.network_write);
168    }
169}