1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ToolMetadata {
12 #[serde(default)]
14 pub namespace: String,
15
16 #[serde(default)]
18 pub risk: ToolRisk,
19
20 #[serde(default)]
22 pub is_read_only: bool,
23
24 #[serde(default)]
26 pub is_concurrency_safe: bool,
27
28 #[serde(default)]
30 pub supports_streaming: bool,
31
32 #[serde(default)]
34 pub supports_batch: bool,
35
36 #[serde(default)]
38 pub supports_rollback: bool,
39
40 #[serde(default)]
42 pub max_output_bytes: Option<usize>,
43
44 #[serde(default)]
46 pub exposure: ToolExposure,
47
48 #[serde(default)]
50 pub capabilities: Vec<String>,
51
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub verifier: Option<String>,
55
56 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub examples: Vec<Value>,
59
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub tags: Vec<String>,
63
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub extensions: Option<HashMap<String, Value>>,
67}
68
69impl ToolMetadata {
70 pub fn read_only() -> Self {
72 Self {
73 is_read_only: true,
74 is_concurrency_safe: true,
75 ..Self::default()
76 }
77 }
78
79 pub fn reader(namespace: &str, tags: &[&str]) -> Self {
81 Self {
82 namespace: namespace.to_string(),
83 risk: ToolRisk::Low,
84 is_read_only: true,
85 is_concurrency_safe: true,
86 exposure: ToolExposure::Direct,
87 capabilities: vec!["repo.read".to_string()],
88 tags: tags.iter().map(|s| s.to_string()).collect(),
89 ..Self::default()
90 }
91 }
92
93 pub fn writer(namespace: &str, tags: &[&str]) -> Self {
95 Self {
96 namespace: namespace.to_string(),
97 risk: ToolRisk::Medium,
98 is_read_only: false,
99 is_concurrency_safe: false,
100 supports_rollback: true,
101 exposure: ToolExposure::Direct,
102 capabilities: vec!["repo.write".to_string()],
103 tags: tags.iter().map(|s| s.to_string()).collect(),
104 ..Self::default()
105 }
106 }
107
108 pub fn command(namespace: &str, tags: &[&str]) -> Self {
110 Self {
111 namespace: namespace.to_string(),
112 risk: ToolRisk::High,
113 is_read_only: false,
114 is_concurrency_safe: false,
115 exposure: ToolExposure::Direct,
116 capabilities: vec!["shell.exec".to_string()],
117 tags: tags.iter().map(|s| s.to_string()).collect(),
118 ..Self::default()
119 }
120 }
121
122 pub fn deferred(namespace: &str, risk: ToolRisk, tags: &[&str]) -> Self {
124 Self {
125 namespace: namespace.to_string(),
126 risk,
127 exposure: ToolExposure::Deferred,
128 tags: tags.iter().map(|s| s.to_string()).collect(),
129 ..Self::default()
130 }
131 }
132
133 pub fn internal(namespace: &str, tags: &[&str]) -> Self {
135 Self {
136 namespace: namespace.to_string(),
137 risk: ToolRisk::Low,
138 is_read_only: true,
139 is_concurrency_safe: true,
140 exposure: ToolExposure::Internal,
141 tags: tags.iter().map(|s| s.to_string()).collect(),
142 ..Self::default()
143 }
144 }
145}
146
147impl Default for ToolMetadata {
148 fn default() -> Self {
149 Self {
150 namespace: String::new(),
151 risk: ToolRisk::Unspecified,
152 is_read_only: false,
153 is_concurrency_safe: false,
154 supports_streaming: false,
155 supports_batch: false,
156 supports_rollback: false,
157 max_output_bytes: None,
158 exposure: ToolExposure::Direct,
159 capabilities: Vec::new(),
160 verifier: None,
161 examples: Vec::new(),
162 tags: Vec::new(),
163 extensions: None,
164 }
165 }
166}
167
168impl ToolMetadata {
169 pub fn is_default(&self) -> bool {
172 self.namespace.is_empty()
173 && self.risk == ToolRisk::Unspecified
174 && !self.is_read_only
175 && !self.is_concurrency_safe
176 && self.capabilities.is_empty()
177 && self.tags.is_empty()
178 }
179
180 pub fn with_exposure(mut self, exposure: ToolExposure) -> Self {
182 self.exposure = exposure;
183 self
184 }
185
186 pub fn with_capability(mut self, caps: &[&str]) -> Self {
188 self.capabilities = caps.iter().map(|s| s.to_string()).collect();
189 self
190 }
191
192 pub fn with_max_output(mut self, bytes: usize) -> Self {
194 self.max_output_bytes = Some(bytes);
195 self
196 }
197
198 pub fn with_tags(mut self, tags: &[&str]) -> Self {
200 self.tags = tags.iter().map(|s| s.to_string()).collect();
201 self
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210pub enum ToolRisk {
211 Unspecified,
213 Low,
215 Medium,
217 High,
219 Critical,
221}
222
223impl Default for ToolRisk {
224 fn default() -> Self {
225 Self::Unspecified
226 }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
234pub enum ToolExposure {
235 Direct,
237 Deferred,
239 Hidden,
241 #[serde(rename = "model_only")]
243 ModelOnly,
244 Internal,
246}
247
248impl Default for ToolExposure {
249 fn default() -> Self {
250 Self::Direct
251 }
252}
253
254pub mod capabilities {
259 pub const REPO_READ: &str = "repo.read";
260 pub const REPO_WRITE: &str = "repo.write";
261 pub const REPO_WRITE_SRC: &str = "repo.write.src";
262 pub const REPO_WRITE_TESTS: &str = "repo.write.tests";
263 pub const REPO_WRITE_DOCS: &str = "repo.write.docs";
264 pub const REPO_WRITE_CI: &str = "repo.write.ci";
265 pub const REPO_WRITE_LOCKFILE: &str = "repo.write.lockfile";
266 pub const SHELL_EXEC: &str = "shell.exec";
267 pub const SHELL_PRIVILEGED: &str = "shell.privileged";
268 pub const NETWORK_GITHUB: &str = "network.github";
269 pub const NETWORK_PACKAGE: &str = "network.package";
270 pub const NETWORK_GENERAL: &str = "network.general";
271 pub const SECRETS_READ: &str = "secrets.read";
272 pub const MCP_ACCESS: &str = "mcp.access";
273 pub const AGENT_SPAWN: &str = "agent.spawn";
274 pub const MEMORY_READ: &str = "memory.read";
275 pub const MEMORY_WRITE: &str = "memory.write";
276 pub const VERIFIER_RUN: &str = "verifier.run";
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use serde_json::json;
283
284 #[test]
285 fn metadata_default_serde_roundtrip() {
286 let m = ToolMetadata::default();
287 let json = serde_json::to_value(&m).unwrap();
288 let restored: ToolMetadata = serde_json::from_value(json).unwrap();
289 assert_eq!(m.risk, restored.risk);
290 assert_eq!(m.exposure, restored.exposure);
291 assert_eq!(m.is_read_only, restored.is_read_only);
292 }
293
294 #[test]
295 fn metadata_read_only_preset() {
296 let m = ToolMetadata::read_only();
297 assert!(m.is_read_only);
298 assert!(m.is_concurrency_safe);
299 }
300
301 #[test]
302 fn metadata_reader_preset() {
303 let m = ToolMetadata::reader("file", &["read", "search"]);
304 assert!(m.is_read_only);
305 assert!(m.is_concurrency_safe);
306 assert_eq!(m.risk, ToolRisk::Low);
307 assert_eq!(m.exposure, ToolExposure::Direct);
308 assert_eq!(m.namespace, "file");
309 assert!(m.capabilities.contains(&"repo.read".to_string()));
310 }
311
312 #[test]
313 fn metadata_writer_preset() {
314 let m = ToolMetadata::writer("file", &["edit", "patch"]);
315 assert!(!m.is_read_only);
316 assert!(!m.is_concurrency_safe);
317 assert!(m.supports_rollback);
318 assert_eq!(m.risk, ToolRisk::Medium);
319 }
320
321 #[test]
322 fn metadata_command_preset() {
323 let m = ToolMetadata::command("process", &["shell", "bash"]);
324 assert_eq!(m.risk, ToolRisk::High);
325 assert!(!m.is_concurrency_safe);
326 assert!(m.capabilities.contains(&"shell.exec".to_string()));
327 }
328
329 #[test]
330 fn metadata_deferred_preset() {
331 let m = ToolMetadata::deferred("mcp", ToolRisk::Medium, &["mcp", "external"]);
332 assert_eq!(m.exposure, ToolExposure::Deferred);
333 assert_eq!(m.risk, ToolRisk::Medium);
334 }
335
336 #[test]
337 fn metadata_internal_preset() {
338 let m = ToolMetadata::internal("runtime", &["internal"]);
339 assert_eq!(m.exposure, ToolExposure::Internal);
340 assert!(m.is_read_only);
341 assert!(m.is_concurrency_safe);
342 }
343
344 #[test]
345 fn metadata_full_serialization() {
346 let m = ToolMetadata {
347 namespace: "test".to_string(),
348 risk: ToolRisk::High,
349 is_read_only: false,
350 is_concurrency_safe: false,
351 supports_streaming: true,
352 supports_batch: false,
353 supports_rollback: true,
354 max_output_bytes: Some(65536),
355 exposure: ToolExposure::Direct,
356 capabilities: vec!["test.cap".to_string()],
357 verifier: Some("verify.test".to_string()),
358 examples: vec![json!({"key": "value"})],
359 tags: vec!["tag1".to_string(), "tag2".to_string()],
360 extensions: None,
361 };
362 let v = serde_json::to_value(&m).unwrap();
363 let restored: ToolMetadata = serde_json::from_value(v).unwrap();
364 assert_eq!(restored.namespace, "test");
365 assert_eq!(restored.risk, ToolRisk::High);
366 assert_eq!(restored.max_output_bytes, Some(65536));
367 assert_eq!(restored.verifier, Some("verify.test".to_string()));
368 assert_eq!(restored.tags.len(), 2);
369 }
370
371 #[test]
372 fn risk_default_is_unspecified() {
373 assert_eq!(ToolRisk::default(), ToolRisk::Unspecified);
374 }
375
376 #[test]
377 fn exposure_default_is_direct() {
378 assert_eq!(ToolExposure::default(), ToolExposure::Direct);
379 }
380}