1use std::collections::BTreeSet;
16use std::sync::Arc;
17
18use async_trait::async_trait;
19use serde_json::Value;
20
21use adk_agent::LlmAgentBuilder;
22use adk_core::{Agent, Llm, Tool, ToolConfirmationPolicy, ToolContext};
23#[cfg(feature = "sandbox")]
24use adk_sandbox::{ExecRequest, Language, SandboxBackend};
25
26use crate::types::{ManagedAgentDef, PermissionMode, PermissionPolicy, ToolConfig};
27
28#[derive(Debug, thiserror::Error)]
30pub enum BuildError {
31 #[error("invalid agent definition: {0}")]
33 InvalidDef(String),
34
35 #[error("agent build failed: {0}")]
37 BuildFailed(String),
38}
39
40#[cfg(feature = "sandbox")]
65pub fn build_agent(
66 def: &ManagedAgentDef,
67 model: Arc<dyn Llm>,
68 sandbox: Option<Arc<dyn SandboxBackend>>,
69) -> Result<Arc<dyn Agent>, BuildError> {
70 let mut builder = LlmAgentBuilder::new(&def.name).model(model);
71
72 if let Some(ref system) = def.system {
74 builder = builder.instruction(system.clone());
75 }
76
77 if let Some(ref description) = def.description {
79 builder = builder.description(description.clone());
80 }
81
82 for tool_config in &def.tools {
84 let tool: Arc<dyn Tool> = match tool_config {
85 ToolConfig::Bash {} => Arc::new(ManagedBuiltinTool::new(
86 "bash",
87 "Execute bash shell commands in the agent's workspace.",
88 sandbox.clone(),
89 )),
90 ToolConfig::Filesystem {} => Arc::new(ManagedBuiltinTool::new(
91 "filesystem",
92 "Read, write, and manage files in the agent's workspace.",
93 sandbox.clone(),
94 )),
95 ToolConfig::WebSearch {} => Arc::new(ManagedBuiltinTool::new(
96 "web_search",
97 "Search the web for information.",
98 sandbox.clone(),
99 )),
100 ToolConfig::WebFetch {} => Arc::new(ManagedBuiltinTool::new(
101 "web_fetch",
102 "Fetch and extract content from a URL.",
103 sandbox.clone(),
104 )),
105 ToolConfig::CodeExecution {} => Arc::new(ManagedBuiltinTool::new(
106 "code_execution",
107 "Execute code in a sandboxed environment.",
108 sandbox.clone(),
109 )),
110 ToolConfig::Custom { name, description, input_schema } => {
111 Arc::new(ManagedCustomTool::new(
112 name.clone(),
113 description.clone().unwrap_or_default(),
114 input_schema.clone(),
115 ))
116 }
117 };
118 builder = builder.tool(tool);
119 }
120
121 if let Some(ref policy) = def.permission_policy {
123 let confirmation_policy = map_permission_policy(policy);
124 builder = builder.tool_confirmation_policy(confirmation_policy);
125 }
126
127 if !def.mcp_servers.is_empty() {
129 tracing::debug!(
130 mcp_count = def.mcp_servers.len(),
131 "MCP server configs noted (wiring deferred to session loop)"
132 );
133 }
134 if !def.skills.is_empty() {
135 tracing::debug!(
136 skill_count = def.skills.len(),
137 "skill refs noted (wiring deferred to session loop)"
138 );
139 }
140
141 let agent = builder.build().map_err(|e| BuildError::BuildFailed(e.to_string()))?;
142
143 Ok(Arc::new(agent))
144}
145
146#[cfg(not(feature = "sandbox"))]
150pub fn build_agent(
151 def: &ManagedAgentDef,
152 model: Arc<dyn Llm>,
153) -> Result<Arc<dyn Agent>, BuildError> {
154 let mut builder = LlmAgentBuilder::new(&def.name).model(model);
155
156 if let Some(ref system) = def.system {
158 builder = builder.instruction(system.clone());
159 }
160
161 if let Some(ref description) = def.description {
163 builder = builder.description(description.clone());
164 }
165
166 for tool_config in &def.tools {
168 let tool: Arc<dyn Tool> = match tool_config {
169 ToolConfig::Bash {} => Arc::new(ManagedBuiltinTool::new(
170 "bash",
171 "Execute bash shell commands in the agent's workspace.",
172 )),
173 ToolConfig::Filesystem {} => Arc::new(ManagedBuiltinTool::new(
174 "filesystem",
175 "Read, write, and manage files in the agent's workspace.",
176 )),
177 ToolConfig::WebSearch {} => {
178 Arc::new(ManagedBuiltinTool::new("web_search", "Search the web for information."))
179 }
180 ToolConfig::WebFetch {} => Arc::new(ManagedBuiltinTool::new(
181 "web_fetch",
182 "Fetch and extract content from a URL.",
183 )),
184 ToolConfig::CodeExecution {} => Arc::new(ManagedBuiltinTool::new(
185 "code_execution",
186 "Execute code in a sandboxed environment.",
187 )),
188 ToolConfig::Custom { name, description, input_schema } => {
189 Arc::new(ManagedCustomTool::new(
190 name.clone(),
191 description.clone().unwrap_or_default(),
192 input_schema.clone(),
193 ))
194 }
195 };
196 builder = builder.tool(tool);
197 }
198
199 if let Some(ref policy) = def.permission_policy {
201 let confirmation_policy = map_permission_policy(policy);
202 builder = builder.tool_confirmation_policy(confirmation_policy);
203 }
204
205 if !def.mcp_servers.is_empty() {
207 tracing::debug!(
208 mcp_count = def.mcp_servers.len(),
209 "MCP server configs noted (wiring deferred to session loop)"
210 );
211 }
212 if !def.skills.is_empty() {
213 tracing::debug!(
214 skill_count = def.skills.len(),
215 "skill refs noted (wiring deferred to session loop)"
216 );
217 }
218
219 let agent = builder.build().map_err(|e| BuildError::BuildFailed(e.to_string()))?;
220
221 Ok(Arc::new(agent))
222}
223
224fn map_permission_policy(policy: &PermissionPolicy) -> ToolConfirmationPolicy {
233 let tools_requiring_confirmation: BTreeSet<String> = policy
235 .tools
236 .iter()
237 .filter(|(_, mode)| matches!(mode, PermissionMode::Prompt | PermissionMode::Deny))
238 .map(|(name, _)| name.clone())
239 .collect();
240
241 match policy.default {
242 PermissionMode::AutoApprove => {
243 if tools_requiring_confirmation.is_empty() {
244 ToolConfirmationPolicy::Never
245 } else {
246 ToolConfirmationPolicy::PerTool(tools_requiring_confirmation)
247 }
248 }
249 PermissionMode::Prompt | PermissionMode::Deny => {
250 ToolConfirmationPolicy::Always
254 }
255 }
256}
257
258#[derive(Clone)]
271pub struct ManagedBuiltinTool {
272 name: String,
273 description: String,
274 #[cfg(feature = "sandbox")]
276 sandbox: Option<Arc<dyn SandboxBackend>>,
277}
278
279impl std::fmt::Debug for ManagedBuiltinTool {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 let mut d = f.debug_struct("ManagedBuiltinTool");
282 d.field("name", &self.name).field("description", &self.description);
283 #[cfg(feature = "sandbox")]
284 d.field("sandbox", &self.sandbox.as_ref().map(|s| s.name()));
285 d.finish()
286 }
287}
288
289impl ManagedBuiltinTool {
290 #[cfg(feature = "sandbox")]
292 pub fn new(
293 name: impl Into<String>,
294 description: impl Into<String>,
295 sandbox: Option<Arc<dyn SandboxBackend>>,
296 ) -> Self {
297 Self { name: name.into(), description: description.into(), sandbox }
298 }
299
300 #[cfg(not(feature = "sandbox"))]
302 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
303 Self { name: name.into(), description: description.into() }
304 }
305
306 async fn execute_bash(&self, args: &Value) -> adk_core::Result<Value> {
308 let command = args.get("command").and_then(|v| v.as_str()).unwrap_or_default();
309
310 if command.is_empty() {
311 return Ok(serde_json::json!({
312 "error": "no command provided",
313 "exit_code": 1
314 }));
315 }
316
317 let output = tokio::process::Command::new("sh")
318 .arg("-c")
319 .arg(command)
320 .output()
321 .await
322 .map_err(|e| adk_core::AdkError::tool(format!("failed to spawn bash: {e}")))?;
323
324 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
325 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
326 let exit_code = output.status.code().unwrap_or(-1);
327
328 Ok(serde_json::json!({
329 "stdout": stdout,
330 "stderr": stderr,
331 "exit_code": exit_code
332 }))
333 }
334
335 async fn execute_filesystem(&self, args: &Value) -> adk_core::Result<Value> {
337 let operation = args.get("operation").and_then(|v| v.as_str()).unwrap_or("read");
338
339 let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
340
341 match operation {
342 "read" => {
343 if path.is_empty() {
344 return Ok(serde_json::json!({"error": "path is required"}));
345 }
346 match tokio::fs::read_to_string(path).await {
347 Ok(content) => Ok(serde_json::json!({"content": content})),
348 Err(e) => Ok(serde_json::json!({"error": format!("read failed: {e}")})),
349 }
350 }
351 "write" => {
352 let content = args.get("content").and_then(|v| v.as_str()).unwrap_or("");
353 if path.is_empty() {
354 return Ok(serde_json::json!({"error": "path is required"}));
355 }
356 match tokio::fs::write(path, content).await {
357 Ok(()) => Ok(serde_json::json!({"status": "written", "path": path})),
358 Err(e) => Ok(serde_json::json!({"error": format!("write failed: {e}")})),
359 }
360 }
361 "list" => {
362 let target = if path.is_empty() { "." } else { path };
363 match tokio::fs::read_dir(target).await {
364 Ok(mut entries) => {
365 let mut files = Vec::new();
366 while let Ok(Some(entry)) = entries.next_entry().await {
367 files.push(entry.file_name().to_string_lossy().to_string());
368 }
369 Ok(serde_json::json!({"files": files}))
370 }
371 Err(e) => Ok(serde_json::json!({"error": format!("list failed: {e}")})),
372 }
373 }
374 other => Ok(serde_json::json!({
375 "error": format!("unsupported filesystem operation: {other}")
376 })),
377 }
378 }
379
380 #[cfg(feature = "sandbox")]
382 async fn execute_via_sandbox(
383 &self,
384 sandbox: &Arc<dyn SandboxBackend>,
385 language: Language,
386 args: &Value,
387 ) -> adk_core::Result<Value> {
388 use std::collections::HashMap;
389 use std::time::Duration;
390
391 let code = match language {
392 Language::Command => {
393 args.get("command").and_then(|v| v.as_str()).unwrap_or_default().to_string()
395 }
396 _ => {
397 args.get("code").and_then(|v| v.as_str()).unwrap_or_default().to_string()
399 }
400 };
401
402 if code.is_empty() {
403 return Ok(serde_json::json!({"error": "no code/command provided"}));
404 }
405
406 let timeout_secs = args.get("timeout").and_then(|v| v.as_u64()).unwrap_or(30);
407
408 let request = ExecRequest {
409 language,
410 code,
411 stdin: args.get("stdin").and_then(|v| v.as_str()).map(String::from),
412 timeout: Duration::from_secs(timeout_secs),
413 memory_limit_mb: args.get("memory_limit_mb").and_then(|v| v.as_u64()).map(|v| v as u32),
414 env: HashMap::new(),
415 };
416
417 match sandbox.execute(request).await {
418 Ok(result) => Ok(serde_json::json!({
419 "stdout": result.stdout,
420 "stderr": result.stderr,
421 "exit_code": result.exit_code,
422 "duration_ms": result.duration.as_millis() as u64
423 })),
424 Err(e) => Ok(serde_json::json!({
425 "error": format!("sandbox execution failed: {e}"),
426 "exit_code": -1
427 })),
428 }
429 }
430}
431
432#[async_trait]
433impl Tool for ManagedBuiltinTool {
434 fn name(&self) -> &str {
435 &self.name
436 }
437
438 fn description(&self) -> &str {
439 &self.description
440 }
441
442 async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> adk_core::Result<Value> {
443 match self.name.as_str() {
444 "bash" => {
445 #[cfg(feature = "sandbox")]
446 if let Some(ref sandbox) = self.sandbox {
447 return self.execute_via_sandbox(sandbox, Language::Command, &args).await;
448 }
449 self.execute_bash(&args).await
450 }
451 "filesystem" => self.execute_filesystem(&args).await,
452 "code_execution" => {
453 let language = args.get("language").and_then(|v| v.as_str()).unwrap_or("python");
456 let code = args.get("code").and_then(|v| v.as_str()).unwrap_or_default();
457
458 if code.is_empty() {
459 return Ok(serde_json::json!({"error": "no code provided"}));
460 }
461
462 #[cfg(feature = "sandbox")]
463 if let Some(ref sandbox) = self.sandbox {
464 let lang = match language {
465 "python" | "python3" => Language::Python,
466 "javascript" | "js" | "node" => Language::JavaScript,
467 "bash" | "sh" => Language::Command,
468 "rust" => Language::Rust,
469 "typescript" | "ts" => Language::TypeScript,
470 other => {
471 return Ok(serde_json::json!({
472 "error": format!("unsupported language for sandbox: {other}")
473 }));
474 }
475 };
476 return self.execute_via_sandbox(sandbox, lang, &args).await;
477 }
478
479 let interpreter = match language {
480 "python" | "python3" => "python3",
481 "javascript" | "js" | "node" => "node",
482 "bash" | "sh" => "sh",
483 other => {
484 return Ok(serde_json::json!({
485 "error": format!("unsupported language: {other}. Configure a sandbox backend for full language support.")
486 }));
487 }
488 };
489
490 let output = tokio::process::Command::new(interpreter)
491 .arg("-c")
492 .arg(code)
493 .output()
494 .await
495 .map_err(|e| {
496 adk_core::AdkError::tool(format!("failed to spawn {interpreter}: {e}"))
497 })?;
498
499 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
500 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
501 let exit_code = output.status.code().unwrap_or(-1);
502
503 Ok(serde_json::json!({
504 "stdout": stdout,
505 "stderr": stderr,
506 "exit_code": exit_code
507 }))
508 }
509 "web_search" => {
510 let query = args.get("query").and_then(|v| v.as_str()).unwrap_or_default();
513 Ok(serde_json::json!({
514 "error": "web_search is not configured for in-process execution. Configure an API key or use a provider with built-in search grounding.",
515 "query": query
516 }))
517 }
518 "web_fetch" => {
519 let url = args.get("url").and_then(|v| v.as_str()).unwrap_or_default();
520 Ok(serde_json::json!({
521 "error": "web_fetch is not configured for in-process execution. Configure an HTTP client or sandbox with network access.",
522 "url": url
523 }))
524 }
525 other => Err(adk_core::AdkError::tool(format!("unknown built-in tool: {other}"))),
526 }
527 }
528}
529
530#[derive(Debug, Clone)]
551pub struct ManagedCustomTool {
552 name: String,
553 description: String,
554 input_schema: Value,
555}
556
557impl ManagedCustomTool {
558 pub fn new(name: String, description: String, input_schema: Value) -> Self {
566 Self { name, description, input_schema }
567 }
568}
569
570#[async_trait]
571impl Tool for ManagedCustomTool {
572 fn name(&self) -> &str {
573 &self.name
574 }
575
576 fn description(&self) -> &str {
577 &self.description
578 }
579
580 fn parameters_schema(&self) -> Option<Value> {
581 Some(self.input_schema.clone())
582 }
583
584 fn is_long_running(&self) -> bool {
585 true
589 }
590
591 async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> adk_core::Result<Value> {
592 Ok(serde_json::json!({
596 "status": "pending_client_execution",
597 "tool": self.name,
598 "message": "This tool requires client-side execution. The result will be provided by the client.",
599 "args_received": args
600 }))
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607 use crate::types::{ManagedAgentDef, ModelRef, PermissionMode, PermissionPolicy, ToolConfig};
608 use adk_core::{Content, FinishReason, Llm, LlmRequest, LlmResponse, LlmResponseStream};
609 use async_stream::stream;
610 use std::collections::HashMap;
611
612 struct MockLlm {
614 name: String,
615 }
616
617 impl MockLlm {
618 fn new(name: &str) -> Self {
619 Self { name: name.to_string() }
620 }
621 }
622
623 #[async_trait]
624 impl Llm for MockLlm {
625 fn name(&self) -> &str {
626 &self.name
627 }
628
629 async fn generate_content(
630 &self,
631 _request: LlmRequest,
632 _stream: bool,
633 ) -> adk_core::Result<LlmResponseStream> {
634 let s = stream! {
635 yield Ok(LlmResponse {
636 content: Some(Content::new("model").with_text("Hello")),
637 partial: false,
638 turn_complete: true,
639 finish_reason: Some(FinishReason::Stop),
640 ..Default::default()
641 });
642 };
643 Ok(Box::pin(s))
644 }
645 }
646
647 #[cfg(feature = "sandbox")]
649 fn test_build_agent(def: &ManagedAgentDef, model: Arc<dyn Llm>) -> Arc<dyn Agent> {
650 build_agent(def, model, None).unwrap()
651 }
652
653 #[cfg(not(feature = "sandbox"))]
654 fn test_build_agent(def: &ManagedAgentDef, model: Arc<dyn Llm>) -> Arc<dyn Agent> {
655 build_agent(def, model).unwrap()
656 }
657
658 #[test]
659 fn test_build_agent_minimal_def() {
660 let def = ManagedAgentDef {
661 name: "test-agent".to_string(),
662 model: ModelRef::Shorthand("gemini-2.5-flash".to_string()),
663 system: None,
664 description: None,
665 tools: vec![],
666 mcp_servers: vec![],
667 skills: vec![],
668 permission_policy: None,
669 metadata: None,
670 };
671
672 let model: Arc<dyn Llm> = Arc::new(MockLlm::new("mock-model"));
673 let agent = test_build_agent(&def, model);
674
675 assert_eq!(agent.name(), "test-agent");
676 }
677
678 #[test]
679 fn test_build_agent_with_system_prompt() {
680 let def = ManagedAgentDef {
681 name: "prompted-agent".to_string(),
682 model: ModelRef::Shorthand("gemini-2.5-flash".to_string()),
683 system: Some("You are a helpful assistant.".to_string()),
684 description: Some("A helpful agent".to_string()),
685 tools: vec![],
686 mcp_servers: vec![],
687 skills: vec![],
688 permission_policy: None,
689 metadata: None,
690 };
691
692 let model: Arc<dyn Llm> = Arc::new(MockLlm::new("mock-model"));
693 let agent = test_build_agent(&def, model);
694
695 assert_eq!(agent.name(), "prompted-agent");
696 assert_eq!(agent.description(), "A helpful agent");
697 }
698
699 #[test]
700 fn test_build_agent_with_builtin_tools() {
701 let def = ManagedAgentDef {
702 name: "tool-agent".to_string(),
703 model: ModelRef::Shorthand("gemini-2.5-flash".to_string()),
704 system: None,
705 description: None,
706 tools: vec![
707 ToolConfig::Bash {},
708 ToolConfig::Filesystem {},
709 ToolConfig::WebSearch {},
710 ToolConfig::WebFetch {},
711 ToolConfig::CodeExecution {},
712 ],
713 mcp_servers: vec![],
714 skills: vec![],
715 permission_policy: None,
716 metadata: None,
717 };
718
719 let model: Arc<dyn Llm> = Arc::new(MockLlm::new("mock-model"));
720 let agent = test_build_agent(&def, model);
721 assert_eq!(agent.name(), "tool-agent");
722 }
723
724 #[test]
725 fn test_build_agent_with_custom_tool() {
726 let def = ManagedAgentDef {
727 name: "custom-tool-agent".to_string(),
728 model: ModelRef::Shorthand("gemini-2.5-flash".to_string()),
729 system: None,
730 description: None,
731 tools: vec![ToolConfig::Custom {
732 name: "get_weather".to_string(),
733 description: Some("Get current weather".to_string()),
734 input_schema: serde_json::json!({
735 "type": "object",
736 "properties": {
737 "city": {"type": "string"}
738 },
739 "required": ["city"]
740 }),
741 }],
742 mcp_servers: vec![],
743 skills: vec![],
744 permission_policy: None,
745 metadata: None,
746 };
747
748 let model: Arc<dyn Llm> = Arc::new(MockLlm::new("mock-model"));
749 let agent = test_build_agent(&def, model);
750 assert_eq!(agent.name(), "custom-tool-agent");
751 }
752
753 #[test]
754 fn test_build_agent_with_permission_policy_auto_approve() {
755 let def = ManagedAgentDef {
756 name: "auto-agent".to_string(),
757 model: ModelRef::Shorthand("gemini-2.5-flash".to_string()),
758 system: None,
759 description: None,
760 tools: vec![ToolConfig::Bash {}],
761 mcp_servers: vec![],
762 skills: vec![],
763 permission_policy: Some(PermissionPolicy {
764 default: PermissionMode::AutoApprove,
765 tools: HashMap::new(),
766 }),
767 metadata: None,
768 };
769
770 let model: Arc<dyn Llm> = Arc::new(MockLlm::new("mock-model"));
771 let agent = test_build_agent(&def, model);
772 assert_eq!(agent.name(), "auto-agent");
773 }
774
775 #[test]
776 fn test_build_agent_with_permission_policy_prompt_default() {
777 let def = ManagedAgentDef {
778 name: "prompt-agent".to_string(),
779 model: ModelRef::Shorthand("gemini-2.5-flash".to_string()),
780 system: None,
781 description: None,
782 tools: vec![ToolConfig::Bash {}],
783 mcp_servers: vec![],
784 skills: vec![],
785 permission_policy: Some(PermissionPolicy {
786 default: PermissionMode::Prompt,
787 tools: HashMap::new(),
788 }),
789 metadata: None,
790 };
791
792 let model: Arc<dyn Llm> = Arc::new(MockLlm::new("mock-model"));
793 let agent = test_build_agent(&def, model);
794 assert_eq!(agent.name(), "prompt-agent");
795 }
796
797 #[test]
798 fn test_build_agent_with_per_tool_permission() {
799 let def = ManagedAgentDef {
800 name: "mixed-agent".to_string(),
801 model: ModelRef::Shorthand("gemini-2.5-flash".to_string()),
802 system: None,
803 description: None,
804 tools: vec![ToolConfig::Bash {}, ToolConfig::Filesystem {}],
805 mcp_servers: vec![],
806 skills: vec![],
807 permission_policy: Some(PermissionPolicy {
808 default: PermissionMode::AutoApprove,
809 tools: HashMap::from([
810 ("bash".to_string(), PermissionMode::Prompt),
811 ("delete_file".to_string(), PermissionMode::Deny),
812 ]),
813 }),
814 metadata: None,
815 };
816
817 let model: Arc<dyn Llm> = Arc::new(MockLlm::new("mock-model"));
818 let agent = test_build_agent(&def, model);
819 assert_eq!(agent.name(), "mixed-agent");
820 }
821
822 #[test]
825 fn test_map_auto_approve_no_overrides() {
826 let policy =
827 PermissionPolicy { default: PermissionMode::AutoApprove, tools: HashMap::new() };
828 assert_eq!(map_permission_policy(&policy), ToolConfirmationPolicy::Never);
829 }
830
831 #[test]
832 fn test_map_prompt_default() {
833 let policy = PermissionPolicy { default: PermissionMode::Prompt, tools: HashMap::new() };
834 assert_eq!(map_permission_policy(&policy), ToolConfirmationPolicy::Always);
835 }
836
837 #[test]
838 fn test_map_deny_default() {
839 let policy = PermissionPolicy { default: PermissionMode::Deny, tools: HashMap::new() };
840 assert_eq!(map_permission_policy(&policy), ToolConfirmationPolicy::Always);
841 }
842
843 #[test]
844 fn test_map_auto_approve_with_per_tool_prompt() {
845 let policy = PermissionPolicy {
846 default: PermissionMode::AutoApprove,
847 tools: HashMap::from([
848 ("bash".to_string(), PermissionMode::Prompt),
849 ("delete_file".to_string(), PermissionMode::Deny),
850 ]),
851 };
852 let result = map_permission_policy(&policy);
853 match result {
854 ToolConfirmationPolicy::PerTool(tools) => {
855 assert!(tools.contains("bash"));
856 assert!(tools.contains("delete_file"));
857 assert_eq!(tools.len(), 2);
858 }
859 other => panic!("expected PerTool, got: {other:?}"),
860 }
861 }
862
863 #[test]
864 fn test_map_auto_approve_with_auto_approve_overrides_only() {
865 let policy = PermissionPolicy {
866 default: PermissionMode::AutoApprove,
867 tools: HashMap::from([("read_file".to_string(), PermissionMode::AutoApprove)]),
868 };
869 assert_eq!(map_permission_policy(&policy), ToolConfirmationPolicy::Never);
871 }
872
873 #[cfg(feature = "sandbox")]
877 fn make_builtin_tool(name: &str, desc: &str) -> ManagedBuiltinTool {
878 ManagedBuiltinTool::new(name, desc, None)
879 }
880
881 #[cfg(not(feature = "sandbox"))]
882 fn make_builtin_tool(name: &str, desc: &str) -> ManagedBuiltinTool {
883 ManagedBuiltinTool::new(name, desc)
884 }
885
886 #[test]
887 fn test_builtin_tool_metadata() {
888 let tool = make_builtin_tool("bash", "Execute bash commands.");
889 assert_eq!(tool.name(), "bash");
890 assert_eq!(tool.description(), "Execute bash commands.");
891 }
892
893 #[tokio::test]
894 async fn test_builtin_tool_bash_executes() {
895 let tool = make_builtin_tool("bash", "Execute bash commands.");
896 let ctx = Arc::new(adk_tool::SimpleToolContext::new("test-caller"));
897 let result = tool.execute(ctx, serde_json::json!({"command": "echo hello"})).await.unwrap();
898 assert_eq!(result["exit_code"], 0);
899 assert!(result["stdout"].as_str().unwrap().contains("hello"));
900 }
901
902 #[tokio::test]
903 async fn test_builtin_tool_web_search_returns_error() {
904 let tool = make_builtin_tool("web_search", "Search the web.");
905 let ctx = Arc::new(adk_tool::SimpleToolContext::new("test-caller"));
906 let result = tool.execute(ctx, serde_json::json!({"query": "rust lang"})).await.unwrap();
907 assert!(result["error"].as_str().unwrap().contains("not configured"));
908 }
909
910 #[test]
913 fn test_custom_tool_metadata() {
914 let schema = serde_json::json!({
915 "type": "object",
916 "properties": {"city": {"type": "string"}}
917 });
918 let tool = ManagedCustomTool::new(
919 "get_weather".to_string(),
920 "Get current weather".to_string(),
921 schema.clone(),
922 );
923 assert_eq!(tool.name(), "get_weather");
924 assert_eq!(tool.description(), "Get current weather");
925 assert_eq!(tool.parameters_schema(), Some(schema));
926 assert!(tool.is_long_running());
927 }
928
929 #[tokio::test]
930 async fn test_custom_tool_execute_returns_pending_status() {
931 let tool = ManagedCustomTool::new(
932 "my_tool".to_string(),
933 "A custom tool".to_string(),
934 serde_json::json!({"type": "object"}),
935 );
936 let ctx = Arc::new(adk_tool::SimpleToolContext::new("test-caller"));
937
938 let result = tool.execute(ctx, serde_json::json!({"city": "Seattle"})).await.unwrap();
939
940 assert_eq!(result["status"], "pending_client_execution");
941 assert_eq!(result["tool"], "my_tool");
942 assert_eq!(result["args_received"]["city"], "Seattle");
943 }
944
945 #[test]
946 fn test_custom_tool_is_long_running() {
947 let tool = ManagedCustomTool::new(
948 "deploy".to_string(),
949 "Deploy to production".to_string(),
950 serde_json::json!({"type": "object"}),
951 );
952 assert!(tool.is_long_running());
954 }
955}