Skip to main content

adk_managed/types/
tools.rs

1//! Tool-related types for the managed agent runtime.
2//!
3//! Defines [`ToolConfig`], [`McpServerConfig`], [`SkillRef`], and
4//! [`PermissionPolicy`] types conforming to CANON §3.7, §3.8, §3.9 wire shapes.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10/// Tool declaration: built-in or custom (client-executed).
11///
12/// Built-in tools execute server-side in the sandbox. Custom tools are
13/// client-executed — the runtime parks the loop until the client returns
14/// a result via `user.custom_tool_result`.
15///
16/// # Wire Shapes (CANON §3.7)
17///
18/// ```json
19/// {"type": "bash"}
20/// {"type": "filesystem"}
21/// {"type": "web_search"}
22/// {"type": "web_fetch"}
23/// {"type": "code_execution"}
24/// {"type": "custom", "name": "get_weather", "input_schema": {"type": "object"}}
25/// ```
26///
27/// # Example
28///
29/// ```rust
30/// use adk_managed::types::ToolConfig;
31/// use serde_json::json;
32///
33/// let tool = ToolConfig::Custom {
34///     name: "get_weather".to_string(),
35///     description: Some("Get current weather".to_string()),
36///     input_schema: json!({"type": "object", "properties": {"city": {"type": "string"}}}),
37/// };
38/// let json = serde_json::to_string(&tool).unwrap();
39/// assert!(json.contains(r#""type":"custom""#));
40/// assert!(json.contains(r#""name":"get_weather""#));
41/// ```
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(tag = "type", rename_all = "snake_case")]
44#[non_exhaustive]
45pub enum ToolConfig {
46    /// Bash shell execution tool (server-side).
47    Bash {},
48    /// Filesystem access tool (server-side).
49    Filesystem {},
50    /// Web search tool (server-side).
51    WebSearch {},
52    /// Web fetch/scrape tool (server-side).
53    WebFetch {},
54    /// Code execution tool (server-side).
55    CodeExecution {},
56    /// Custom client-executed tool.
57    Custom {
58        /// Tool name (unique within the agent definition).
59        name: String,
60        /// Optional human-readable description.
61        #[serde(skip_serializing_if = "Option::is_none")]
62        description: Option<String>,
63        /// JSON Schema for the tool's input parameters.
64        input_schema: serde_json::Value,
65    },
66}
67
68/// MCP (Model Context Protocol) server configuration.
69///
70/// Declares an MCP server that the runtime should connect to for additional
71/// tool capabilities.
72///
73/// # Wire Shape (CANON §3.8)
74///
75/// ```json
76/// {
77///   "name": "my-mcp-server",
78///   "transport": "stdio",
79///   "command": "npx",
80///   "args": ["-y", "@modelcontextprotocol/server-filesystem"],
81///   "env": {"HOME": "/tmp"},
82///   "auto_approve": ["read_file", "list_dir"]
83/// }
84/// ```
85///
86/// # Example
87///
88/// ```rust
89/// use adk_managed::types::McpServerConfig;
90///
91/// let config = McpServerConfig {
92///     name: "filesystem".to_string(),
93///     transport: "stdio".to_string(),
94///     command: Some("npx".to_string()),
95///     args: vec!["-y".to_string(), "@mcp/server-filesystem".to_string()],
96///     url: None,
97///     env: Default::default(),
98///     auto_approve: vec!["read_file".to_string()],
99/// };
100/// let json = serde_json::to_string(&config).unwrap();
101/// assert!(json.contains(r#""name":"filesystem""#));
102/// ```
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub struct McpServerConfig {
106    /// Unique name for this MCP server.
107    pub name: String,
108    /// Transport type: `"stdio"`, `"sse"`, `"streamable_http"`, etc.
109    pub transport: String,
110    /// Command to launch the server (for stdio transport).
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub command: Option<String>,
113    /// Arguments to pass to the command.
114    #[serde(default, skip_serializing_if = "Vec::is_empty")]
115    pub args: Vec<String>,
116    /// URL for network-based transports (SSE, HTTP).
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub url: Option<String>,
119    /// Environment variables to set when launching the server.
120    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
121    pub env: HashMap<String, String>,
122    /// Tool names that are pre-approved (no confirmation needed).
123    #[serde(default, skip_serializing_if = "Vec::is_empty")]
124    pub auto_approve: Vec<String>,
125}
126
127/// Reference to a skill package.
128///
129/// Skills are pre-built capability packages that the runtime can load
130/// and attach to an agent.
131///
132/// # Wire Shape (CANON §3.9)
133///
134/// ```json
135/// {"skill_id": "code-review-v2"}
136/// ```
137///
138/// # Example
139///
140/// ```rust
141/// use adk_managed::types::SkillRef;
142///
143/// let skill = SkillRef { skill_id: "code-review-v2".to_string() };
144/// let json = serde_json::to_string(&skill).unwrap();
145/// assert_eq!(json, r#"{"skill_id":"code-review-v2"}"#);
146/// ```
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct SkillRef {
149    /// The skill package identifier.
150    pub skill_id: String,
151}
152
153/// Permission policy for tool execution.
154///
155/// Controls whether tools require confirmation before execution.
156/// A default mode applies to all tools unless overridden per-tool.
157///
158/// # Wire Shape (CANON §3.9)
159///
160/// ```json
161/// {
162///   "default": "prompt",
163///   "tools": {
164///     "read_file": "auto_approve",
165///     "delete_file": "deny"
166///   }
167/// }
168/// ```
169///
170/// # Example
171///
172/// ```rust
173/// use adk_managed::types::{PermissionPolicy, PermissionMode};
174/// use std::collections::HashMap;
175///
176/// let policy = PermissionPolicy {
177///     default: PermissionMode::Prompt,
178///     tools: HashMap::from([
179///         ("read_file".to_string(), PermissionMode::AutoApprove),
180///         ("delete_file".to_string(), PermissionMode::Deny),
181///     ]),
182/// };
183/// let json = serde_json::to_value(&policy).unwrap();
184/// assert_eq!(json["default"], "auto_approve".replace("auto_approve", "prompt"));
185/// ```
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct PermissionPolicy {
188    /// Default permission mode for all tools not explicitly listed.
189    pub default: PermissionMode,
190    /// Per-tool permission overrides. Key is the tool name.
191    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
192    pub tools: HashMap<String, PermissionMode>,
193}
194
195/// Permission mode for tool execution.
196///
197/// Determines whether a tool call proceeds automatically, requires
198/// user confirmation, or is denied outright.
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(rename_all = "snake_case")]
201pub enum PermissionMode {
202    /// Tool executes without confirmation.
203    AutoApprove,
204    /// Tool requires user confirmation before execution.
205    Prompt,
206    /// Tool execution is denied.
207    Deny,
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use serde_json::json;
214
215    // --- ToolConfig tests ---
216
217    #[test]
218    fn test_bash_tool_serialization() {
219        let tool = ToolConfig::Bash {};
220        let serialized = serde_json::to_value(&tool).unwrap();
221        assert_eq!(serialized, json!({"type": "bash"}));
222
223        let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
224        assert!(matches!(deserialized, ToolConfig::Bash {}));
225    }
226
227    #[test]
228    fn test_filesystem_tool_serialization() {
229        let tool = ToolConfig::Filesystem {};
230        let serialized = serde_json::to_value(&tool).unwrap();
231        assert_eq!(serialized, json!({"type": "filesystem"}));
232
233        let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
234        assert!(matches!(deserialized, ToolConfig::Filesystem {}));
235    }
236
237    #[test]
238    fn test_web_search_tool_serialization() {
239        let tool = ToolConfig::WebSearch {};
240        let serialized = serde_json::to_value(&tool).unwrap();
241        assert_eq!(serialized, json!({"type": "web_search"}));
242
243        let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
244        assert!(matches!(deserialized, ToolConfig::WebSearch {}));
245    }
246
247    #[test]
248    fn test_web_fetch_tool_serialization() {
249        let tool = ToolConfig::WebFetch {};
250        let serialized = serde_json::to_value(&tool).unwrap();
251        assert_eq!(serialized, json!({"type": "web_fetch"}));
252
253        let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
254        assert!(matches!(deserialized, ToolConfig::WebFetch {}));
255    }
256
257    #[test]
258    fn test_code_execution_tool_serialization() {
259        let tool = ToolConfig::CodeExecution {};
260        let serialized = serde_json::to_value(&tool).unwrap();
261        assert_eq!(serialized, json!({"type": "code_execution"}));
262
263        let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
264        assert!(matches!(deserialized, ToolConfig::CodeExecution {}));
265    }
266
267    #[test]
268    fn test_custom_tool_with_description_serialization() {
269        let schema = json!({
270            "type": "object",
271            "properties": {
272                "city": {"type": "string"}
273            },
274            "required": ["city"]
275        });
276
277        let tool = ToolConfig::Custom {
278            name: "get_weather".to_string(),
279            description: Some("Get the current weather for a city".to_string()),
280            input_schema: schema.clone(),
281        };
282
283        let serialized = serde_json::to_value(&tool).unwrap();
284        assert_eq!(serialized["type"], "custom");
285        assert_eq!(serialized["name"], "get_weather");
286        assert_eq!(serialized["description"], "Get the current weather for a city");
287        assert_eq!(serialized["input_schema"], schema);
288
289        let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
290        match deserialized {
291            ToolConfig::Custom { name, description, input_schema } => {
292                assert_eq!(name, "get_weather");
293                assert_eq!(description, Some("Get the current weather for a city".to_string()));
294                assert_eq!(input_schema, schema);
295            }
296            _ => panic!("Expected Custom variant"),
297        }
298    }
299
300    #[test]
301    fn test_custom_tool_without_description_serialization() {
302        let schema = json!({"type": "object"});
303
304        let tool = ToolConfig::Custom {
305            name: "my_tool".to_string(),
306            description: None,
307            input_schema: schema.clone(),
308        };
309
310        let serialized = serde_json::to_value(&tool).unwrap();
311        assert_eq!(serialized["type"], "custom");
312        assert_eq!(serialized["name"], "my_tool");
313        // description should be omitted when None
314        assert!(serialized.get("description").is_none());
315        assert_eq!(serialized["input_schema"], schema);
316
317        let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
318        match deserialized {
319            ToolConfig::Custom { name, description, .. } => {
320                assert_eq!(name, "my_tool");
321                assert_eq!(description, None);
322            }
323            _ => panic!("Expected Custom variant"),
324        }
325    }
326
327    #[test]
328    fn test_tool_config_vec_round_trip() {
329        let tools = vec![
330            ToolConfig::Bash {},
331            ToolConfig::WebSearch {},
332            ToolConfig::Custom {
333                name: "deploy".to_string(),
334                description: Some("Deploy the app".to_string()),
335                input_schema: json!({"type": "object", "properties": {"env": {"type": "string"}}}),
336            },
337        ];
338
339        let serialized = serde_json::to_value(&tools).unwrap();
340        let deserialized: Vec<ToolConfig> = serde_json::from_value(serialized).unwrap();
341        assert_eq!(deserialized.len(), 3);
342        assert!(matches!(deserialized[0], ToolConfig::Bash {}));
343        assert!(matches!(deserialized[1], ToolConfig::WebSearch {}));
344        assert!(matches!(deserialized[2], ToolConfig::Custom { .. }));
345    }
346
347    #[test]
348    fn test_unknown_tool_type_rejected() {
349        let json_str = r#"{"type": "unknown_tool"}"#;
350        let result: Result<ToolConfig, _> = serde_json::from_str(json_str);
351        assert!(result.is_err(), "Unknown tool type should be rejected");
352    }
353
354    // --- McpServerConfig tests ---
355
356    #[test]
357    fn test_mcp_server_stdio_serialization() {
358        let config = McpServerConfig {
359            name: "filesystem".to_string(),
360            transport: "stdio".to_string(),
361            command: Some("npx".to_string()),
362            args: vec!["-y".to_string(), "@modelcontextprotocol/server-filesystem".to_string()],
363            url: None,
364            env: HashMap::from([("HOME".to_string(), "/tmp".to_string())]),
365            auto_approve: vec!["read_file".to_string(), "list_dir".to_string()],
366        };
367
368        let serialized = serde_json::to_value(&config).unwrap();
369        assert_eq!(serialized["name"], "filesystem");
370        assert_eq!(serialized["transport"], "stdio");
371        assert_eq!(serialized["command"], "npx");
372        assert_eq!(serialized["args"], json!(["-y", "@modelcontextprotocol/server-filesystem"]));
373        assert!(serialized.get("url").is_none());
374        assert_eq!(serialized["env"]["HOME"], "/tmp");
375        assert_eq!(serialized["auto_approve"], json!(["read_file", "list_dir"]));
376
377        let deserialized: McpServerConfig = serde_json::from_value(serialized).unwrap();
378        assert_eq!(deserialized.name, "filesystem");
379        assert_eq!(deserialized.transport, "stdio");
380        assert_eq!(deserialized.command, Some("npx".to_string()));
381        assert_eq!(deserialized.args.len(), 2);
382        assert_eq!(deserialized.url, None);
383        assert_eq!(deserialized.env.get("HOME").unwrap(), "/tmp");
384        assert_eq!(deserialized.auto_approve.len(), 2);
385    }
386
387    #[test]
388    fn test_mcp_server_sse_serialization() {
389        let config = McpServerConfig {
390            name: "remote-tools".to_string(),
391            transport: "sse".to_string(),
392            command: None,
393            args: vec![],
394            url: Some("https://mcp.example.com/sse".to_string()),
395            env: HashMap::new(),
396            auto_approve: vec![],
397        };
398
399        let serialized = serde_json::to_value(&config).unwrap();
400        assert_eq!(serialized["name"], "remote-tools");
401        assert_eq!(serialized["transport"], "sse");
402        // command, args, env, auto_approve should be omitted when empty/None
403        assert!(serialized.get("command").is_none());
404        assert!(serialized.get("args").is_none());
405        assert_eq!(serialized["url"], "https://mcp.example.com/sse");
406        assert!(serialized.get("env").is_none());
407        assert!(serialized.get("auto_approve").is_none());
408
409        let deserialized: McpServerConfig = serde_json::from_value(serialized).unwrap();
410        assert_eq!(deserialized.name, "remote-tools");
411        assert_eq!(deserialized.transport, "sse");
412        assert_eq!(deserialized.command, None);
413        assert!(deserialized.args.is_empty());
414        assert_eq!(deserialized.url, Some("https://mcp.example.com/sse".to_string()));
415        assert!(deserialized.env.is_empty());
416        assert!(deserialized.auto_approve.is_empty());
417    }
418
419    #[test]
420    fn test_mcp_server_from_json_string() {
421        let json_str = r#"{
422            "name": "my-server",
423            "transport": "stdio",
424            "command": "node",
425            "args": ["server.js"],
426            "env": {"PORT": "3000"}
427        }"#;
428
429        let config: McpServerConfig = serde_json::from_str(json_str).unwrap();
430        assert_eq!(config.name, "my-server");
431        assert_eq!(config.transport, "stdio");
432        assert_eq!(config.command, Some("node".to_string()));
433        assert_eq!(config.args, vec!["server.js"]);
434        assert_eq!(config.env.get("PORT").unwrap(), "3000");
435        assert!(config.auto_approve.is_empty());
436    }
437
438    // --- SkillRef tests ---
439
440    #[test]
441    fn test_skill_ref_serialization() {
442        let skill = SkillRef { skill_id: "code-review-v2".to_string() };
443
444        let serialized = serde_json::to_value(&skill).unwrap();
445        assert_eq!(serialized, json!({"skill_id": "code-review-v2"}));
446
447        let deserialized: SkillRef = serde_json::from_value(serialized).unwrap();
448        assert_eq!(deserialized.skill_id, "code-review-v2");
449    }
450
451    #[test]
452    fn test_skill_ref_vec_round_trip() {
453        let skills = vec![
454            SkillRef { skill_id: "code-review-v2".to_string() },
455            SkillRef { skill_id: "testing-assistant".to_string() },
456        ];
457
458        let serialized = serde_json::to_value(&skills).unwrap();
459        let deserialized: Vec<SkillRef> = serde_json::from_value(serialized).unwrap();
460        assert_eq!(deserialized.len(), 2);
461        assert_eq!(deserialized[0].skill_id, "code-review-v2");
462        assert_eq!(deserialized[1].skill_id, "testing-assistant");
463    }
464
465    // --- PermissionMode tests ---
466
467    #[test]
468    fn test_permission_mode_auto_approve_serialization() {
469        let mode = PermissionMode::AutoApprove;
470        let serialized = serde_json::to_value(mode).unwrap();
471        assert_eq!(serialized, json!("auto_approve"));
472
473        let deserialized: PermissionMode = serde_json::from_value(serialized).unwrap();
474        assert_eq!(deserialized, PermissionMode::AutoApprove);
475    }
476
477    #[test]
478    fn test_permission_mode_prompt_serialization() {
479        let mode = PermissionMode::Prompt;
480        let serialized = serde_json::to_value(mode).unwrap();
481        assert_eq!(serialized, json!("prompt"));
482
483        let deserialized: PermissionMode = serde_json::from_value(serialized).unwrap();
484        assert_eq!(deserialized, PermissionMode::Prompt);
485    }
486
487    #[test]
488    fn test_permission_mode_deny_serialization() {
489        let mode = PermissionMode::Deny;
490        let serialized = serde_json::to_value(mode).unwrap();
491        assert_eq!(serialized, json!("deny"));
492
493        let deserialized: PermissionMode = serde_json::from_value(serialized).unwrap();
494        assert_eq!(deserialized, PermissionMode::Deny);
495    }
496
497    // --- PermissionPolicy tests ---
498
499    #[test]
500    fn test_permission_policy_with_overrides_serialization() {
501        let policy = PermissionPolicy {
502            default: PermissionMode::Prompt,
503            tools: HashMap::from([
504                ("read_file".to_string(), PermissionMode::AutoApprove),
505                ("delete_file".to_string(), PermissionMode::Deny),
506            ]),
507        };
508
509        let serialized = serde_json::to_value(&policy).unwrap();
510        assert_eq!(serialized["default"], "prompt");
511        assert_eq!(serialized["tools"]["read_file"], "auto_approve");
512        assert_eq!(serialized["tools"]["delete_file"], "deny");
513
514        let deserialized: PermissionPolicy = serde_json::from_value(serialized).unwrap();
515        assert_eq!(deserialized.default, PermissionMode::Prompt);
516        assert_eq!(deserialized.tools.get("read_file"), Some(&PermissionMode::AutoApprove));
517        assert_eq!(deserialized.tools.get("delete_file"), Some(&PermissionMode::Deny));
518    }
519
520    #[test]
521    fn test_permission_policy_without_overrides_serialization() {
522        let policy =
523            PermissionPolicy { default: PermissionMode::AutoApprove, tools: HashMap::new() };
524
525        let serialized = serde_json::to_value(&policy).unwrap();
526        assert_eq!(serialized["default"], "auto_approve");
527        // tools should be omitted when empty
528        assert!(serialized.get("tools").is_none());
529
530        let deserialized: PermissionPolicy = serde_json::from_value(serialized).unwrap();
531        assert_eq!(deserialized.default, PermissionMode::AutoApprove);
532        assert!(deserialized.tools.is_empty());
533    }
534
535    #[test]
536    fn test_permission_policy_from_json_string() {
537        let json_str = r#"{
538            "default": "deny",
539            "tools": {
540                "read_file": "auto_approve",
541                "write_file": "prompt"
542            }
543        }"#;
544
545        let policy: PermissionPolicy = serde_json::from_str(json_str).unwrap();
546        assert_eq!(policy.default, PermissionMode::Deny);
547        assert_eq!(policy.tools.len(), 2);
548        assert_eq!(policy.tools.get("read_file"), Some(&PermissionMode::AutoApprove));
549        assert_eq!(policy.tools.get("write_file"), Some(&PermissionMode::Prompt));
550    }
551
552    #[test]
553    fn test_permission_policy_default_only_from_json() {
554        let json_str = r#"{"default": "auto_approve"}"#;
555        let policy: PermissionPolicy = serde_json::from_str(json_str).unwrap();
556        assert_eq!(policy.default, PermissionMode::AutoApprove);
557        assert!(policy.tools.is_empty());
558    }
559
560    // --- CANON wire shape conformance tests ---
561
562    #[test]
563    fn test_canon_tool_config_wire_shape() {
564        // Verify that the wire shapes match CANON §3.7 exactly
565        let tools_json = json!([
566            {"type": "bash"},
567            {"type": "filesystem"},
568            {"type": "web_search"},
569            {"type": "web_fetch"},
570            {"type": "code_execution"},
571            {
572                "type": "custom",
573                "name": "get_weather",
574                "description": "Get weather for a location",
575                "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
576            }
577        ]);
578
579        let tools: Vec<ToolConfig> = serde_json::from_value(tools_json.clone()).unwrap();
580        assert_eq!(tools.len(), 6);
581
582        // Re-serialize and compare
583        let reserialized = serde_json::to_value(&tools).unwrap();
584        assert_eq!(reserialized, tools_json);
585    }
586
587    #[test]
588    fn test_canon_mcp_server_wire_shape() {
589        // Verify CANON §3.8 wire shape
590        let mcp_json = json!({
591            "name": "my-mcp-server",
592            "transport": "stdio",
593            "command": "npx",
594            "args": ["-y", "@modelcontextprotocol/server-filesystem"],
595            "env": {"HOME": "/tmp"},
596            "auto_approve": ["read_file", "list_dir"]
597        });
598
599        let config: McpServerConfig = serde_json::from_value(mcp_json.clone()).unwrap();
600        let reserialized = serde_json::to_value(&config).unwrap();
601        assert_eq!(reserialized, mcp_json);
602    }
603
604    #[test]
605    fn test_canon_permission_policy_wire_shape() {
606        // Verify CANON §3.9 wire shape
607        let policy_json = json!({
608            "default": "prompt",
609            "tools": {
610                "read_file": "auto_approve",
611                "delete_file": "deny"
612            }
613        });
614
615        let policy: PermissionPolicy = serde_json::from_value(policy_json.clone()).unwrap();
616        let reserialized = serde_json::to_value(&policy).unwrap();
617
618        // Check structural equivalence (HashMap ordering may differ)
619        assert_eq!(reserialized["default"], policy_json["default"]);
620        assert_eq!(reserialized["tools"]["read_file"], policy_json["tools"]["read_file"]);
621        assert_eq!(reserialized["tools"]["delete_file"], policy_json["tools"]["delete_file"]);
622    }
623
624    #[test]
625    fn test_debug_and_clone_impls() {
626        let tool = ToolConfig::Custom {
627            name: "test".to_string(),
628            description: None,
629            input_schema: json!({}),
630        };
631        let debug_str = format!("{tool:?}");
632        assert!(debug_str.contains("Custom"));
633
634        let cloned = tool.clone();
635        let original_json = serde_json::to_value(&tool).unwrap();
636        let cloned_json = serde_json::to_value(&cloned).unwrap();
637        assert_eq!(original_json, cloned_json);
638
639        let mode = PermissionMode::Prompt;
640        let mode_clone = mode;
641        assert_eq!(mode, mode_clone);
642        assert_eq!(format!("{mode:?}"), "Prompt");
643    }
644}