zagens_tools/
tool_manifest.rs1use serde::{Deserialize, Serialize};
8
9use crate::ToolCapability;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum FootprintProvenance {
15 BuiltIn,
17 UserConfig,
19 McpSelfDeclared,
21}
22
23#[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#[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 #[must_use]
45 pub fn is_empty(&self) -> bool {
46 !self.workspace_write && !self.network_write
47 }
48}
49
50#[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 #[must_use]
61 pub fn writes_workspace_state(&self) -> bool {
62 self.writes.workspace_write
63 }
64}
65
66#[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 #[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#[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}