Skip to main content

agentkit_tools_core/
lib.rs

1//! Core abstractions for defining, registering, executing, and governing
2//! tools in agentkit.
3//!
4//! This crate provides the [`Tool`] trait, [`ToolRegistry`],
5//! [`BasicToolExecutor`], and a layered permission system built on
6//! [`PermissionChecker`], [`PermissionPolicy`], and
7//! [`CompositePermissionChecker`]. Together these types let you:
8//!
9//! - **Define tools** by implementing [`Tool`] with a [`ToolSpec`] and
10//!   async `invoke` method.
11//! - **Register tools** in a [`ToolRegistry`] and hand it to an executor
12//!   or capability provider.
13//! - **Check permissions** before execution using composable policies
14//!   ([`PathPolicy`], [`CommandPolicy`], [`McpServerPolicy`],
15//!   [`CustomKindPolicy`]).
16//! - **Handle interruptions** (approval prompts, OAuth flows) via the
17//!   [`ToolInterruption`] / [`ApprovalRequest`] / [`AuthRequest`] types.
18//! - **Bridge to the capability layer** with [`ToolCapabilityProvider`],
19//!   which wraps every registered tool as an [`Invocable`].
20
21use std::any::Any;
22use std::collections::{BTreeMap, BTreeSet};
23use std::fmt;
24use std::path::{Path, PathBuf};
25use std::sync::Arc;
26use std::sync::atomic::{AtomicU64, Ordering};
27use std::time::Duration;
28
29use agentkit_capabilities::{
30    CapabilityContext, CapabilityError, CapabilityName, CapabilityProvider, Invocable,
31    InvocableOutput, InvocableRequest, InvocableResult, InvocableSpec, PromptProvider,
32    ResourceProvider,
33};
34use agentkit_core::{
35    ApprovalId, Item, ItemKind, MetadataMap, Part, SessionId, TaskId, ToolCallId, ToolOutput,
36    ToolResultPart, TurnCancellation, TurnId,
37};
38use async_trait::async_trait;
39use serde::{Deserialize, Serialize};
40use serde_json::Value;
41use thiserror::Error;
42
43/// Unique name identifying a [`Tool`] within a [`ToolRegistry`].
44///
45/// Tool names are used as registry keys and appear in [`ToolRequest`]s to
46/// route calls to the correct implementation. Names are compared in a
47/// case-sensitive, lexicographic order.
48///
49/// # Example
50///
51/// ```rust
52/// use agentkit_tools_core::ToolName;
53///
54/// let name = ToolName::new("file_read");
55/// assert_eq!(name.to_string(), "file_read");
56///
57/// // Also converts from &str:
58/// let name: ToolName = "shell_exec".into();
59/// ```
60#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
61pub struct ToolName(pub String);
62
63impl ToolName {
64    /// Creates a new `ToolName` from any value that converts into a [`String`].
65    pub fn new(value: impl Into<String>) -> Self {
66        Self(value.into())
67    }
68}
69
70impl fmt::Display for ToolName {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        self.0.fmt(f)
73    }
74}
75
76impl From<&str> for ToolName {
77    fn from(value: &str) -> Self {
78        Self::new(value)
79    }
80}
81
82/// Hints that describe behavioural properties of a tool.
83///
84/// These flags are advisory — they influence UI presentation and permission
85/// policies but do not enforce behaviour at runtime. For example, a
86/// permission policy may automatically require approval for tools that
87/// set `destructive_hint` to `true`.
88#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
89pub struct ToolAnnotations {
90    /// The tool only reads data and has no side-effects.
91    pub read_only_hint: bool,
92    /// The tool may perform destructive operations (e.g. file deletion).
93    pub destructive_hint: bool,
94    /// Repeated calls with the same input produce the same effect.
95    pub idempotent_hint: bool,
96    /// The tool should prompt for user approval before execution.
97    pub needs_approval_hint: bool,
98    /// The tool can stream partial results during execution.
99    pub supports_streaming_hint: bool,
100}
101
102impl ToolAnnotations {
103    /// Builds the default advisory flags.
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    /// Marks the tool as read-only.
109    pub fn read_only() -> Self {
110        Self::default().with_read_only(true)
111    }
112
113    /// Marks the tool as destructive.
114    pub fn destructive() -> Self {
115        Self::default().with_destructive(true)
116    }
117
118    /// Marks the tool as requiring approval.
119    pub fn needs_approval() -> Self {
120        Self::default().with_needs_approval(true)
121    }
122
123    /// Marks the tool as supporting streaming.
124    pub fn streaming() -> Self {
125        Self::default().with_supports_streaming(true)
126    }
127
128    pub fn with_read_only(mut self, read_only_hint: bool) -> Self {
129        self.read_only_hint = read_only_hint;
130        self
131    }
132
133    pub fn with_destructive(mut self, destructive_hint: bool) -> Self {
134        self.destructive_hint = destructive_hint;
135        self
136    }
137
138    pub fn with_idempotent(mut self, idempotent_hint: bool) -> Self {
139        self.idempotent_hint = idempotent_hint;
140        self
141    }
142
143    pub fn with_needs_approval(mut self, needs_approval_hint: bool) -> Self {
144        self.needs_approval_hint = needs_approval_hint;
145        self
146    }
147
148    pub fn with_supports_streaming(mut self, supports_streaming_hint: bool) -> Self {
149        self.supports_streaming_hint = supports_streaming_hint;
150        self
151    }
152}
153
154/// Declarative specification of a tool's identity, schema, and behavioural hints.
155///
156/// Every [`Tool`] implementation exposes a `ToolSpec` that the framework uses to
157/// advertise the tool to an LLM, validate inputs, and drive permission checks.
158///
159/// # Example
160///
161/// ```rust
162/// use agentkit_tools_core::{ToolAnnotations, ToolName, ToolSpec};
163/// use serde_json::json;
164///
165/// let spec = ToolSpec::new(
166///     ToolName::new("grep_search"),
167///     "Search files by regex pattern",
168///     json!({
169///         "type": "object",
170///         "properties": {
171///             "pattern": { "type": "string" },
172///             "path": { "type": "string" }
173///         },
174///         "required": ["pattern"]
175///     }),
176/// )
177/// .with_annotations(ToolAnnotations::read_only());
178/// ```
179#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
180pub struct ToolSpec {
181    /// Machine-readable name used to route tool calls.
182    pub name: ToolName,
183    /// Human-readable description sent to the LLM so it knows when to use this tool.
184    pub description: String,
185    /// JSON Schema describing the expected input object.
186    pub input_schema: Value,
187    /// Advisory behavioural hints (read-only, destructive, etc.).
188    pub annotations: ToolAnnotations,
189    /// Arbitrary key-value pairs for framework extensions.
190    pub metadata: MetadataMap,
191}
192
193impl ToolSpec {
194    /// Builds a tool spec with default annotations and empty metadata.
195    pub fn new(
196        name: impl Into<ToolName>,
197        description: impl Into<String>,
198        input_schema: Value,
199    ) -> Self {
200        Self {
201            name: name.into(),
202            description: description.into(),
203            input_schema,
204            annotations: ToolAnnotations::default(),
205            metadata: MetadataMap::new(),
206        }
207    }
208
209    /// Replaces the tool annotations.
210    pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
211        self.annotations = annotations;
212        self
213    }
214
215    /// Replaces the tool metadata.
216    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
217        self.metadata = metadata;
218        self
219    }
220}
221
222/// An incoming request to execute a tool.
223///
224/// Created by the agent loop when the model emits a tool-call. The
225/// [`BasicToolExecutor`] uses `tool_name` to look up the [`Tool`] in the
226/// registry and forwards this request to [`Tool::invoke`].
227#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
228pub struct ToolRequest {
229    /// Provider-assigned identifier for this specific call.
230    pub call_id: ToolCallId,
231    /// Name of the tool to invoke (must match a registered [`ToolName`]).
232    pub tool_name: ToolName,
233    /// JSON input parsed from the model's tool-call arguments.
234    pub input: Value,
235    /// Session that owns this call.
236    pub session_id: SessionId,
237    /// Turn within the session that triggered this call.
238    pub turn_id: TurnId,
239    /// Arbitrary key-value pairs for framework extensions.
240    pub metadata: MetadataMap,
241}
242
243impl ToolRequest {
244    /// Builds a tool request with empty metadata.
245    pub fn new(
246        call_id: impl Into<ToolCallId>,
247        tool_name: impl Into<ToolName>,
248        input: Value,
249        session_id: impl Into<SessionId>,
250        turn_id: impl Into<TurnId>,
251    ) -> Self {
252        Self {
253            call_id: call_id.into(),
254            tool_name: tool_name.into(),
255            input,
256            session_id: session_id.into(),
257            turn_id: turn_id.into(),
258            metadata: MetadataMap::new(),
259        }
260    }
261
262    /// Replaces the request metadata.
263    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
264        self.metadata = metadata;
265        self
266    }
267}
268
269/// The output produced by a successful tool invocation.
270///
271/// Returned from [`Tool::invoke`] and wrapped by [`ToolExecutionOutcome::Completed`]
272/// after the executor finishes permission checks and execution.
273#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
274pub struct ToolResult {
275    /// The content payload sent back to the model.
276    pub result: ToolResultPart,
277    /// Wall-clock time the tool took to run, if measured.
278    pub duration: Option<Duration>,
279    /// Arbitrary key-value pairs for framework extensions.
280    pub metadata: MetadataMap,
281}
282
283impl ToolResult {
284    /// Builds a tool result with no duration and empty metadata.
285    pub fn new(result: ToolResultPart) -> Self {
286        Self {
287            result,
288            duration: None,
289            metadata: MetadataMap::new(),
290        }
291    }
292
293    /// Sets the measured duration.
294    pub fn with_duration(mut self, duration: Duration) -> Self {
295        self.duration = Some(duration);
296        self
297    }
298
299    /// Replaces the result metadata.
300    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
301        self.metadata = metadata;
302        self
303    }
304}
305
306/// Trait for dependency injection into tool implementations.
307///
308/// Tools that need access to shared state (database handles, HTTP clients,
309/// configuration, etc.) can downcast the `&dyn ToolResources` provided in
310/// [`ToolContext`] to a concrete type.
311///
312/// The unit type `()` implements `ToolResources` and serves as the default
313/// when no shared resources are needed.
314///
315/// # Example
316///
317/// ```rust
318/// use std::any::Any;
319/// use agentkit_tools_core::ToolResources;
320///
321/// struct AppResources {
322///     project_root: std::path::PathBuf,
323/// }
324///
325/// impl ToolResources for AppResources {
326///     fn as_any(&self) -> &dyn Any {
327///         self
328///     }
329/// }
330/// ```
331pub trait ToolResources: Send + Sync {
332    /// Returns a reference to `self` as [`Any`] so callers can downcast to
333    /// the concrete resource type.
334    fn as_any(&self) -> &dyn Any;
335}
336
337impl ToolResources for () {
338    fn as_any(&self) -> &dyn Any {
339        self
340    }
341}
342
343/// Runtime context passed to every [`Tool::invoke`] call.
344///
345/// Provides the tool with access to session/turn metadata, the active
346/// permission checker, shared resources, and a cancellation signal so the
347/// tool can abort long-running work when a turn is cancelled.
348pub struct ToolContext<'a> {
349    /// Capability-layer context carrying session and turn identifiers.
350    pub capability: CapabilityContext<'a>,
351    /// The active permission checker for sub-operations the tool may perform.
352    pub permissions: &'a dyn PermissionChecker,
353    /// Shared resources (e.g. database handles, config) injected by the host.
354    pub resources: &'a dyn ToolResources,
355    /// Signal that the current turn has been cancelled by the user.
356    pub cancellation: Option<TurnCancellation>,
357}
358
359/// Owned execution context that can outlive a single stack frame.
360///
361/// This is useful for schedulers or task managers that need to move a tool
362/// execution onto another task while still constructing the borrowed
363/// [`ToolContext`] expected by existing tool implementations.
364#[derive(Clone)]
365pub struct OwnedToolContext {
366    /// Session identifier for the invocation.
367    pub session_id: SessionId,
368    /// Turn identifier for the invocation.
369    pub turn_id: TurnId,
370    /// Arbitrary invocation metadata.
371    pub metadata: MetadataMap,
372    /// Shared permission checker.
373    pub permissions: Arc<dyn PermissionChecker>,
374    /// Shared resources injected by the host.
375    pub resources: Arc<dyn ToolResources>,
376    /// Cooperative cancellation signal for the invocation.
377    pub cancellation: Option<TurnCancellation>,
378}
379
380impl OwnedToolContext {
381    /// Creates a borrowed [`ToolContext`] view over this owned context.
382    pub fn borrowed(&self) -> ToolContext<'_> {
383        ToolContext {
384            capability: CapabilityContext {
385                session_id: Some(&self.session_id),
386                turn_id: Some(&self.turn_id),
387                metadata: &self.metadata,
388            },
389            permissions: self.permissions.as_ref(),
390            resources: self.resources.as_ref(),
391            cancellation: self.cancellation.clone(),
392        }
393    }
394}
395
396/// A description of an operation that requires permission before it can proceed.
397///
398/// Tool implementations return `PermissionRequest` objects from
399/// [`Tool::proposed_requests`] so the executor can evaluate them against the
400/// active [`PermissionChecker`] before invoking the tool.
401///
402/// Built-in implementations include [`ShellPermissionRequest`],
403/// [`FileSystemPermissionRequest`], and [`McpPermissionRequest`].
404///
405/// # Implementing a custom request
406///
407/// ```rust
408/// use std::any::Any;
409/// use agentkit_core::MetadataMap;
410/// use agentkit_tools_core::PermissionRequest;
411///
412/// struct NetworkPermissionRequest {
413///     url: String,
414///     metadata: MetadataMap,
415/// }
416///
417/// impl PermissionRequest for NetworkPermissionRequest {
418///     fn kind(&self) -> &'static str { "network.http" }
419///     fn summary(&self) -> String { format!("HTTP request to {}", self.url) }
420///     fn metadata(&self) -> &MetadataMap { &self.metadata }
421///     fn as_any(&self) -> &dyn Any { self }
422/// }
423/// ```
424pub trait PermissionRequest: Send + Sync {
425    /// A dot-separated category string (e.g. `"filesystem.write"`, `"shell.command"`).
426    fn kind(&self) -> &'static str;
427    /// Human-readable one-line description of what is being requested.
428    fn summary(&self) -> String;
429    /// Arbitrary metadata attached to this request.
430    fn metadata(&self) -> &MetadataMap;
431    /// Returns `self` as [`Any`] so policies can downcast to the concrete type.
432    fn as_any(&self) -> &dyn Any;
433}
434
435/// Machine-readable code indicating why a permission was denied.
436///
437/// Returned inside a [`PermissionDenial`] so callers can programmatically
438/// react to specific denial categories.
439#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
440pub enum PermissionCode {
441    /// A filesystem path is outside the allowed set.
442    PathNotAllowed,
443    /// A shell command or executable is not permitted.
444    CommandNotAllowed,
445    /// A network operation is not permitted.
446    NetworkNotAllowed,
447    /// An MCP server is not in the trusted set.
448    ServerNotTrusted,
449    /// An MCP auth scope is not in the allowed set.
450    AuthScopeNotAllowed,
451    /// A custom permission policy explicitly denied the request.
452    CustomPolicyDenied,
453    /// No policy recognised the request kind.
454    UnknownRequest,
455}
456
457/// Structured denial produced when a [`PermissionChecker`] rejects an operation.
458///
459/// Contains a machine-readable [`PermissionCode`] and a human-readable
460/// message suitable for logging or displaying to the user.
461#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
462pub struct PermissionDenial {
463    /// Machine-readable denial category.
464    pub code: PermissionCode,
465    /// Human-readable explanation of why the operation was denied.
466    pub message: String,
467    /// Arbitrary metadata carried from the original request.
468    pub metadata: MetadataMap,
469}
470
471/// Why a permission policy is requesting human approval before proceeding.
472///
473/// Used inside [`ApprovalRequest`] so the UI layer can display context-appropriate
474/// prompts to the user.
475#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
476pub enum ApprovalReason {
477    /// The active policy always requires confirmation for this kind of operation.
478    PolicyRequiresConfirmation,
479    /// The operation was flagged as higher risk than usual.
480    EscalatedRisk,
481    /// The target (server, path, etc.) was not recognised by any policy.
482    UnknownTarget,
483    /// The operation targets a filesystem path that is not in the allowed set.
484    SensitivePath,
485    /// The shell command is not in the pre-approved allow-list.
486    SensitiveCommand,
487    /// The MCP server is not in the trusted set.
488    SensitiveServer,
489    /// The MCP auth scope is not in the pre-approved set.
490    SensitiveAuthScope,
491}
492
493/// A request sent to the host when a tool execution needs human approval.
494///
495/// The agent loop surfaces this to the user. Once the user responds, the
496/// loop can re-submit the tool call via [`ToolExecutor::execute_approved`].
497#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
498pub struct ApprovalRequest {
499    /// Runtime task identifier associated with this approval request, if any.
500    pub task_id: Option<TaskId>,
501    /// The originating tool call id when this approval was raised from a
502    /// tool invocation. Hosts can use this to resolve specific approvals.
503    pub call_id: Option<ToolCallId>,
504    /// Stable identifier so the executor can match the approval to its request.
505    pub id: ApprovalId,
506    /// The [`PermissionRequest::kind`] string that triggered the approval flow.
507    pub request_kind: String,
508    /// Why approval is needed.
509    pub reason: ApprovalReason,
510    /// Human-readable summary shown to the user.
511    pub summary: String,
512    /// Arbitrary metadata carried from the original permission request.
513    pub metadata: MetadataMap,
514}
515
516impl ApprovalRequest {
517    /// Builds an approval request with no task or call id.
518    pub fn new(
519        id: impl Into<ApprovalId>,
520        request_kind: impl Into<String>,
521        reason: ApprovalReason,
522        summary: impl Into<String>,
523    ) -> Self {
524        Self {
525            task_id: None,
526            call_id: None,
527            id: id.into(),
528            request_kind: request_kind.into(),
529            reason,
530            summary: summary.into(),
531            metadata: MetadataMap::new(),
532        }
533    }
534
535    /// Sets the associated task id.
536    pub fn with_task_id(mut self, task_id: impl Into<TaskId>) -> Self {
537        self.task_id = Some(task_id.into());
538        self
539    }
540
541    /// Sets the associated tool call id.
542    pub fn with_call_id(mut self, call_id: impl Into<ToolCallId>) -> Self {
543        self.call_id = Some(call_id.into());
544        self
545    }
546
547    /// Replaces the approval metadata.
548    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
549        self.metadata = metadata;
550        self
551    }
552}
553
554/// The user's response to an [`ApprovalRequest`].
555#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
556pub enum ApprovalDecision {
557    /// The user approved the operation.
558    Approve,
559    /// The user denied the operation, optionally with a reason.
560    Deny {
561        /// Optional human-readable explanation for the denial.
562        reason: Option<String>,
563    },
564}
565
566/// A request for authentication credentials before a tool can proceed.
567///
568/// Emitted as [`ToolInterruption::AuthRequired`] when a tool (typically an
569/// MCP integration) needs OAuth tokens, API keys, or other credentials that
570/// the user must supply interactively.
571#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
572pub struct AuthRequest {
573    /// Runtime task identifier associated with this auth request, if any.
574    pub task_id: Option<TaskId>,
575    /// Unique identifier for this auth challenge.
576    pub id: String,
577    /// Name of the authentication provider (e.g. `"github"`, `"google"`).
578    pub provider: String,
579    /// The operation that triggered the auth requirement.
580    pub operation: AuthOperation,
581    /// Provider-specific challenge data (e.g. OAuth URLs, scopes).
582    pub challenge: MetadataMap,
583}
584
585impl AuthRequest {
586    /// Builds an auth request with no task id and empty challenge metadata.
587    pub fn new(
588        id: impl Into<String>,
589        provider: impl Into<String>,
590        operation: AuthOperation,
591    ) -> Self {
592        Self {
593            task_id: None,
594            id: id.into(),
595            provider: provider.into(),
596            operation,
597            challenge: MetadataMap::new(),
598        }
599    }
600
601    /// Sets the associated task id.
602    pub fn with_task_id(mut self, task_id: impl Into<TaskId>) -> Self {
603        self.task_id = Some(task_id.into());
604        self
605    }
606
607    /// Replaces the auth challenge payload.
608    pub fn with_challenge(mut self, challenge: MetadataMap) -> Self {
609        self.challenge = challenge;
610        self
611    }
612}
613
614/// Describes the operation that triggered an [`AuthRequest`].
615///
616/// The agent loop can inspect this to decide how to present the auth
617/// challenge and where to deliver the resulting credentials.
618#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
619pub enum AuthOperation {
620    /// A local tool call that requires auth.
621    ToolCall {
622        tool_name: String,
623        input: Value,
624        call_id: Option<ToolCallId>,
625        session_id: Option<SessionId>,
626        turn_id: Option<TurnId>,
627        metadata: MetadataMap,
628    },
629    /// Connecting to an MCP server that requires auth.
630    McpConnect {
631        server_id: String,
632        metadata: MetadataMap,
633    },
634    /// Invoking a tool on an MCP server that requires auth.
635    McpToolCall {
636        server_id: String,
637        tool_name: String,
638        input: Value,
639        metadata: MetadataMap,
640    },
641    /// Reading a resource from an MCP server that requires auth.
642    McpResourceRead {
643        server_id: String,
644        resource_id: String,
645        metadata: MetadataMap,
646    },
647    /// Fetching a prompt from an MCP server that requires auth.
648    McpPromptGet {
649        server_id: String,
650        prompt_id: String,
651        args: Value,
652        metadata: MetadataMap,
653    },
654    /// An application-defined operation that requires auth.
655    Custom {
656        kind: String,
657        payload: Value,
658        metadata: MetadataMap,
659    },
660}
661
662impl AuthOperation {
663    /// Returns the MCP server ID if this operation targets one, or looks it
664    /// up in metadata for `ToolCall` and `Custom` variants.
665    pub fn server_id(&self) -> Option<&str> {
666        match self {
667            Self::McpConnect { server_id, .. }
668            | Self::McpToolCall { server_id, .. }
669            | Self::McpResourceRead { server_id, .. }
670            | Self::McpPromptGet { server_id, .. } => Some(server_id.as_str()),
671            Self::ToolCall { metadata, .. } | Self::Custom { metadata, .. } => {
672                metadata.get("server_id").and_then(Value::as_str)
673            }
674        }
675    }
676}
677
678/// The outcome of an [`AuthRequest`] after the user interacts with the auth flow.
679#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
680pub enum AuthResolution {
681    /// The user completed authentication and supplied credentials.
682    Provided {
683        /// The original auth request.
684        request: AuthRequest,
685        /// Credentials the user provided (tokens, keys, etc.).
686        credentials: MetadataMap,
687    },
688    /// The user cancelled the authentication flow.
689    Cancelled {
690        /// The original auth request that was cancelled.
691        request: AuthRequest,
692    },
693}
694
695impl AuthResolution {
696    /// Builds a successful auth resolution.
697    pub fn provided(request: AuthRequest, credentials: MetadataMap) -> Self {
698        Self::Provided {
699            request,
700            credentials,
701        }
702    }
703
704    /// Builds a cancelled auth resolution.
705    pub fn cancelled(request: AuthRequest) -> Self {
706        Self::Cancelled { request }
707    }
708
709    /// Returns a reference to the underlying [`AuthRequest`] regardless of
710    /// the resolution variant.
711    pub fn request(&self) -> &AuthRequest {
712        match self {
713            Self::Provided { request, .. } | Self::Cancelled { request } => request,
714        }
715    }
716}
717
718impl AuthRequest {
719    /// Convenience accessor that delegates to [`AuthOperation::server_id`].
720    pub fn server_id(&self) -> Option<&str> {
721        self.operation.server_id()
722    }
723}
724
725/// A tool execution was paused because it needs external input.
726///
727/// The agent loop should handle the interruption (show a prompt, open an
728/// OAuth flow, etc.) and then re-submit the tool call.
729#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
730pub enum ToolInterruption {
731    /// The operation requires human approval before it can proceed.
732    ApprovalRequired(ApprovalRequest),
733    /// The operation requires authentication credentials.
734    AuthRequired(AuthRequest),
735}
736
737/// The verdict from a [`PermissionChecker`] for a single [`PermissionRequest`].
738#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
739pub enum PermissionDecision {
740    /// The operation is allowed to proceed.
741    Allow,
742    /// The operation is denied.
743    Deny(PermissionDenial),
744    /// The operation may proceed only after the user approves.
745    RequireApproval(ApprovalRequest),
746}
747
748/// Evaluates a [`PermissionRequest`] and returns a final [`PermissionDecision`].
749///
750/// The [`BasicToolExecutor`] calls `evaluate` for every permission request
751/// returned by [`Tool::proposed_requests`] before invoking the tool. If any
752/// request is denied, execution is aborted; if any request requires approval,
753/// the executor returns a [`ToolInterruption`].
754///
755/// For composing multiple policies, see [`CompositePermissionChecker`].
756///
757/// # Example
758///
759/// ```rust
760/// use agentkit_tools_core::{PermissionChecker, PermissionDecision, PermissionRequest};
761///
762/// /// A checker that allows every operation unconditionally.
763/// struct AllowAll;
764///
765/// impl PermissionChecker for AllowAll {
766///     fn evaluate(&self, _request: &dyn PermissionRequest) -> PermissionDecision {
767///         PermissionDecision::Allow
768///     }
769/// }
770/// ```
771pub trait PermissionChecker: Send + Sync {
772    /// Evaluate a single permission request and return the decision.
773    fn evaluate(&self, request: &dyn PermissionRequest) -> PermissionDecision;
774}
775
776/// The result of a single [`PermissionPolicy`] evaluation.
777///
778/// Unlike [`PermissionDecision`], a policy can return [`PolicyMatch::NoOpinion`]
779/// to indicate it has nothing to say about this request kind, letting other
780/// policies in the [`CompositePermissionChecker`] chain decide.
781#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
782pub enum PolicyMatch {
783    /// This policy does not apply to the given request kind.
784    NoOpinion,
785    /// This policy explicitly allows the operation.
786    Allow,
787    /// This policy explicitly denies the operation.
788    Deny(PermissionDenial),
789    /// This policy requires user approval before the operation can proceed.
790    RequireApproval(ApprovalRequest),
791}
792
793/// A single, focused permission rule that contributes to a composite decision.
794///
795/// Policies are combined inside a [`CompositePermissionChecker`]. Each policy
796/// inspects the request and either returns a definitive answer or
797/// [`PolicyMatch::NoOpinion`] to defer.
798///
799/// Built-in policies: [`PathPolicy`], [`CommandPolicy`], [`McpServerPolicy`],
800/// [`CustomKindPolicy`].
801pub trait PermissionPolicy: Send + Sync {
802    /// Evaluate the request and return a match or [`PolicyMatch::NoOpinion`].
803    fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch;
804}
805
806/// Chains multiple [`PermissionPolicy`] implementations into a single [`PermissionChecker`].
807///
808/// Policies are evaluated in registration order. The first `Deny` short-circuits
809/// immediately. If any policy returns `RequireApproval`, that is used unless a
810/// later policy denies. If at least one policy returns `Allow` and none deny or
811/// require approval, the result is `Allow`. Otherwise the `fallback` decision
812/// is returned.
813///
814/// # Example
815///
816/// ```rust
817/// use agentkit_tools_core::{
818///     CommandPolicy, CompositePermissionChecker, PathPolicy, PermissionDecision,
819/// };
820///
821/// let checker = CompositePermissionChecker::new(PermissionDecision::Allow)
822///     .with_policy(PathPolicy::new().allow_root("/workspace"))
823///     .with_policy(CommandPolicy::new().allow_executable("git"));
824/// ```
825pub struct CompositePermissionChecker {
826    policies: Vec<Box<dyn PermissionPolicy>>,
827    fallback: PermissionDecision,
828}
829
830impl CompositePermissionChecker {
831    /// Creates a new composite checker with the given fallback decision.
832    ///
833    /// The fallback is used when no policy has an opinion about a request.
834    ///
835    /// # Arguments
836    ///
837    /// * `fallback` - Decision returned when every policy returns [`PolicyMatch::NoOpinion`].
838    pub fn new(fallback: PermissionDecision) -> Self {
839        Self {
840            policies: Vec::new(),
841            fallback,
842        }
843    }
844
845    /// Appends a policy to the evaluation chain and returns `self` for chaining.
846    pub fn with_policy(mut self, policy: impl PermissionPolicy + 'static) -> Self {
847        self.policies.push(Box::new(policy));
848        self
849    }
850}
851
852impl PermissionChecker for CompositePermissionChecker {
853    fn evaluate(&self, request: &dyn PermissionRequest) -> PermissionDecision {
854        let mut saw_allow = false;
855        let mut approval = None;
856
857        for policy in &self.policies {
858            match policy.evaluate(request) {
859                PolicyMatch::NoOpinion => {}
860                PolicyMatch::Allow => saw_allow = true,
861                PolicyMatch::Deny(denial) => return PermissionDecision::Deny(denial),
862                PolicyMatch::RequireApproval(req) => approval = Some(req),
863            }
864        }
865
866        if let Some(req) = approval {
867            PermissionDecision::RequireApproval(req)
868        } else if saw_allow {
869            PermissionDecision::Allow
870        } else {
871            self.fallback.clone()
872        }
873    }
874}
875
876/// Permission request for executing a shell command.
877///
878/// Evaluated by [`CommandPolicy`] to decide whether the executable, arguments,
879/// working directory, and environment variables are acceptable.
880#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
881pub struct ShellPermissionRequest {
882    /// The executable name or path (e.g. `"git"`, `"/usr/bin/curl"`).
883    pub executable: String,
884    /// Command-line arguments passed to the executable.
885    pub argv: Vec<String>,
886    /// Working directory for the command, if specified.
887    pub cwd: Option<PathBuf>,
888    /// Names of environment variables the command will receive.
889    pub env_keys: Vec<String>,
890    /// Arbitrary metadata for policy extensions.
891    pub metadata: MetadataMap,
892}
893
894impl PermissionRequest for ShellPermissionRequest {
895    fn kind(&self) -> &'static str {
896        "shell.command"
897    }
898
899    fn summary(&self) -> String {
900        if self.argv.is_empty() {
901            self.executable.clone()
902        } else {
903            format!("{} {}", self.executable, self.argv.join(" "))
904        }
905    }
906
907    fn metadata(&self) -> &MetadataMap {
908        &self.metadata
909    }
910
911    fn as_any(&self) -> &dyn Any {
912        self
913    }
914}
915
916/// Permission request for a filesystem operation.
917///
918/// Evaluated by [`PathPolicy`] to decide whether the target path(s) fall
919/// within allowed or protected directory roots.
920#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
921pub enum FileSystemPermissionRequest {
922    /// Read a file's contents.
923    Read {
924        path: PathBuf,
925        metadata: MetadataMap,
926    },
927    /// Write (create or overwrite) a file.
928    Write {
929        path: PathBuf,
930        metadata: MetadataMap,
931    },
932    /// Edit (modify in place) an existing file.
933    Edit {
934        path: PathBuf,
935        metadata: MetadataMap,
936    },
937    /// Delete a file or directory.
938    Delete {
939        path: PathBuf,
940        metadata: MetadataMap,
941    },
942    /// Move or rename a file.
943    Move {
944        from: PathBuf,
945        to: PathBuf,
946        metadata: MetadataMap,
947    },
948    /// List directory contents.
949    List {
950        path: PathBuf,
951        metadata: MetadataMap,
952    },
953    /// Create a directory (including parents).
954    CreateDir {
955        path: PathBuf,
956        metadata: MetadataMap,
957    },
958}
959
960impl FileSystemPermissionRequest {
961    fn metadata_map(&self) -> &MetadataMap {
962        match self {
963            Self::Read { metadata, .. }
964            | Self::Write { metadata, .. }
965            | Self::Edit { metadata, .. }
966            | Self::Delete { metadata, .. }
967            | Self::Move { metadata, .. }
968            | Self::List { metadata, .. }
969            | Self::CreateDir { metadata, .. } => metadata,
970        }
971    }
972}
973
974impl PermissionRequest for FileSystemPermissionRequest {
975    fn kind(&self) -> &'static str {
976        match self {
977            Self::Read { .. } => "filesystem.read",
978            Self::Write { .. } => "filesystem.write",
979            Self::Edit { .. } => "filesystem.edit",
980            Self::Delete { .. } => "filesystem.delete",
981            Self::Move { .. } => "filesystem.move",
982            Self::List { .. } => "filesystem.list",
983            Self::CreateDir { .. } => "filesystem.mkdir",
984        }
985    }
986
987    fn summary(&self) -> String {
988        match self {
989            Self::Read { path, .. } => format!("Read {}", path.display()),
990            Self::Write { path, .. } => format!("Write {}", path.display()),
991            Self::Edit { path, .. } => format!("Edit {}", path.display()),
992            Self::Delete { path, .. } => format!("Delete {}", path.display()),
993            Self::Move { from, to, .. } => {
994                format!("Move {} to {}", from.display(), to.display())
995            }
996            Self::List { path, .. } => format!("List {}", path.display()),
997            Self::CreateDir { path, .. } => format!("Create directory {}", path.display()),
998        }
999    }
1000
1001    fn metadata(&self) -> &MetadataMap {
1002        self.metadata_map()
1003    }
1004
1005    fn as_any(&self) -> &dyn Any {
1006        self
1007    }
1008}
1009
1010/// Permission request for an MCP (Model Context Protocol) operation.
1011///
1012/// Evaluated by [`McpServerPolicy`] to decide whether the target server is
1013/// trusted and the requested auth scopes are allowed.
1014#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1015pub enum McpPermissionRequest {
1016    /// Connect to an MCP server.
1017    Connect {
1018        server_id: String,
1019        metadata: MetadataMap,
1020    },
1021    /// Invoke a tool exposed by an MCP server.
1022    InvokeTool {
1023        server_id: String,
1024        tool_name: String,
1025        metadata: MetadataMap,
1026    },
1027    /// Read a resource from an MCP server.
1028    ReadResource {
1029        server_id: String,
1030        resource_id: String,
1031        metadata: MetadataMap,
1032    },
1033    /// Fetch a prompt template from an MCP server.
1034    FetchPrompt {
1035        server_id: String,
1036        prompt_id: String,
1037        metadata: MetadataMap,
1038    },
1039    /// Request an auth scope on an MCP server.
1040    UseAuthScope {
1041        server_id: String,
1042        scope: String,
1043        metadata: MetadataMap,
1044    },
1045}
1046
1047impl McpPermissionRequest {
1048    fn metadata_map(&self) -> &MetadataMap {
1049        match self {
1050            Self::Connect { metadata, .. }
1051            | Self::InvokeTool { metadata, .. }
1052            | Self::ReadResource { metadata, .. }
1053            | Self::FetchPrompt { metadata, .. }
1054            | Self::UseAuthScope { metadata, .. } => metadata,
1055        }
1056    }
1057}
1058
1059impl PermissionRequest for McpPermissionRequest {
1060    fn kind(&self) -> &'static str {
1061        match self {
1062            Self::Connect { .. } => "mcp.connect",
1063            Self::InvokeTool { .. } => "mcp.invoke_tool",
1064            Self::ReadResource { .. } => "mcp.read_resource",
1065            Self::FetchPrompt { .. } => "mcp.fetch_prompt",
1066            Self::UseAuthScope { .. } => "mcp.use_auth_scope",
1067        }
1068    }
1069
1070    fn summary(&self) -> String {
1071        match self {
1072            Self::Connect { server_id, .. } => format!("Connect MCP server {server_id}"),
1073            Self::InvokeTool {
1074                server_id,
1075                tool_name,
1076                ..
1077            } => format!("Invoke MCP tool {server_id}.{tool_name}"),
1078            Self::ReadResource {
1079                server_id,
1080                resource_id,
1081                ..
1082            } => format!("Read MCP resource {server_id}:{resource_id}"),
1083            Self::FetchPrompt {
1084                server_id,
1085                prompt_id,
1086                ..
1087            } => format!("Fetch MCP prompt {server_id}:{prompt_id}"),
1088            Self::UseAuthScope {
1089                server_id, scope, ..
1090            } => format!("Use MCP auth scope {server_id}:{scope}"),
1091        }
1092    }
1093
1094    fn metadata(&self) -> &MetadataMap {
1095        self.metadata_map()
1096    }
1097
1098    fn as_any(&self) -> &dyn Any {
1099        self
1100    }
1101}
1102
1103/// A [`PermissionPolicy`] that matches requests whose [`PermissionRequest::kind`]
1104/// starts with `"custom."` and allows or denies them by name.
1105///
1106/// Use this to govern application-defined permission categories without
1107/// writing a full policy implementation.
1108///
1109/// # Example
1110///
1111/// ```rust
1112/// use agentkit_tools_core::CustomKindPolicy;
1113///
1114/// let policy = CustomKindPolicy::new(true)
1115///     .allow_kind("custom.analytics")
1116///     .deny_kind("custom.billing");
1117/// ```
1118pub struct CustomKindPolicy {
1119    allowed_kinds: BTreeSet<String>,
1120    denied_kinds: BTreeSet<String>,
1121    require_approval_by_default: bool,
1122}
1123
1124impl CustomKindPolicy {
1125    /// Creates a new policy.
1126    ///
1127    /// # Arguments
1128    ///
1129    /// * `require_approval_by_default` - When `true`, unrecognised `custom.*`
1130    ///   kinds require approval instead of returning [`PolicyMatch::NoOpinion`].
1131    pub fn new(require_approval_by_default: bool) -> Self {
1132        Self {
1133            allowed_kinds: BTreeSet::new(),
1134            denied_kinds: BTreeSet::new(),
1135            require_approval_by_default,
1136        }
1137    }
1138
1139    /// Adds a kind string to the allow-list.
1140    pub fn allow_kind(mut self, kind: impl Into<String>) -> Self {
1141        self.allowed_kinds.insert(kind.into());
1142        self
1143    }
1144
1145    /// Adds a kind string to the deny-list.
1146    pub fn deny_kind(mut self, kind: impl Into<String>) -> Self {
1147        self.denied_kinds.insert(kind.into());
1148        self
1149    }
1150}
1151
1152impl PermissionPolicy for CustomKindPolicy {
1153    fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1154        let kind = request.kind();
1155        if !kind.starts_with("custom.") {
1156            return PolicyMatch::NoOpinion;
1157        }
1158        if self.denied_kinds.contains(kind) {
1159            return PolicyMatch::Deny(PermissionDenial {
1160                code: PermissionCode::CustomPolicyDenied,
1161                message: format!("custom permission kind {kind} is denied"),
1162                metadata: request.metadata().clone(),
1163            });
1164        }
1165        if self.allowed_kinds.contains(kind) {
1166            return PolicyMatch::Allow;
1167        }
1168        if self.require_approval_by_default {
1169            PolicyMatch::RequireApproval(ApprovalRequest {
1170                task_id: None,
1171                call_id: None,
1172                id: ApprovalId::new(format!("approval:{kind}")),
1173                request_kind: kind.to_string(),
1174                reason: ApprovalReason::PolicyRequiresConfirmation,
1175                summary: request.summary(),
1176                metadata: request.metadata().clone(),
1177            })
1178        } else {
1179            PolicyMatch::NoOpinion
1180        }
1181    }
1182}
1183
1184/// A [`PermissionPolicy`] that governs [`FileSystemPermissionRequest`]s by
1185/// checking whether target paths fall within allowed or protected directory trees.
1186///
1187/// Protected roots take priority: any path under a protected root is denied
1188/// immediately. Paths under an allowed root are permitted. Paths outside both
1189/// sets either require approval or are denied, depending on
1190/// `require_approval_outside_allowed`.
1191///
1192/// # Example
1193///
1194/// ```rust
1195/// use agentkit_tools_core::PathPolicy;
1196///
1197/// let policy = PathPolicy::new()
1198///     .allow_root("/workspace/project")
1199///     .protect_root("/workspace/project/.env")
1200///     .require_approval_outside_allowed(true);
1201/// ```
1202pub struct PathPolicy {
1203    allowed_roots: Vec<PathBuf>,
1204    protected_roots: Vec<PathBuf>,
1205    require_approval_outside_allowed: bool,
1206}
1207
1208impl PathPolicy {
1209    /// Creates a new path policy with no roots and approval required for
1210    /// paths outside allowed roots.
1211    pub fn new() -> Self {
1212        Self {
1213            allowed_roots: Vec::new(),
1214            protected_roots: Vec::new(),
1215            require_approval_outside_allowed: true,
1216        }
1217    }
1218
1219    /// Adds a directory tree that filesystem operations are allowed to target.
1220    pub fn allow_root(mut self, root: impl Into<PathBuf>) -> Self {
1221        self.allowed_roots.push(root.into());
1222        self
1223    }
1224
1225    /// Adds a directory tree that filesystem operations are never allowed to target.
1226    pub fn protect_root(mut self, root: impl Into<PathBuf>) -> Self {
1227        self.protected_roots.push(root.into());
1228        self
1229    }
1230
1231    /// When `true` (the default), paths outside allowed roots trigger an
1232    /// approval request instead of an outright denial.
1233    pub fn require_approval_outside_allowed(mut self, value: bool) -> Self {
1234        self.require_approval_outside_allowed = value;
1235        self
1236    }
1237}
1238
1239impl Default for PathPolicy {
1240    fn default() -> Self {
1241        Self::new()
1242    }
1243}
1244
1245impl PermissionPolicy for PathPolicy {
1246    fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1247        let Some(fs) = request
1248            .as_any()
1249            .downcast_ref::<FileSystemPermissionRequest>()
1250        else {
1251            return PolicyMatch::NoOpinion;
1252        };
1253
1254        let raw_paths: Vec<&Path> = match fs {
1255            FileSystemPermissionRequest::Move { from, to, .. } => {
1256                vec![from.as_path(), to.as_path()]
1257            }
1258            FileSystemPermissionRequest::Read { path, .. }
1259            | FileSystemPermissionRequest::Write { path, .. }
1260            | FileSystemPermissionRequest::Edit { path, .. }
1261            | FileSystemPermissionRequest::Delete { path, .. }
1262            | FileSystemPermissionRequest::List { path, .. }
1263            | FileSystemPermissionRequest::CreateDir { path, .. } => vec![path.as_path()],
1264        };
1265
1266        let candidate_paths: Vec<PathBuf> = raw_paths
1267            .iter()
1268            .map(|p| std::path::absolute(p).unwrap_or_else(|_| p.to_path_buf()))
1269            .collect();
1270
1271        if candidate_paths.iter().any(|path| {
1272            self.protected_roots
1273                .iter()
1274                .any(|root| path.starts_with(root))
1275        }) {
1276            return PolicyMatch::Deny(PermissionDenial {
1277                code: PermissionCode::PathNotAllowed,
1278                message: format!("path access denied for {}", fs.summary()),
1279                metadata: fs.metadata().clone(),
1280            });
1281        }
1282
1283        if self.allowed_roots.is_empty() {
1284            return PolicyMatch::NoOpinion;
1285        }
1286
1287        let all_allowed = candidate_paths
1288            .iter()
1289            .all(|path| self.allowed_roots.iter().any(|root| path.starts_with(root)));
1290
1291        if all_allowed {
1292            PolicyMatch::Allow
1293        } else if self.require_approval_outside_allowed {
1294            PolicyMatch::RequireApproval(ApprovalRequest {
1295                task_id: None,
1296                call_id: None,
1297                id: ApprovalId::new(format!("approval:{}", fs.kind())),
1298                request_kind: fs.kind().to_string(),
1299                reason: ApprovalReason::SensitivePath,
1300                summary: fs.summary(),
1301                metadata: fs.metadata().clone(),
1302            })
1303        } else {
1304            PolicyMatch::Deny(PermissionDenial {
1305                code: PermissionCode::PathNotAllowed,
1306                message: format!("path outside allowed roots for {}", fs.summary()),
1307                metadata: fs.metadata().clone(),
1308            })
1309        }
1310    }
1311}
1312
1313/// A [`PermissionPolicy`] that governs [`ShellPermissionRequest`]s by checking
1314/// the executable name, working directory, and environment variables.
1315///
1316/// Denied executables and env keys are rejected immediately. Allowed
1317/// executables pass. Unknown executables either require approval or are
1318/// denied, depending on `require_approval_for_unknown`.
1319///
1320/// # Example
1321///
1322/// ```rust
1323/// use agentkit_tools_core::CommandPolicy;
1324///
1325/// let policy = CommandPolicy::new()
1326///     .allow_executable("git")
1327///     .allow_executable("cargo")
1328///     .deny_executable("rm")
1329///     .deny_env_key("AWS_SECRET_ACCESS_KEY")
1330///     .allow_cwd("/workspace")
1331///     .require_approval_for_unknown(true);
1332/// ```
1333pub struct CommandPolicy {
1334    allowed_executables: BTreeSet<String>,
1335    denied_executables: BTreeSet<String>,
1336    allowed_cwds: Vec<PathBuf>,
1337    denied_env_keys: BTreeSet<String>,
1338    require_approval_for_unknown: bool,
1339}
1340
1341impl CommandPolicy {
1342    /// Creates a new command policy with no rules and approval required
1343    /// for unknown executables.
1344    pub fn new() -> Self {
1345        Self {
1346            allowed_executables: BTreeSet::new(),
1347            denied_executables: BTreeSet::new(),
1348            allowed_cwds: Vec::new(),
1349            denied_env_keys: BTreeSet::new(),
1350            require_approval_for_unknown: true,
1351        }
1352    }
1353
1354    /// Adds an executable name to the allow-list.
1355    pub fn allow_executable(mut self, executable: impl Into<String>) -> Self {
1356        self.allowed_executables.insert(executable.into());
1357        self
1358    }
1359
1360    /// Adds an executable name to the deny-list.
1361    pub fn deny_executable(mut self, executable: impl Into<String>) -> Self {
1362        self.denied_executables.insert(executable.into());
1363        self
1364    }
1365
1366    /// Adds a directory root that commands are allowed to run in.
1367    pub fn allow_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
1368        self.allowed_cwds.push(cwd.into());
1369        self
1370    }
1371
1372    /// Adds an environment variable name to the deny-list.
1373    pub fn deny_env_key(mut self, key: impl Into<String>) -> Self {
1374        self.denied_env_keys.insert(key.into());
1375        self
1376    }
1377
1378    /// When `true` (the default), executables not in the allow-list trigger
1379    /// an approval request instead of an outright denial.
1380    pub fn require_approval_for_unknown(mut self, value: bool) -> Self {
1381        self.require_approval_for_unknown = value;
1382        self
1383    }
1384}
1385
1386impl Default for CommandPolicy {
1387    fn default() -> Self {
1388        Self::new()
1389    }
1390}
1391
1392impl PermissionPolicy for CommandPolicy {
1393    fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1394        let Some(shell) = request.as_any().downcast_ref::<ShellPermissionRequest>() else {
1395            return PolicyMatch::NoOpinion;
1396        };
1397
1398        if self.denied_executables.contains(&shell.executable)
1399            || shell
1400                .env_keys
1401                .iter()
1402                .any(|key| self.denied_env_keys.contains(key))
1403        {
1404            return PolicyMatch::Deny(PermissionDenial {
1405                code: PermissionCode::CommandNotAllowed,
1406                message: format!("command denied for {}", shell.summary()),
1407                metadata: shell.metadata().clone(),
1408            });
1409        }
1410
1411        if let Some(cwd) = &shell.cwd
1412            && !self.allowed_cwds.is_empty()
1413            && !self.allowed_cwds.iter().any(|root| cwd.starts_with(root))
1414        {
1415            return PolicyMatch::RequireApproval(ApprovalRequest {
1416                task_id: None,
1417                call_id: None,
1418                id: ApprovalId::new("approval:shell.cwd"),
1419                request_kind: shell.kind().to_string(),
1420                reason: ApprovalReason::SensitiveCommand,
1421                summary: shell.summary(),
1422                metadata: shell.metadata().clone(),
1423            });
1424        }
1425
1426        if self.allowed_executables.is_empty()
1427            || self.allowed_executables.contains(&shell.executable)
1428        {
1429            PolicyMatch::Allow
1430        } else if self.require_approval_for_unknown {
1431            PolicyMatch::RequireApproval(ApprovalRequest {
1432                task_id: None,
1433                call_id: None,
1434                id: ApprovalId::new("approval:shell.command"),
1435                request_kind: shell.kind().to_string(),
1436                reason: ApprovalReason::SensitiveCommand,
1437                summary: shell.summary(),
1438                metadata: shell.metadata().clone(),
1439            })
1440        } else {
1441            PolicyMatch::Deny(PermissionDenial {
1442                code: PermissionCode::CommandNotAllowed,
1443                message: format!("executable {} is not allowed", shell.executable),
1444                metadata: shell.metadata().clone(),
1445            })
1446        }
1447    }
1448}
1449
1450/// A [`PermissionPolicy`] that governs [`McpPermissionRequest`]s by checking
1451/// whether the target server is trusted and the requested auth scopes are
1452/// in the allow-list.
1453///
1454/// # Example
1455///
1456/// ```rust
1457/// use agentkit_tools_core::McpServerPolicy;
1458///
1459/// let policy = McpServerPolicy::new()
1460///     .trust_server("github-mcp")
1461///     .allow_auth_scope("repo:read");
1462/// ```
1463pub struct McpServerPolicy {
1464    trusted_servers: BTreeSet<String>,
1465    allowed_auth_scopes: BTreeSet<String>,
1466    require_approval_for_untrusted: bool,
1467}
1468
1469impl McpServerPolicy {
1470    /// Creates a new MCP server policy with approval required for untrusted
1471    /// servers.
1472    pub fn new() -> Self {
1473        Self {
1474            trusted_servers: BTreeSet::new(),
1475            allowed_auth_scopes: BTreeSet::new(),
1476            require_approval_for_untrusted: true,
1477        }
1478    }
1479
1480    /// Marks a server as trusted so operations targeting it are allowed.
1481    pub fn trust_server(mut self, server_id: impl Into<String>) -> Self {
1482        self.trusted_servers.insert(server_id.into());
1483        self
1484    }
1485
1486    /// Adds an auth scope to the allow-list.
1487    pub fn allow_auth_scope(mut self, scope: impl Into<String>) -> Self {
1488        self.allowed_auth_scopes.insert(scope.into());
1489        self
1490    }
1491}
1492
1493impl Default for McpServerPolicy {
1494    fn default() -> Self {
1495        Self::new()
1496    }
1497}
1498
1499impl PermissionPolicy for McpServerPolicy {
1500    fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1501        let Some(mcp) = request.as_any().downcast_ref::<McpPermissionRequest>() else {
1502            return PolicyMatch::NoOpinion;
1503        };
1504
1505        let server_id = match mcp {
1506            McpPermissionRequest::Connect { server_id, .. }
1507            | McpPermissionRequest::InvokeTool { server_id, .. }
1508            | McpPermissionRequest::ReadResource { server_id, .. }
1509            | McpPermissionRequest::FetchPrompt { server_id, .. }
1510            | McpPermissionRequest::UseAuthScope { server_id, .. } => server_id,
1511        };
1512
1513        if !self.trusted_servers.is_empty() && !self.trusted_servers.contains(server_id) {
1514            return if self.require_approval_for_untrusted {
1515                PolicyMatch::RequireApproval(ApprovalRequest {
1516                    task_id: None,
1517                    call_id: None,
1518                    id: ApprovalId::new(format!("approval:mcp:{server_id}")),
1519                    request_kind: mcp.kind().to_string(),
1520                    reason: ApprovalReason::SensitiveServer,
1521                    summary: mcp.summary(),
1522                    metadata: mcp.metadata().clone(),
1523                })
1524            } else {
1525                PolicyMatch::Deny(PermissionDenial {
1526                    code: PermissionCode::ServerNotTrusted,
1527                    message: format!("MCP server {server_id} is not trusted"),
1528                    metadata: mcp.metadata().clone(),
1529                })
1530            };
1531        }
1532
1533        if let McpPermissionRequest::UseAuthScope { scope, .. } = mcp
1534            && !self.allowed_auth_scopes.is_empty()
1535            && !self.allowed_auth_scopes.contains(scope)
1536        {
1537            return PolicyMatch::Deny(PermissionDenial {
1538                code: PermissionCode::AuthScopeNotAllowed,
1539                message: format!("MCP auth scope {scope} is not allowed"),
1540                metadata: mcp.metadata().clone(),
1541            });
1542        }
1543
1544        PolicyMatch::Allow
1545    }
1546}
1547
1548/// The central abstraction for an executable tool in an agentkit agent.
1549///
1550/// Implement this trait to define a tool that an LLM can call. Each tool
1551/// provides a [`ToolSpec`] describing its name, schema, and hints, optional
1552/// permission requests via [`proposed_requests`](Tool::proposed_requests),
1553/// and the actual execution logic in [`invoke`](Tool::invoke).
1554///
1555/// # Example
1556///
1557/// ```rust
1558/// use agentkit_core::{MetadataMap, ToolOutput, ToolResultPart};
1559/// use agentkit_tools_core::{
1560///     Tool, ToolContext, ToolError, ToolName, ToolRequest, ToolResult, ToolSpec,
1561/// };
1562/// use async_trait::async_trait;
1563/// use serde_json::json;
1564///
1565/// struct TimeTool {
1566///     spec: ToolSpec,
1567/// }
1568///
1569/// impl TimeTool {
1570///     fn new() -> Self {
1571///         Self {
1572///             spec: ToolSpec::new(
1573///                 ToolName::new("current_time"),
1574///                 "Returns the current UTC time",
1575///                 json!({ "type": "object" }),
1576///             ),
1577///         }
1578///     }
1579/// }
1580///
1581/// #[async_trait]
1582/// impl Tool for TimeTool {
1583///     fn spec(&self) -> &ToolSpec {
1584///         &self.spec
1585///     }
1586///
1587///     async fn invoke(
1588///         &self,
1589///         request: ToolRequest,
1590///         _ctx: &mut ToolContext<'_>,
1591///     ) -> Result<ToolResult, ToolError> {
1592///         Ok(ToolResult::new(ToolResultPart::success(
1593///             request.call_id,
1594///             ToolOutput::text("2026-03-22T12:00:00Z"),
1595///         )))
1596///     }
1597/// }
1598/// ```
1599#[async_trait]
1600pub trait Tool: Send + Sync {
1601    /// Returns the static specification for this tool.
1602    fn spec(&self) -> &ToolSpec;
1603
1604    /// Returns the current specification for this tool, if it should be
1605    /// advertised right now.
1606    ///
1607    /// Most tools are static and can rely on the default implementation,
1608    /// which clones [`spec`](Self::spec). Override this when the description
1609    /// or input schema should reflect runtime state, or when the tool should
1610    /// be temporarily hidden from the model.
1611    fn current_spec(&self) -> Option<ToolSpec> {
1612        Some(self.spec().clone())
1613    }
1614
1615    /// Returns permission requests the executor should evaluate before calling
1616    /// [`invoke`](Tool::invoke).
1617    ///
1618    /// The default implementation returns an empty list (no permissions needed).
1619    /// Override this to declare filesystem, shell, or custom permission
1620    /// requirements based on the incoming request.
1621    ///
1622    /// # Errors
1623    ///
1624    /// Return [`ToolError::InvalidInput`] if the request input is malformed
1625    /// and permission requests cannot be constructed.
1626    fn proposed_requests(
1627        &self,
1628        _request: &ToolRequest,
1629    ) -> Result<Vec<Box<dyn PermissionRequest>>, ToolError> {
1630        Ok(Vec::new())
1631    }
1632
1633    /// Executes the tool and returns a result or error.
1634    ///
1635    /// # Errors
1636    ///
1637    /// Return an appropriate [`ToolError`] variant on failure. Returning
1638    /// [`ToolError::AuthRequired`] causes the executor to emit a
1639    /// [`ToolInterruption::AuthRequired`] instead of treating it as a
1640    /// hard failure.
1641    async fn invoke(
1642        &self,
1643        request: ToolRequest,
1644        ctx: &mut ToolContext<'_>,
1645    ) -> Result<ToolResult, ToolError>;
1646}
1647
1648/// A name-keyed collection of [`Tool`] implementations.
1649///
1650/// The registry owns `Arc`-wrapped tools and is passed to a
1651/// [`BasicToolExecutor`] (or consumed by [`ToolCapabilityProvider`]) so the
1652/// agent loop can look up tools by name at execution time.
1653///
1654/// # Example
1655///
1656/// ```rust
1657/// use agentkit_tools_core::ToolRegistry;
1658/// # use agentkit_tools_core::{Tool, ToolContext, ToolError, ToolName, ToolRequest, ToolResult, ToolSpec};
1659/// # use async_trait::async_trait;
1660/// # use serde_json::json;
1661/// # struct NoopTool(ToolSpec);
1662/// # #[async_trait]
1663/// # impl Tool for NoopTool {
1664/// #     fn spec(&self) -> &ToolSpec { &self.0 }
1665/// #     async fn invoke(&self, _r: ToolRequest, _c: &mut ToolContext<'_>) -> Result<ToolResult, ToolError> { todo!() }
1666/// # }
1667///
1668/// let registry = ToolRegistry::new()
1669///     .with(NoopTool(ToolSpec::new(
1670///         ToolName::new("noop"),
1671///         "Does nothing",
1672///         json!({"type": "object"}),
1673///     )));
1674///
1675/// assert!(registry.get(&ToolName::new("noop")).is_some());
1676/// assert_eq!(registry.specs().len(), 1);
1677/// ```
1678#[derive(Clone, Default)]
1679pub struct ToolRegistry {
1680    tools: BTreeMap<ToolName, Arc<dyn Tool>>,
1681}
1682
1683impl ToolRegistry {
1684    /// Creates an empty registry.
1685    pub fn new() -> Self {
1686        Self::default()
1687    }
1688
1689    /// Registers a tool by value and returns `&mut self` for imperative chaining.
1690    pub fn register<T>(&mut self, tool: T) -> &mut Self
1691    where
1692        T: Tool + 'static,
1693    {
1694        self.tools.insert(tool.spec().name.clone(), Arc::new(tool));
1695        self
1696    }
1697
1698    /// Registers a tool by value and returns `self` for builder-style chaining.
1699    pub fn with<T>(mut self, tool: T) -> Self
1700    where
1701        T: Tool + 'static,
1702    {
1703        self.register(tool);
1704        self
1705    }
1706
1707    /// Registers a pre-wrapped `Arc<dyn Tool>`.
1708    pub fn register_arc(&mut self, tool: Arc<dyn Tool>) -> &mut Self {
1709        self.tools.insert(tool.spec().name.clone(), tool);
1710        self
1711    }
1712
1713    /// Looks up a tool by name, returning `None` if not registered.
1714    pub fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1715        self.tools.get(name).cloned()
1716    }
1717
1718    /// Returns all registered tools as a `Vec`.
1719    pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
1720        self.tools.values().cloned().collect()
1721    }
1722
1723    /// Returns the [`ToolSpec`] for every registered tool.
1724    pub fn specs(&self) -> Vec<ToolSpec> {
1725        self.tools
1726            .values()
1727            .filter_map(|tool| tool.current_spec())
1728            .collect()
1729    }
1730}
1731
1732impl ToolSpec {
1733    /// Converts this spec into an [`InvocableSpec`] for use with the
1734    /// capability layer.
1735    pub fn as_invocable_spec(&self) -> InvocableSpec {
1736        InvocableSpec::new(
1737            CapabilityName::new(self.name.0.clone()),
1738            self.description.clone(),
1739            self.input_schema.clone(),
1740        )
1741        .with_metadata(self.metadata.clone())
1742    }
1743}
1744
1745/// Wraps a [`Tool`] as an [`Invocable`] so it can be surfaced through the
1746/// agentkit capability layer.
1747///
1748/// Created automatically by [`ToolCapabilityProvider::from_registry`]; you
1749/// rarely need to construct one yourself.
1750pub struct ToolInvocableAdapter {
1751    spec: InvocableSpec,
1752    tool: Arc<dyn Tool>,
1753    permissions: Arc<dyn PermissionChecker>,
1754    resources: Arc<dyn ToolResources>,
1755    next_call_id: AtomicU64,
1756}
1757
1758impl ToolInvocableAdapter {
1759    /// Creates a new adapter that wraps `tool` with the given permission
1760    /// checker and shared resources.
1761    pub fn new(
1762        tool: Arc<dyn Tool>,
1763        permissions: Arc<dyn PermissionChecker>,
1764        resources: Arc<dyn ToolResources>,
1765    ) -> Option<Self> {
1766        let spec = tool.current_spec()?.as_invocable_spec();
1767        Some(Self {
1768            spec,
1769            tool,
1770            permissions,
1771            resources,
1772            next_call_id: AtomicU64::new(1),
1773        })
1774    }
1775}
1776
1777#[async_trait]
1778impl Invocable for ToolInvocableAdapter {
1779    fn spec(&self) -> &InvocableSpec {
1780        &self.spec
1781    }
1782
1783    async fn invoke(
1784        &self,
1785        request: InvocableRequest,
1786        ctx: &mut CapabilityContext<'_>,
1787    ) -> Result<InvocableResult, CapabilityError> {
1788        let tool_request = ToolRequest {
1789            call_id: ToolCallId::new(format!(
1790                "tool-call-{}",
1791                self.next_call_id.fetch_add(1, Ordering::Relaxed)
1792            )),
1793            tool_name: self.tool.spec().name.clone(),
1794            input: request.input,
1795            session_id: ctx
1796                .session_id
1797                .cloned()
1798                .unwrap_or_else(|| SessionId::new("capability-session")),
1799            turn_id: ctx
1800                .turn_id
1801                .cloned()
1802                .unwrap_or_else(|| TurnId::new("capability-turn")),
1803            metadata: request.metadata,
1804        };
1805
1806        for permission_request in self
1807            .tool
1808            .proposed_requests(&tool_request)
1809            .map_err(|error| CapabilityError::InvalidInput(error.to_string()))?
1810        {
1811            match self.permissions.evaluate(permission_request.as_ref()) {
1812                PermissionDecision::Allow => {}
1813                PermissionDecision::Deny(denial) => {
1814                    return Err(CapabilityError::ExecutionFailed(format!(
1815                        "tool permission denied: {denial:?}"
1816                    )));
1817                }
1818                PermissionDecision::RequireApproval(req) => {
1819                    return Err(CapabilityError::Unavailable(format!(
1820                        "tool invocation requires approval: {}",
1821                        req.summary
1822                    )));
1823                }
1824            }
1825        }
1826
1827        let mut tool_ctx = ToolContext {
1828            capability: CapabilityContext {
1829                session_id: ctx.session_id,
1830                turn_id: ctx.turn_id,
1831                metadata: ctx.metadata,
1832            },
1833            permissions: self.permissions.as_ref(),
1834            resources: self.resources.as_ref(),
1835            cancellation: None,
1836        };
1837
1838        let result = self
1839            .tool
1840            .invoke(tool_request, &mut tool_ctx)
1841            .await
1842            .map_err(|error| CapabilityError::ExecutionFailed(error.to_string()))?;
1843
1844        Ok(InvocableResult {
1845            output: match result.result.output {
1846                ToolOutput::Text(text) => InvocableOutput::Text(text),
1847                ToolOutput::Structured(value) => InvocableOutput::Structured(value),
1848                ToolOutput::Parts(parts) => InvocableOutput::Items(vec![Item {
1849                    id: None,
1850                    kind: ItemKind::Tool,
1851                    parts,
1852                    metadata: MetadataMap::new(),
1853                }]),
1854                ToolOutput::Files(files) => {
1855                    let parts = files.into_iter().map(Part::File).collect();
1856                    InvocableOutput::Items(vec![Item {
1857                        id: None,
1858                        kind: ItemKind::Tool,
1859                        parts,
1860                        metadata: MetadataMap::new(),
1861                    }])
1862                }
1863            },
1864            metadata: result.metadata,
1865        })
1866    }
1867}
1868
1869/// A [`CapabilityProvider`] that exposes every tool in a [`ToolRegistry`]
1870/// as an [`Invocable`] in the agentkit capability layer.
1871///
1872/// This is the bridge between the tool subsystem and the generic capability
1873/// API that the agent loop consumes.
1874pub struct ToolCapabilityProvider {
1875    invocables: Vec<Arc<dyn Invocable>>,
1876}
1877
1878impl ToolCapabilityProvider {
1879    /// Builds a provider from all tools in `registry`, sharing the given
1880    /// permission checker and resources across every adapter.
1881    pub fn from_registry(
1882        registry: &ToolRegistry,
1883        permissions: Arc<dyn PermissionChecker>,
1884        resources: Arc<dyn ToolResources>,
1885    ) -> Self {
1886        let invocables = registry
1887            .tools()
1888            .into_iter()
1889            .filter_map(|tool| {
1890                ToolInvocableAdapter::new(tool, permissions.clone(), resources.clone())
1891                    .map(|adapter| Arc::new(adapter) as Arc<dyn Invocable>)
1892            })
1893            .collect();
1894
1895        Self { invocables }
1896    }
1897}
1898
1899impl CapabilityProvider for ToolCapabilityProvider {
1900    fn invocables(&self) -> Vec<Arc<dyn Invocable>> {
1901        self.invocables.clone()
1902    }
1903
1904    fn resources(&self) -> Vec<Arc<dyn ResourceProvider>> {
1905        Vec::new()
1906    }
1907
1908    fn prompts(&self) -> Vec<Arc<dyn PromptProvider>> {
1909        Vec::new()
1910    }
1911}
1912
1913/// The three-way result of a [`ToolExecutor::execute`] call.
1914///
1915/// Unlike a simple `Result`, this type distinguishes between a successful
1916/// completion, an interruption requiring user input (approval or auth), and
1917/// an outright failure.
1918#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1919pub enum ToolExecutionOutcome {
1920    /// The tool ran to completion and produced a result.
1921    Completed(ToolResult),
1922    /// The tool was interrupted and needs user input before it can continue.
1923    Interrupted(ToolInterruption),
1924    /// The tool failed with an error.
1925    Failed(ToolError),
1926}
1927
1928/// Trait for executing tool calls with permission checking and interruption
1929/// handling.
1930///
1931/// The agent loop calls [`execute`](ToolExecutor::execute) for every tool
1932/// call the model emits. If execution returns
1933/// [`ToolExecutionOutcome::Interrupted`], the loop collects user input and
1934/// retries with [`execute_approved`](ToolExecutor::execute_approved).
1935#[async_trait]
1936pub trait ToolExecutor: Send + Sync {
1937    /// Returns the current specification for every available tool.
1938    fn specs(&self) -> Vec<ToolSpec>;
1939
1940    /// Looks up the tool, evaluates permissions, and invokes it.
1941    async fn execute(
1942        &self,
1943        request: ToolRequest,
1944        ctx: &mut ToolContext<'_>,
1945    ) -> ToolExecutionOutcome;
1946
1947    /// Looks up the tool, evaluates permissions, and invokes it using an
1948    /// owned execution context.
1949    async fn execute_owned(
1950        &self,
1951        request: ToolRequest,
1952        ctx: OwnedToolContext,
1953    ) -> ToolExecutionOutcome {
1954        let mut borrowed = ctx.borrowed();
1955        self.execute(request, &mut borrowed).await
1956    }
1957
1958    /// Re-executes a tool call that was previously interrupted for approval.
1959    ///
1960    /// The default implementation ignores `approved_request` and delegates
1961    /// to [`execute`](ToolExecutor::execute). [`BasicToolExecutor`]
1962    /// overrides this to skip the approval gate for the matching request.
1963    async fn execute_approved(
1964        &self,
1965        request: ToolRequest,
1966        approved_request: &ApprovalRequest,
1967        ctx: &mut ToolContext<'_>,
1968    ) -> ToolExecutionOutcome {
1969        let _ = approved_request;
1970        self.execute(request, ctx).await
1971    }
1972
1973    /// Re-executes a tool call that was previously interrupted for approval
1974    /// using an owned execution context.
1975    async fn execute_approved_owned(
1976        &self,
1977        request: ToolRequest,
1978        approved_request: &ApprovalRequest,
1979        ctx: OwnedToolContext,
1980    ) -> ToolExecutionOutcome {
1981        let mut borrowed = ctx.borrowed();
1982        self.execute_approved(request, approved_request, &mut borrowed)
1983            .await
1984    }
1985}
1986
1987/// The default [`ToolExecutor`] that looks up tools in a [`ToolRegistry`],
1988/// checks permissions via [`Tool::proposed_requests`], and invokes the tool.
1989///
1990/// # Example
1991///
1992/// ```rust,no_run
1993/// use agentkit_tools_core::{BasicToolExecutor, ToolRegistry};
1994///
1995/// let registry = ToolRegistry::new();
1996/// let executor = BasicToolExecutor::new(registry);
1997/// // Pass `executor` to the agent loop.
1998/// ```
1999pub struct BasicToolExecutor {
2000    registry: ToolRegistry,
2001}
2002
2003impl BasicToolExecutor {
2004    /// Creates an executor backed by the given registry.
2005    pub fn new(registry: ToolRegistry) -> Self {
2006        Self { registry }
2007    }
2008
2009    /// Returns the [`ToolSpec`] for every tool in the underlying registry.
2010    pub fn specs(&self) -> Vec<ToolSpec> {
2011        self.registry.specs()
2012    }
2013
2014    async fn execute_inner(
2015        &self,
2016        request: ToolRequest,
2017        approved_request_id: Option<&ApprovalId>,
2018        ctx: &mut ToolContext<'_>,
2019    ) -> ToolExecutionOutcome {
2020        let Some(tool) = self.registry.get(&request.tool_name) else {
2021            return ToolExecutionOutcome::Failed(ToolError::NotFound(request.tool_name));
2022        };
2023
2024        match tool.proposed_requests(&request) {
2025            Ok(requests) => {
2026                for permission_request in requests {
2027                    match ctx.permissions.evaluate(permission_request.as_ref()) {
2028                        PermissionDecision::Allow => {}
2029                        PermissionDecision::Deny(denial) => {
2030                            return ToolExecutionOutcome::Failed(ToolError::PermissionDenied(
2031                                denial,
2032                            ));
2033                        }
2034                        PermissionDecision::RequireApproval(mut req) => {
2035                            req.call_id = Some(request.call_id.clone());
2036                            if approved_request_id != Some(&req.id) {
2037                                return ToolExecutionOutcome::Interrupted(
2038                                    ToolInterruption::ApprovalRequired(req),
2039                                );
2040                            }
2041                        }
2042                    }
2043                }
2044            }
2045            Err(error) => return ToolExecutionOutcome::Failed(error),
2046        }
2047
2048        match tool.invoke(request, ctx).await {
2049            Ok(result) => ToolExecutionOutcome::Completed(result),
2050            Err(ToolError::AuthRequired(request)) => {
2051                ToolExecutionOutcome::Interrupted(ToolInterruption::AuthRequired(*request))
2052            }
2053            Err(error) => ToolExecutionOutcome::Failed(error),
2054        }
2055    }
2056}
2057
2058#[async_trait]
2059impl ToolExecutor for BasicToolExecutor {
2060    fn specs(&self) -> Vec<ToolSpec> {
2061        self.registry.specs()
2062    }
2063
2064    async fn execute(
2065        &self,
2066        request: ToolRequest,
2067        ctx: &mut ToolContext<'_>,
2068    ) -> ToolExecutionOutcome {
2069        self.execute_inner(request, None, ctx).await
2070    }
2071
2072    async fn execute_approved(
2073        &self,
2074        request: ToolRequest,
2075        approved_request: &ApprovalRequest,
2076        ctx: &mut ToolContext<'_>,
2077    ) -> ToolExecutionOutcome {
2078        self.execute_inner(request, Some(&approved_request.id), ctx)
2079            .await
2080    }
2081}
2082
2083/// Errors that can occur during tool lookup, permission checking, or execution.
2084///
2085/// Returned from [`Tool::invoke`] and also used internally by
2086/// [`BasicToolExecutor`] to represent lookup and permission failures.
2087#[derive(Debug, Error, Clone, PartialEq, Serialize, Deserialize)]
2088pub enum ToolError {
2089    /// No tool with the given name exists in the registry.
2090    #[error("tool not found: {0}")]
2091    NotFound(ToolName),
2092    /// The input JSON did not match the tool's expected schema.
2093    #[error("invalid tool input: {0}")]
2094    InvalidInput(String),
2095    /// A permission policy denied the operation.
2096    #[error("tool permission denied: {0:?}")]
2097    PermissionDenied(PermissionDenial),
2098    /// The tool ran but encountered a runtime error.
2099    #[error("tool execution failed: {0}")]
2100    ExecutionFailed(String),
2101    /// The tool needs authentication credentials to proceed.
2102    ///
2103    /// The executor converts this into [`ToolInterruption::AuthRequired`].
2104    #[error("tool auth required: {0:?}")]
2105    AuthRequired(Box<AuthRequest>),
2106    /// The tool is temporarily unavailable.
2107    #[error("tool unavailable: {0}")]
2108    Unavailable(String),
2109    /// The turn was cancelled while the tool was running.
2110    #[error("tool execution cancelled")]
2111    Cancelled,
2112    /// An unexpected internal error.
2113    #[error("internal tool error: {0}")]
2114    Internal(String),
2115}
2116
2117impl ToolError {
2118    /// Convenience constructor for the [`PermissionDenied`](ToolError::PermissionDenied) variant.
2119    pub fn permission_denied(denial: PermissionDenial) -> Self {
2120        Self::PermissionDenied(denial)
2121    }
2122}
2123
2124impl From<PermissionDenial> for ToolError {
2125    fn from(value: PermissionDenial) -> Self {
2126        Self::permission_denied(value)
2127    }
2128}
2129
2130#[cfg(test)]
2131mod tests {
2132    use super::*;
2133    use async_trait::async_trait;
2134    use serde_json::json;
2135
2136    #[test]
2137    fn command_policy_can_deny_unknown_executables_without_approval() {
2138        let policy = CommandPolicy::new()
2139            .allow_executable("pwd")
2140            .require_approval_for_unknown(false);
2141        let request = ShellPermissionRequest {
2142            executable: "rm".into(),
2143            argv: vec!["-rf".into(), "/tmp/demo".into()],
2144            cwd: None,
2145            env_keys: Vec::new(),
2146            metadata: MetadataMap::new(),
2147        };
2148
2149        match policy.evaluate(&request) {
2150            PolicyMatch::Deny(denial) => {
2151                assert_eq!(denial.code, PermissionCode::CommandNotAllowed);
2152            }
2153            other => panic!("unexpected policy match: {other:?}"),
2154        }
2155    }
2156
2157    #[derive(Clone)]
2158    struct HiddenTool {
2159        spec: ToolSpec,
2160    }
2161
2162    impl HiddenTool {
2163        fn new() -> Self {
2164            Self {
2165                spec: ToolSpec {
2166                    name: ToolName::new("hidden"),
2167                    description: "hidden".into(),
2168                    input_schema: json!({"type": "object"}),
2169                    annotations: ToolAnnotations::default(),
2170                    metadata: MetadataMap::new(),
2171                },
2172            }
2173        }
2174    }
2175
2176    #[async_trait]
2177    impl Tool for HiddenTool {
2178        fn spec(&self) -> &ToolSpec {
2179            &self.spec
2180        }
2181
2182        fn current_spec(&self) -> Option<ToolSpec> {
2183            None
2184        }
2185
2186        async fn invoke(
2187            &self,
2188            request: ToolRequest,
2189            _ctx: &mut ToolContext<'_>,
2190        ) -> Result<ToolResult, ToolError> {
2191            Ok(ToolResult {
2192                result: ToolResultPart {
2193                    call_id: request.call_id,
2194                    output: ToolOutput::Text("hidden".into()),
2195                    is_error: false,
2196                    metadata: MetadataMap::new(),
2197                },
2198                duration: None,
2199                metadata: MetadataMap::new(),
2200            })
2201        }
2202    }
2203
2204    #[test]
2205    fn hidden_tools_are_omitted_from_specs_and_capabilities() {
2206        let registry = ToolRegistry::new().with(HiddenTool::new());
2207
2208        assert!(registry.specs().is_empty());
2209
2210        let provider = ToolCapabilityProvider::from_registry(
2211            &registry,
2212            Arc::new(AllowAllPermissionChecker),
2213            Arc::new(()),
2214        );
2215        assert!(provider.invocables().is_empty());
2216    }
2217
2218    struct AllowAllPermissionChecker;
2219
2220    impl PermissionChecker for AllowAllPermissionChecker {
2221        fn evaluate(&self, _request: &dyn PermissionRequest) -> PermissionDecision {
2222            PermissionDecision::Allow
2223        }
2224    }
2225}