enact_core/tool/agent_tool.rs
1//! AgentTool - wraps a Callable as a Tool (agent-as-tool pattern)
2//!
3//! ⚠️ **SECURITY WARNING: PRIVILEGED ADAPTER**
4//!
5//! `AgentTool` is **NOT** a general-purpose adapter. It is a **privileged** component
6//! that must only be constructed by **trusted runtime components**.
7//!
8//! ## Why This Is Dangerous
9//!
10//! `AgentTool` allows exposing a `Callable` (which may be an agent, graph, or other
11//! complex execution unit) as a `Tool`. This creates a powerful capability:
12//!
13//! - **Callable** = execution (agents, graphs, complex workflows)
14//! - **Tool** = capability (side-effect functions for LLMs to invoke)
15//!
16//! Wrapping a Callable as a Tool means an LLM can invoke agents/graphs as tools,
17//! creating recursive agent execution patterns. This is powerful but **dangerous**
18//! if exposed to untrusted contexts.
19//!
20//! ## Security Invariant
21//!
22//! **AgentTool may only be constructed by trusted runtime components.**
23//!
24//! This means:
25//! - ✅ **Allowed**: Kernel, runner, or other trusted execution components
26//! - ❌ **Forbidden**: User-provided code, untrusted plugins, dynamic tool registries
27//!
28//! ## Why This Matters
29//!
30//! If `AgentTool` is exposed to untrusted contexts:
31//! - Malicious users could expose arbitrary agents as tools
32//! - Untrusted code could create recursive agent loops
33//! - Policy boundaries could be bypassed
34//! - Quota limits could be circumvented
35//!
36//! ## Usage Pattern
37//!
38//! ```rust,ignore
39//! // ✅ CORRECT: Created by trusted runtime component
40//! // In kernel or runner, with proper policy checks
41//! let agent = Arc::new(LlmCallable::new(...));
42//! let agent_tool = AgentTool::new(agent, "agent_name", "Agent description");
43//!
44//! // ❌ WRONG: Created from user input or untrusted source
45//! // let agent_tool = AgentTool::from_user_input(...); // DON'T DO THIS!
46//! ```
47//!
48//! ## Policy Enforcement
49//!
50//! Even when created by trusted components, `AgentTool` instances must still:
51//! - Go through `ToolExecutor` for execution (policy enforcement)
52//! - Respect `ToolPolicy` trust levels
53//! - Be subject to quota limits
54//! - Follow the same security boundaries as any other tool
55//!
56//! The privilege is in **construction**, not in **execution**.
57
58use super::Tool;
59use crate::callable::DynCallable;
60use async_trait::async_trait;
61use serde_json::{json, Value};
62
63/// AgentTool - wraps a Callable as a Tool
64///
65/// ⚠️ **PRIVILEGED**: This adapter may only be constructed by trusted runtime components.
66/// See module-level documentation for security implications.
67///
68/// This allows exposing agents, graphs, or other Callables as tools that can be
69/// invoked by LLMs. The Callable's `run()` method is invoked with the tool's
70/// JSON arguments serialized as a string.
71pub struct AgentTool {
72 /// The underlying callable being wrapped
73 callable: DynCallable,
74 /// Tool name (may differ from callable name)
75 name: String,
76 /// Tool description
77 description: String,
78 /// JSON schema for tool parameters
79 parameters: Value,
80}
81
82impl AgentTool {
83 /// Create a new AgentTool
84 ///
85 /// ⚠️ **SECURITY**: This constructor is privileged. Only trusted runtime components
86 /// (kernel, runner) should create `AgentTool` instances.
87 ///
88 /// # Arguments
89 ///
90 /// * `callable` - The callable to wrap (agent, graph, etc.)
91 /// * `name` - Tool name (for LLM tool schema)
92 /// * `description` - Tool description (for LLM tool schema)
93 /// * `parameters` - JSON schema for tool parameters (defaults to empty object)
94 ///
95 /// # Example
96 ///
97 /// ```rust,ignore
98 /// // ✅ CORRECT: Created by trusted runtime
99 /// let agent = Arc::new(LlmCallable::new(...));
100 /// let tool = AgentTool::new(
101 /// agent,
102 /// "sub_agent",
103 /// "A sub-agent that handles specific tasks",
104 /// json!({
105 /// "type": "object",
106 /// "properties": {
107 /// "task": {"type": "string", "description": "Task description"}
108 /// },
109 /// "required": ["task"]
110 /// })
111 /// );
112 /// ```
113 pub fn new(
114 callable: DynCallable,
115 name: impl Into<String>,
116 description: impl Into<String>,
117 parameters: Value,
118 ) -> Self {
119 Self {
120 callable,
121 name: name.into(),
122 description: description.into(),
123 parameters,
124 }
125 }
126
127 /// Create a new AgentTool with default empty parameters schema
128 ///
129 /// ⚠️ **SECURITY**: Same privilege requirements as `new()`.
130 pub fn simple(
131 callable: DynCallable,
132 name: impl Into<String>,
133 description: impl Into<String>,
134 ) -> Self {
135 Self::new(
136 callable,
137 name,
138 description,
139 json!({
140 "type": "object",
141 "properties": {},
142 "required": []
143 }),
144 )
145 }
146}
147
148#[async_trait]
149impl Tool for AgentTool {
150 fn name(&self) -> &str {
151 &self.name
152 }
153
154 fn description(&self) -> &str {
155 &self.description
156 }
157
158 fn parameters_schema(&self) -> Value {
159 self.parameters.clone()
160 }
161
162 async fn execute(&self, args: Value) -> anyhow::Result<Value> {
163 // Serialize JSON arguments to string for Callable::run()
164 let input = serde_json::to_string(&args)?;
165
166 // Execute the underlying callable
167 let output = self.callable.run(&input).await?;
168
169 // Parse output as JSON (callable returns string, but we expect JSON)
170 // If parsing fails, wrap the string in a JSON object
171 match serde_json::from_str::<Value>(&output) {
172 Ok(json) => Ok(json),
173 Err(_) => Ok(json!({ "result": output })),
174 }
175 }
176}