enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! AgentTool - wraps a Callable as a Tool (agent-as-tool pattern)
//!
//! ⚠️ **SECURITY WARNING: PRIVILEGED ADAPTER**
//!
//! `AgentTool` is **NOT** a general-purpose adapter. It is a **privileged** component
//! that must only be constructed by **trusted runtime components**.
//!
//! ## Why This Is Dangerous
//!
//! `AgentTool` allows exposing a `Callable` (which may be an agent, graph, or other
//! complex execution unit) as a `Tool`. This creates a powerful capability:
//!
//! - **Callable** = execution (agents, graphs, complex workflows)
//! - **Tool** = capability (side-effect functions for LLMs to invoke)
//!
//! Wrapping a Callable as a Tool means an LLM can invoke agents/graphs as tools,
//! creating recursive agent execution patterns. This is powerful but **dangerous**
//! if exposed to untrusted contexts.
//!
//! ## Security Invariant
//!
//! **AgentTool may only be constructed by trusted runtime components.**
//!
//! This means:
//! - ✅ **Allowed**: Kernel, runner, or other trusted execution components
//! - ❌ **Forbidden**: User-provided code, untrusted plugins, dynamic tool registries
//!
//! ## Why This Matters
//!
//! If `AgentTool` is exposed to untrusted contexts:
//! - Malicious users could expose arbitrary agents as tools
//! - Untrusted code could create recursive agent loops
//! - Policy boundaries could be bypassed
//! - Quota limits could be circumvented
//!
//! ## Usage Pattern
//!
//! ```rust,ignore
//! // ✅ CORRECT: Created by trusted runtime component
//! // In kernel or runner, with proper policy checks
//! let agent = Arc::new(LlmCallable::new(...));
//! let agent_tool = AgentTool::new(agent, "agent_name", "Agent description");
//!
//! // ❌ WRONG: Created from user input or untrusted source
//! // let agent_tool = AgentTool::from_user_input(...); // DON'T DO THIS!
//! ```
//!
//! ## Policy Enforcement
//!
//! Even when created by trusted components, `AgentTool` instances must still:
//! - Go through `ToolExecutor` for execution (policy enforcement)
//! - Respect `ToolPolicy` trust levels
//! - Be subject to quota limits
//! - Follow the same security boundaries as any other tool
//!
//! The privilege is in **construction**, not in **execution**.

use super::Tool;
use crate::callable::DynCallable;
use async_trait::async_trait;
use serde_json::{json, Value};

/// AgentTool - wraps a Callable as a Tool
///
/// ⚠️ **PRIVILEGED**: This adapter may only be constructed by trusted runtime components.
/// See module-level documentation for security implications.
///
/// This allows exposing agents, graphs, or other Callables as tools that can be
/// invoked by LLMs. The Callable's `run()` method is invoked with the tool's
/// JSON arguments serialized as a string.
pub struct AgentTool {
    /// The underlying callable being wrapped
    callable: DynCallable,
    /// Tool name (may differ from callable name)
    name: String,
    /// Tool description
    description: String,
    /// JSON schema for tool parameters
    parameters: Value,
}

impl AgentTool {
    /// Create a new AgentTool
    ///
    /// ⚠️ **SECURITY**: This constructor is privileged. Only trusted runtime components
    /// (kernel, runner) should create `AgentTool` instances.
    ///
    /// # Arguments
    ///
    /// * `callable` - The callable to wrap (agent, graph, etc.)
    /// * `name` - Tool name (for LLM tool schema)
    /// * `description` - Tool description (for LLM tool schema)
    /// * `parameters` - JSON schema for tool parameters (defaults to empty object)
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// // ✅ CORRECT: Created by trusted runtime
    /// let agent = Arc::new(LlmCallable::new(...));
    /// let tool = AgentTool::new(
    ///     agent,
    ///     "sub_agent",
    ///     "A sub-agent that handles specific tasks",
    ///     json!({
    ///         "type": "object",
    ///         "properties": {
    ///             "task": {"type": "string", "description": "Task description"}
    ///         },
    ///         "required": ["task"]
    ///     })
    /// );
    /// ```
    pub fn new(
        callable: DynCallable,
        name: impl Into<String>,
        description: impl Into<String>,
        parameters: Value,
    ) -> Self {
        Self {
            callable,
            name: name.into(),
            description: description.into(),
            parameters,
        }
    }

    /// Create a new AgentTool with default empty parameters schema
    ///
    /// ⚠️ **SECURITY**: Same privilege requirements as `new()`.
    pub fn simple(
        callable: DynCallable,
        name: impl Into<String>,
        description: impl Into<String>,
    ) -> Self {
        Self::new(
            callable,
            name,
            description,
            json!({
                "type": "object",
                "properties": {},
                "required": []
            }),
        )
    }
}

#[async_trait]
impl Tool for AgentTool {
    fn name(&self) -> &str {
        &self.name
    }

    fn description(&self) -> &str {
        &self.description
    }

    fn parameters_schema(&self) -> Value {
        self.parameters.clone()
    }

    async fn execute(&self, args: Value) -> anyhow::Result<Value> {
        // Serialize JSON arguments to string for Callable::run()
        let input = serde_json::to_string(&args)?;

        // Execute the underlying callable
        let output = self.callable.run(&input).await?;

        // Parse output as JSON (callable returns string, but we expect JSON)
        // If parsing fails, wrap the string in a JSON object
        match serde_json::from_str::<Value>(&output) {
            Ok(json) => Ok(json),
            Err(_) => Ok(json!({ "result": output })),
        }
    }
}