Skip to main content

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}