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///     .read_only_root("/workspace/project/vendor")
1200///     .protect_root("/workspace/project/.env")
1201///     .require_approval_outside_allowed(true);
1202/// ```
1203pub struct PathPolicy {
1204    allowed_roots: Vec<PathBuf>,
1205    read_only_roots: Vec<PathBuf>,
1206    protected_roots: Vec<PathBuf>,
1207    require_approval_outside_allowed: bool,
1208}
1209
1210impl PathPolicy {
1211    /// Creates a new path policy with no roots and approval required for
1212    /// paths outside allowed roots.
1213    pub fn new() -> Self {
1214        Self {
1215            allowed_roots: Vec::new(),
1216            read_only_roots: Vec::new(),
1217            protected_roots: Vec::new(),
1218            require_approval_outside_allowed: true,
1219        }
1220    }
1221
1222    /// Adds a directory tree that filesystem operations are allowed to target.
1223    pub fn allow_root(mut self, root: impl Into<PathBuf>) -> Self {
1224        self.allowed_roots.push(root.into());
1225        self
1226    }
1227
1228    /// Adds a directory tree that may be read or listed but not mutated.
1229    pub fn read_only_root(mut self, root: impl Into<PathBuf>) -> Self {
1230        self.read_only_roots.push(root.into());
1231        self
1232    }
1233
1234    /// Adds a directory tree that filesystem operations are never allowed to target.
1235    pub fn protect_root(mut self, root: impl Into<PathBuf>) -> Self {
1236        self.protected_roots.push(root.into());
1237        self
1238    }
1239
1240    /// When `true` (the default), paths outside allowed roots trigger an
1241    /// approval request instead of an outright denial.
1242    pub fn require_approval_outside_allowed(mut self, value: bool) -> Self {
1243        self.require_approval_outside_allowed = value;
1244        self
1245    }
1246}
1247
1248impl Default for PathPolicy {
1249    fn default() -> Self {
1250        Self::new()
1251    }
1252}
1253
1254impl PermissionPolicy for PathPolicy {
1255    fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1256        let Some(fs) = request
1257            .as_any()
1258            .downcast_ref::<FileSystemPermissionRequest>()
1259        else {
1260            return PolicyMatch::NoOpinion;
1261        };
1262
1263        let raw_paths: Vec<&Path> = match fs {
1264            FileSystemPermissionRequest::Move { from, to, .. } => {
1265                vec![from.as_path(), to.as_path()]
1266            }
1267            FileSystemPermissionRequest::Read { path, .. }
1268            | FileSystemPermissionRequest::Write { path, .. }
1269            | FileSystemPermissionRequest::Edit { path, .. }
1270            | FileSystemPermissionRequest::Delete { path, .. }
1271            | FileSystemPermissionRequest::List { path, .. }
1272            | FileSystemPermissionRequest::CreateDir { path, .. } => vec![path.as_path()],
1273        };
1274
1275        let candidate_paths: Vec<PathBuf> = raw_paths
1276            .iter()
1277            .map(|p| std::path::absolute(p).unwrap_or_else(|_| p.to_path_buf()))
1278            .collect();
1279
1280        let mutates = matches!(
1281            fs,
1282            FileSystemPermissionRequest::Write { .. }
1283                | FileSystemPermissionRequest::Edit { .. }
1284                | FileSystemPermissionRequest::Delete { .. }
1285                | FileSystemPermissionRequest::Move { .. }
1286                | FileSystemPermissionRequest::CreateDir { .. }
1287        );
1288
1289        if candidate_paths.iter().any(|path| {
1290            self.protected_roots
1291                .iter()
1292                .any(|root| path.starts_with(root))
1293        }) {
1294            return PolicyMatch::Deny(PermissionDenial {
1295                code: PermissionCode::PathNotAllowed,
1296                message: format!("path access denied for {}", fs.summary()),
1297                metadata: fs.metadata().clone(),
1298            });
1299        }
1300
1301        if mutates
1302            && candidate_paths.iter().any(|path| {
1303                self.read_only_roots
1304                    .iter()
1305                    .any(|root| path.starts_with(root))
1306            })
1307        {
1308            return PolicyMatch::Deny(PermissionDenial {
1309                code: PermissionCode::PathNotAllowed,
1310                message: format!("path is read-only for {}", fs.summary()),
1311                metadata: fs.metadata().clone(),
1312            });
1313        }
1314
1315        if self.allowed_roots.is_empty() {
1316            return PolicyMatch::NoOpinion;
1317        }
1318
1319        let all_allowed = candidate_paths
1320            .iter()
1321            .all(|path| self.allowed_roots.iter().any(|root| path.starts_with(root)));
1322
1323        if all_allowed {
1324            PolicyMatch::Allow
1325        } else if self.require_approval_outside_allowed {
1326            PolicyMatch::RequireApproval(ApprovalRequest {
1327                task_id: None,
1328                call_id: None,
1329                id: ApprovalId::new(format!("approval:{}", fs.kind())),
1330                request_kind: fs.kind().to_string(),
1331                reason: ApprovalReason::SensitivePath,
1332                summary: fs.summary(),
1333                metadata: fs.metadata().clone(),
1334            })
1335        } else {
1336            PolicyMatch::Deny(PermissionDenial {
1337                code: PermissionCode::PathNotAllowed,
1338                message: format!("path outside allowed roots for {}", fs.summary()),
1339                metadata: fs.metadata().clone(),
1340            })
1341        }
1342    }
1343}
1344
1345/// A [`PermissionPolicy`] that governs [`ShellPermissionRequest`]s by checking
1346/// the executable name, working directory, and environment variables.
1347///
1348/// Denied executables and env keys are rejected immediately. Allowed
1349/// executables pass. Unknown executables either require approval or are
1350/// denied, depending on `require_approval_for_unknown`.
1351///
1352/// # Example
1353///
1354/// ```rust
1355/// use agentkit_tools_core::CommandPolicy;
1356///
1357/// let policy = CommandPolicy::new()
1358///     .allow_executable("git")
1359///     .allow_executable("cargo")
1360///     .deny_executable("rm")
1361///     .deny_env_key("AWS_SECRET_ACCESS_KEY")
1362///     .allow_cwd("/workspace")
1363///     .require_approval_for_unknown(true);
1364/// ```
1365pub struct CommandPolicy {
1366    allowed_executables: BTreeSet<String>,
1367    denied_executables: BTreeSet<String>,
1368    allowed_cwds: Vec<PathBuf>,
1369    denied_env_keys: BTreeSet<String>,
1370    require_approval_for_unknown: bool,
1371}
1372
1373impl CommandPolicy {
1374    /// Creates a new command policy with no rules and approval required
1375    /// for unknown executables.
1376    pub fn new() -> Self {
1377        Self {
1378            allowed_executables: BTreeSet::new(),
1379            denied_executables: BTreeSet::new(),
1380            allowed_cwds: Vec::new(),
1381            denied_env_keys: BTreeSet::new(),
1382            require_approval_for_unknown: true,
1383        }
1384    }
1385
1386    /// Adds an executable name to the allow-list.
1387    pub fn allow_executable(mut self, executable: impl Into<String>) -> Self {
1388        self.allowed_executables.insert(executable.into());
1389        self
1390    }
1391
1392    /// Adds an executable name to the deny-list.
1393    pub fn deny_executable(mut self, executable: impl Into<String>) -> Self {
1394        self.denied_executables.insert(executable.into());
1395        self
1396    }
1397
1398    /// Adds a directory root that commands are allowed to run in.
1399    pub fn allow_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
1400        self.allowed_cwds.push(cwd.into());
1401        self
1402    }
1403
1404    /// Adds an environment variable name to the deny-list.
1405    pub fn deny_env_key(mut self, key: impl Into<String>) -> Self {
1406        self.denied_env_keys.insert(key.into());
1407        self
1408    }
1409
1410    /// When `true` (the default), executables not in the allow-list trigger
1411    /// an approval request instead of an outright denial.
1412    pub fn require_approval_for_unknown(mut self, value: bool) -> Self {
1413        self.require_approval_for_unknown = value;
1414        self
1415    }
1416}
1417
1418impl Default for CommandPolicy {
1419    fn default() -> Self {
1420        Self::new()
1421    }
1422}
1423
1424impl PermissionPolicy for CommandPolicy {
1425    fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1426        let Some(shell) = request.as_any().downcast_ref::<ShellPermissionRequest>() else {
1427            return PolicyMatch::NoOpinion;
1428        };
1429
1430        if self.denied_executables.contains(&shell.executable)
1431            || shell
1432                .env_keys
1433                .iter()
1434                .any(|key| self.denied_env_keys.contains(key))
1435        {
1436            return PolicyMatch::Deny(PermissionDenial {
1437                code: PermissionCode::CommandNotAllowed,
1438                message: format!("command denied for {}", shell.summary()),
1439                metadata: shell.metadata().clone(),
1440            });
1441        }
1442
1443        if let Some(cwd) = &shell.cwd
1444            && !self.allowed_cwds.is_empty()
1445            && !self.allowed_cwds.iter().any(|root| cwd.starts_with(root))
1446        {
1447            return PolicyMatch::RequireApproval(ApprovalRequest {
1448                task_id: None,
1449                call_id: None,
1450                id: ApprovalId::new("approval:shell.cwd"),
1451                request_kind: shell.kind().to_string(),
1452                reason: ApprovalReason::SensitiveCommand,
1453                summary: shell.summary(),
1454                metadata: shell.metadata().clone(),
1455            });
1456        }
1457
1458        if self.allowed_executables.is_empty()
1459            || self.allowed_executables.contains(&shell.executable)
1460        {
1461            PolicyMatch::Allow
1462        } else if self.require_approval_for_unknown {
1463            PolicyMatch::RequireApproval(ApprovalRequest {
1464                task_id: None,
1465                call_id: None,
1466                id: ApprovalId::new("approval:shell.command"),
1467                request_kind: shell.kind().to_string(),
1468                reason: ApprovalReason::SensitiveCommand,
1469                summary: shell.summary(),
1470                metadata: shell.metadata().clone(),
1471            })
1472        } else {
1473            PolicyMatch::Deny(PermissionDenial {
1474                code: PermissionCode::CommandNotAllowed,
1475                message: format!("executable {} is not allowed", shell.executable),
1476                metadata: shell.metadata().clone(),
1477            })
1478        }
1479    }
1480}
1481
1482/// A [`PermissionPolicy`] that governs [`McpPermissionRequest`]s by checking
1483/// whether the target server is trusted and the requested auth scopes are
1484/// in the allow-list.
1485///
1486/// # Example
1487///
1488/// ```rust
1489/// use agentkit_tools_core::McpServerPolicy;
1490///
1491/// let policy = McpServerPolicy::new()
1492///     .trust_server("github-mcp")
1493///     .allow_auth_scope("repo:read");
1494/// ```
1495pub struct McpServerPolicy {
1496    trusted_servers: BTreeSet<String>,
1497    allowed_auth_scopes: BTreeSet<String>,
1498    require_approval_for_untrusted: bool,
1499}
1500
1501impl McpServerPolicy {
1502    /// Creates a new MCP server policy with approval required for untrusted
1503    /// servers.
1504    pub fn new() -> Self {
1505        Self {
1506            trusted_servers: BTreeSet::new(),
1507            allowed_auth_scopes: BTreeSet::new(),
1508            require_approval_for_untrusted: true,
1509        }
1510    }
1511
1512    /// Marks a server as trusted so operations targeting it are allowed.
1513    pub fn trust_server(mut self, server_id: impl Into<String>) -> Self {
1514        self.trusted_servers.insert(server_id.into());
1515        self
1516    }
1517
1518    /// Adds an auth scope to the allow-list.
1519    pub fn allow_auth_scope(mut self, scope: impl Into<String>) -> Self {
1520        self.allowed_auth_scopes.insert(scope.into());
1521        self
1522    }
1523}
1524
1525impl Default for McpServerPolicy {
1526    fn default() -> Self {
1527        Self::new()
1528    }
1529}
1530
1531impl PermissionPolicy for McpServerPolicy {
1532    fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1533        let Some(mcp) = request.as_any().downcast_ref::<McpPermissionRequest>() else {
1534            return PolicyMatch::NoOpinion;
1535        };
1536
1537        let server_id = match mcp {
1538            McpPermissionRequest::Connect { server_id, .. }
1539            | McpPermissionRequest::InvokeTool { server_id, .. }
1540            | McpPermissionRequest::ReadResource { server_id, .. }
1541            | McpPermissionRequest::FetchPrompt { server_id, .. }
1542            | McpPermissionRequest::UseAuthScope { server_id, .. } => server_id,
1543        };
1544
1545        if !self.trusted_servers.is_empty() && !self.trusted_servers.contains(server_id) {
1546            return if self.require_approval_for_untrusted {
1547                PolicyMatch::RequireApproval(ApprovalRequest {
1548                    task_id: None,
1549                    call_id: None,
1550                    id: ApprovalId::new(format!("approval:mcp:{server_id}")),
1551                    request_kind: mcp.kind().to_string(),
1552                    reason: ApprovalReason::SensitiveServer,
1553                    summary: mcp.summary(),
1554                    metadata: mcp.metadata().clone(),
1555                })
1556            } else {
1557                PolicyMatch::Deny(PermissionDenial {
1558                    code: PermissionCode::ServerNotTrusted,
1559                    message: format!("MCP server {server_id} is not trusted"),
1560                    metadata: mcp.metadata().clone(),
1561                })
1562            };
1563        }
1564
1565        if let McpPermissionRequest::UseAuthScope { scope, .. } = mcp
1566            && !self.allowed_auth_scopes.is_empty()
1567            && !self.allowed_auth_scopes.contains(scope)
1568        {
1569            return PolicyMatch::Deny(PermissionDenial {
1570                code: PermissionCode::AuthScopeNotAllowed,
1571                message: format!("MCP auth scope {scope} is not allowed"),
1572                metadata: mcp.metadata().clone(),
1573            });
1574        }
1575
1576        PolicyMatch::Allow
1577    }
1578}
1579
1580/// The central abstraction for an executable tool in an agentkit agent.
1581///
1582/// Implement this trait to define a tool that an LLM can call. Each tool
1583/// provides a [`ToolSpec`] describing its name, schema, and hints, optional
1584/// permission requests via [`proposed_requests`](Tool::proposed_requests),
1585/// and the actual execution logic in [`invoke`](Tool::invoke).
1586///
1587/// # Example
1588///
1589/// ```rust
1590/// use agentkit_core::{MetadataMap, ToolOutput, ToolResultPart};
1591/// use agentkit_tools_core::{
1592///     Tool, ToolContext, ToolError, ToolName, ToolRequest, ToolResult, ToolSpec,
1593/// };
1594/// use async_trait::async_trait;
1595/// use serde_json::json;
1596///
1597/// struct TimeTool {
1598///     spec: ToolSpec,
1599/// }
1600///
1601/// impl TimeTool {
1602///     fn new() -> Self {
1603///         Self {
1604///             spec: ToolSpec::new(
1605///                 ToolName::new("current_time"),
1606///                 "Returns the current UTC time",
1607///                 json!({ "type": "object" }),
1608///             ),
1609///         }
1610///     }
1611/// }
1612///
1613/// #[async_trait]
1614/// impl Tool for TimeTool {
1615///     fn spec(&self) -> &ToolSpec {
1616///         &self.spec
1617///     }
1618///
1619///     async fn invoke(
1620///         &self,
1621///         request: ToolRequest,
1622///         _ctx: &mut ToolContext<'_>,
1623///     ) -> Result<ToolResult, ToolError> {
1624///         Ok(ToolResult::new(ToolResultPart::success(
1625///             request.call_id,
1626///             ToolOutput::text("2026-03-22T12:00:00Z"),
1627///         )))
1628///     }
1629/// }
1630/// ```
1631#[async_trait]
1632pub trait Tool: Send + Sync {
1633    /// Returns the static specification for this tool.
1634    fn spec(&self) -> &ToolSpec;
1635
1636    /// Returns the current specification for this tool, if it should be
1637    /// advertised right now.
1638    ///
1639    /// Most tools are static and can rely on the default implementation,
1640    /// which clones [`spec`](Self::spec). Override this when the description
1641    /// or input schema should reflect runtime state, or when the tool should
1642    /// be temporarily hidden from the model.
1643    fn current_spec(&self) -> Option<ToolSpec> {
1644        Some(self.spec().clone())
1645    }
1646
1647    /// Returns permission requests the executor should evaluate before calling
1648    /// [`invoke`](Tool::invoke).
1649    ///
1650    /// The default implementation returns an empty list (no permissions needed).
1651    /// Override this to declare filesystem, shell, or custom permission
1652    /// requirements based on the incoming request.
1653    ///
1654    /// # Errors
1655    ///
1656    /// Return [`ToolError::InvalidInput`] if the request input is malformed
1657    /// and permission requests cannot be constructed.
1658    fn proposed_requests(
1659        &self,
1660        _request: &ToolRequest,
1661    ) -> Result<Vec<Box<dyn PermissionRequest>>, ToolError> {
1662        Ok(Vec::new())
1663    }
1664
1665    /// Executes the tool and returns a result or error.
1666    ///
1667    /// # Errors
1668    ///
1669    /// Return an appropriate [`ToolError`] variant on failure. Returning
1670    /// [`ToolError::AuthRequired`] causes the executor to emit a
1671    /// [`ToolInterruption::AuthRequired`] instead of treating it as a
1672    /// hard failure.
1673    async fn invoke(
1674        &self,
1675        request: ToolRequest,
1676        ctx: &mut ToolContext<'_>,
1677    ) -> Result<ToolResult, ToolError>;
1678}
1679
1680/// A name-keyed collection of [`Tool`] implementations.
1681///
1682/// The registry owns `Arc`-wrapped tools and is passed to a
1683/// [`BasicToolExecutor`] (or consumed by [`ToolCapabilityProvider`]) so the
1684/// agent loop can look up tools by name at execution time.
1685///
1686/// # Example
1687///
1688/// ```rust
1689/// use agentkit_tools_core::ToolRegistry;
1690/// # use agentkit_tools_core::{Tool, ToolContext, ToolError, ToolName, ToolRequest, ToolResult, ToolSpec};
1691/// # use async_trait::async_trait;
1692/// # use serde_json::json;
1693/// # struct NoopTool(ToolSpec);
1694/// # #[async_trait]
1695/// # impl Tool for NoopTool {
1696/// #     fn spec(&self) -> &ToolSpec { &self.0 }
1697/// #     async fn invoke(&self, _r: ToolRequest, _c: &mut ToolContext<'_>) -> Result<ToolResult, ToolError> { todo!() }
1698/// # }
1699///
1700/// let registry = ToolRegistry::new()
1701///     .with(NoopTool(ToolSpec::new(
1702///         ToolName::new("noop"),
1703///         "Does nothing",
1704///         json!({"type": "object"}),
1705///     )));
1706///
1707/// assert!(registry.get(&ToolName::new("noop")).is_some());
1708/// assert_eq!(registry.specs().len(), 1);
1709/// ```
1710#[derive(Clone, Default)]
1711pub struct ToolRegistry {
1712    tools: BTreeMap<ToolName, Arc<dyn Tool>>,
1713}
1714
1715impl ToolRegistry {
1716    /// Creates an empty registry.
1717    pub fn new() -> Self {
1718        Self::default()
1719    }
1720
1721    /// Registers a tool by value and returns `&mut self` for imperative chaining.
1722    pub fn register<T>(&mut self, tool: T) -> &mut Self
1723    where
1724        T: Tool + 'static,
1725    {
1726        self.tools.insert(tool.spec().name.clone(), Arc::new(tool));
1727        self
1728    }
1729
1730    /// Registers a tool by value and returns `self` for builder-style chaining.
1731    pub fn with<T>(mut self, tool: T) -> Self
1732    where
1733        T: Tool + 'static,
1734    {
1735        self.register(tool);
1736        self
1737    }
1738
1739    /// Registers a pre-wrapped `Arc<dyn Tool>`.
1740    pub fn register_arc(&mut self, tool: Arc<dyn Tool>) -> &mut Self {
1741        self.tools.insert(tool.spec().name.clone(), tool);
1742        self
1743    }
1744
1745    /// Looks up a tool by name, returning `None` if not registered.
1746    pub fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1747        self.tools.get(name).cloned()
1748    }
1749
1750    /// Returns all registered tools as a `Vec`.
1751    pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
1752        self.tools.values().cloned().collect()
1753    }
1754
1755    /// Merges all tools from another registry into this one, consuming it.
1756    ///
1757    /// Supports builder-style chaining:
1758    ///
1759    /// ```ignore
1760    /// let registry = agentkit_tool_fs::registry()
1761    ///     .merge(agentkit_tool_shell::registry());
1762    /// ```
1763    pub fn merge(mut self, other: Self) -> Self {
1764        self.tools.extend(other.tools);
1765        self
1766    }
1767
1768    /// Returns the [`ToolSpec`] for every registered tool.
1769    pub fn specs(&self) -> Vec<ToolSpec> {
1770        self.tools
1771            .values()
1772            .filter_map(|tool| tool.current_spec())
1773            .collect()
1774    }
1775}
1776
1777impl ToolSpec {
1778    /// Converts this spec into an [`InvocableSpec`] for use with the
1779    /// capability layer.
1780    pub fn as_invocable_spec(&self) -> InvocableSpec {
1781        InvocableSpec::new(
1782            CapabilityName::new(self.name.0.clone()),
1783            self.description.clone(),
1784            self.input_schema.clone(),
1785        )
1786        .with_metadata(self.metadata.clone())
1787    }
1788}
1789
1790/// Wraps a [`Tool`] as an [`Invocable`] so it can be surfaced through the
1791/// agentkit capability layer.
1792///
1793/// Created automatically by [`ToolCapabilityProvider::from_registry`]; you
1794/// rarely need to construct one yourself.
1795pub struct ToolInvocableAdapter {
1796    spec: InvocableSpec,
1797    tool: Arc<dyn Tool>,
1798    permissions: Arc<dyn PermissionChecker>,
1799    resources: Arc<dyn ToolResources>,
1800    next_call_id: AtomicU64,
1801}
1802
1803impl ToolInvocableAdapter {
1804    /// Creates a new adapter that wraps `tool` with the given permission
1805    /// checker and shared resources.
1806    pub fn new(
1807        tool: Arc<dyn Tool>,
1808        permissions: Arc<dyn PermissionChecker>,
1809        resources: Arc<dyn ToolResources>,
1810    ) -> Option<Self> {
1811        let spec = tool.current_spec()?.as_invocable_spec();
1812        Some(Self {
1813            spec,
1814            tool,
1815            permissions,
1816            resources,
1817            next_call_id: AtomicU64::new(1),
1818        })
1819    }
1820}
1821
1822#[async_trait]
1823impl Invocable for ToolInvocableAdapter {
1824    fn spec(&self) -> &InvocableSpec {
1825        &self.spec
1826    }
1827
1828    async fn invoke(
1829        &self,
1830        request: InvocableRequest,
1831        ctx: &mut CapabilityContext<'_>,
1832    ) -> Result<InvocableResult, CapabilityError> {
1833        let tool_request = ToolRequest {
1834            call_id: ToolCallId::new(format!(
1835                "tool-call-{}",
1836                self.next_call_id.fetch_add(1, Ordering::Relaxed)
1837            )),
1838            tool_name: self.tool.spec().name.clone(),
1839            input: request.input,
1840            session_id: ctx
1841                .session_id
1842                .cloned()
1843                .unwrap_or_else(|| SessionId::new("capability-session")),
1844            turn_id: ctx
1845                .turn_id
1846                .cloned()
1847                .unwrap_or_else(|| TurnId::new("capability-turn")),
1848            metadata: request.metadata,
1849        };
1850
1851        for permission_request in self
1852            .tool
1853            .proposed_requests(&tool_request)
1854            .map_err(|error| CapabilityError::InvalidInput(error.to_string()))?
1855        {
1856            match self.permissions.evaluate(permission_request.as_ref()) {
1857                PermissionDecision::Allow => {}
1858                PermissionDecision::Deny(denial) => {
1859                    return Err(CapabilityError::ExecutionFailed(format!(
1860                        "tool permission denied: {denial:?}"
1861                    )));
1862                }
1863                PermissionDecision::RequireApproval(req) => {
1864                    return Err(CapabilityError::Unavailable(format!(
1865                        "tool invocation requires approval: {}",
1866                        req.summary
1867                    )));
1868                }
1869            }
1870        }
1871
1872        let mut tool_ctx = ToolContext {
1873            capability: CapabilityContext {
1874                session_id: ctx.session_id,
1875                turn_id: ctx.turn_id,
1876                metadata: ctx.metadata,
1877            },
1878            permissions: self.permissions.as_ref(),
1879            resources: self.resources.as_ref(),
1880            cancellation: None,
1881        };
1882
1883        let result = self
1884            .tool
1885            .invoke(tool_request, &mut tool_ctx)
1886            .await
1887            .map_err(|error| CapabilityError::ExecutionFailed(error.to_string()))?;
1888
1889        Ok(InvocableResult {
1890            output: match result.result.output {
1891                ToolOutput::Text(text) => InvocableOutput::Text(text),
1892                ToolOutput::Structured(value) => InvocableOutput::Structured(value),
1893                ToolOutput::Parts(parts) => InvocableOutput::Items(vec![Item {
1894                    id: None,
1895                    kind: ItemKind::Tool,
1896                    parts,
1897                    metadata: MetadataMap::new(),
1898                }]),
1899                ToolOutput::Files(files) => {
1900                    let parts = files.into_iter().map(Part::File).collect();
1901                    InvocableOutput::Items(vec![Item {
1902                        id: None,
1903                        kind: ItemKind::Tool,
1904                        parts,
1905                        metadata: MetadataMap::new(),
1906                    }])
1907                }
1908            },
1909            metadata: result.metadata,
1910        })
1911    }
1912}
1913
1914/// A [`CapabilityProvider`] that exposes every tool in a [`ToolRegistry`]
1915/// as an [`Invocable`] in the agentkit capability layer.
1916///
1917/// This is the bridge between the tool subsystem and the generic capability
1918/// API that the agent loop consumes.
1919pub struct ToolCapabilityProvider {
1920    invocables: Vec<Arc<dyn Invocable>>,
1921}
1922
1923impl ToolCapabilityProvider {
1924    /// Builds a provider from all tools in `registry`, sharing the given
1925    /// permission checker and resources across every adapter.
1926    pub fn from_registry(
1927        registry: &ToolRegistry,
1928        permissions: Arc<dyn PermissionChecker>,
1929        resources: Arc<dyn ToolResources>,
1930    ) -> Self {
1931        let invocables = registry
1932            .tools()
1933            .into_iter()
1934            .filter_map(|tool| {
1935                ToolInvocableAdapter::new(tool, permissions.clone(), resources.clone())
1936                    .map(|adapter| Arc::new(adapter) as Arc<dyn Invocable>)
1937            })
1938            .collect();
1939
1940        Self { invocables }
1941    }
1942}
1943
1944impl CapabilityProvider for ToolCapabilityProvider {
1945    fn invocables(&self) -> Vec<Arc<dyn Invocable>> {
1946        self.invocables.clone()
1947    }
1948
1949    fn resources(&self) -> Vec<Arc<dyn ResourceProvider>> {
1950        Vec::new()
1951    }
1952
1953    fn prompts(&self) -> Vec<Arc<dyn PromptProvider>> {
1954        Vec::new()
1955    }
1956}
1957
1958/// The three-way result of a [`ToolExecutor::execute`] call.
1959///
1960/// Unlike a simple `Result`, this type distinguishes between a successful
1961/// completion, an interruption requiring user input (approval or auth), and
1962/// an outright failure.
1963#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1964pub enum ToolExecutionOutcome {
1965    /// The tool ran to completion and produced a result.
1966    Completed(ToolResult),
1967    /// The tool was interrupted and needs user input before it can continue.
1968    Interrupted(ToolInterruption),
1969    /// The tool failed with an error.
1970    Failed(ToolError),
1971}
1972
1973/// Trait for executing tool calls with permission checking and interruption
1974/// handling.
1975///
1976/// The agent loop calls [`execute`](ToolExecutor::execute) for every tool
1977/// call the model emits. If execution returns
1978/// [`ToolExecutionOutcome::Interrupted`], the loop collects user input and
1979/// retries with [`execute_approved`](ToolExecutor::execute_approved).
1980#[async_trait]
1981pub trait ToolExecutor: Send + Sync {
1982    /// Returns the current specification for every available tool.
1983    fn specs(&self) -> Vec<ToolSpec>;
1984
1985    /// Looks up the tool, evaluates permissions, and invokes it.
1986    async fn execute(
1987        &self,
1988        request: ToolRequest,
1989        ctx: &mut ToolContext<'_>,
1990    ) -> ToolExecutionOutcome;
1991
1992    /// Looks up the tool, evaluates permissions, and invokes it using an
1993    /// owned execution context.
1994    async fn execute_owned(
1995        &self,
1996        request: ToolRequest,
1997        ctx: OwnedToolContext,
1998    ) -> ToolExecutionOutcome {
1999        let mut borrowed = ctx.borrowed();
2000        self.execute(request, &mut borrowed).await
2001    }
2002
2003    /// Re-executes a tool call that was previously interrupted for approval.
2004    ///
2005    /// The default implementation ignores `approved_request` and delegates
2006    /// to [`execute`](ToolExecutor::execute). [`BasicToolExecutor`]
2007    /// overrides this to skip the approval gate for the matching request.
2008    async fn execute_approved(
2009        &self,
2010        request: ToolRequest,
2011        approved_request: &ApprovalRequest,
2012        ctx: &mut ToolContext<'_>,
2013    ) -> ToolExecutionOutcome {
2014        let _ = approved_request;
2015        self.execute(request, ctx).await
2016    }
2017
2018    /// Re-executes a tool call that was previously interrupted for approval
2019    /// using an owned execution context.
2020    async fn execute_approved_owned(
2021        &self,
2022        request: ToolRequest,
2023        approved_request: &ApprovalRequest,
2024        ctx: OwnedToolContext,
2025    ) -> ToolExecutionOutcome {
2026        let mut borrowed = ctx.borrowed();
2027        self.execute_approved(request, approved_request, &mut borrowed)
2028            .await
2029    }
2030}
2031
2032/// The default [`ToolExecutor`] that looks up tools in a [`ToolRegistry`],
2033/// checks permissions via [`Tool::proposed_requests`], and invokes the tool.
2034///
2035/// # Example
2036///
2037/// ```rust,no_run
2038/// use agentkit_tools_core::{BasicToolExecutor, ToolRegistry};
2039///
2040/// let registry = ToolRegistry::new();
2041/// let executor = BasicToolExecutor::new(registry);
2042/// // Pass `executor` to the agent loop.
2043/// ```
2044pub struct BasicToolExecutor {
2045    registry: ToolRegistry,
2046}
2047
2048impl BasicToolExecutor {
2049    /// Creates an executor backed by the given registry.
2050    pub fn new(registry: ToolRegistry) -> Self {
2051        Self { registry }
2052    }
2053
2054    /// Returns the [`ToolSpec`] for every tool in the underlying registry.
2055    pub fn specs(&self) -> Vec<ToolSpec> {
2056        self.registry.specs()
2057    }
2058
2059    async fn execute_inner(
2060        &self,
2061        request: ToolRequest,
2062        approved_request_id: Option<&ApprovalId>,
2063        ctx: &mut ToolContext<'_>,
2064    ) -> ToolExecutionOutcome {
2065        let Some(tool) = self.registry.get(&request.tool_name) else {
2066            return ToolExecutionOutcome::Failed(ToolError::NotFound(request.tool_name));
2067        };
2068
2069        match tool.proposed_requests(&request) {
2070            Ok(requests) => {
2071                for permission_request in requests {
2072                    match ctx.permissions.evaluate(permission_request.as_ref()) {
2073                        PermissionDecision::Allow => {}
2074                        PermissionDecision::Deny(denial) => {
2075                            return ToolExecutionOutcome::Failed(ToolError::PermissionDenied(
2076                                denial,
2077                            ));
2078                        }
2079                        PermissionDecision::RequireApproval(mut req) => {
2080                            req.call_id = Some(request.call_id.clone());
2081                            if approved_request_id != Some(&req.id) {
2082                                return ToolExecutionOutcome::Interrupted(
2083                                    ToolInterruption::ApprovalRequired(req),
2084                                );
2085                            }
2086                        }
2087                    }
2088                }
2089            }
2090            Err(error) => return ToolExecutionOutcome::Failed(error),
2091        }
2092
2093        match tool.invoke(request, ctx).await {
2094            Ok(result) => ToolExecutionOutcome::Completed(result),
2095            Err(ToolError::AuthRequired(request)) => {
2096                ToolExecutionOutcome::Interrupted(ToolInterruption::AuthRequired(*request))
2097            }
2098            Err(error) => ToolExecutionOutcome::Failed(error),
2099        }
2100    }
2101}
2102
2103#[async_trait]
2104impl ToolExecutor for BasicToolExecutor {
2105    fn specs(&self) -> Vec<ToolSpec> {
2106        self.registry.specs()
2107    }
2108
2109    async fn execute(
2110        &self,
2111        request: ToolRequest,
2112        ctx: &mut ToolContext<'_>,
2113    ) -> ToolExecutionOutcome {
2114        self.execute_inner(request, None, ctx).await
2115    }
2116
2117    async fn execute_approved(
2118        &self,
2119        request: ToolRequest,
2120        approved_request: &ApprovalRequest,
2121        ctx: &mut ToolContext<'_>,
2122    ) -> ToolExecutionOutcome {
2123        self.execute_inner(request, Some(&approved_request.id), ctx)
2124            .await
2125    }
2126}
2127
2128/// Errors that can occur during tool lookup, permission checking, or execution.
2129///
2130/// Returned from [`Tool::invoke`] and also used internally by
2131/// [`BasicToolExecutor`] to represent lookup and permission failures.
2132#[derive(Debug, Error, Clone, PartialEq, Serialize, Deserialize)]
2133pub enum ToolError {
2134    /// No tool with the given name exists in the registry.
2135    #[error("tool not found: {0}")]
2136    NotFound(ToolName),
2137    /// The input JSON did not match the tool's expected schema.
2138    #[error("invalid tool input: {0}")]
2139    InvalidInput(String),
2140    /// A permission policy denied the operation.
2141    #[error("tool permission denied: {0:?}")]
2142    PermissionDenied(PermissionDenial),
2143    /// The tool ran but encountered a runtime error.
2144    #[error("tool execution failed: {0}")]
2145    ExecutionFailed(String),
2146    /// The tool needs authentication credentials to proceed.
2147    ///
2148    /// The executor converts this into [`ToolInterruption::AuthRequired`].
2149    #[error("tool auth required: {0:?}")]
2150    AuthRequired(Box<AuthRequest>),
2151    /// The tool is temporarily unavailable.
2152    #[error("tool unavailable: {0}")]
2153    Unavailable(String),
2154    /// The turn was cancelled while the tool was running.
2155    #[error("tool execution cancelled")]
2156    Cancelled,
2157    /// An unexpected internal error.
2158    #[error("internal tool error: {0}")]
2159    Internal(String),
2160}
2161
2162impl ToolError {
2163    /// Convenience constructor for the [`PermissionDenied`](ToolError::PermissionDenied) variant.
2164    pub fn permission_denied(denial: PermissionDenial) -> Self {
2165        Self::PermissionDenied(denial)
2166    }
2167}
2168
2169impl From<PermissionDenial> for ToolError {
2170    fn from(value: PermissionDenial) -> Self {
2171        Self::permission_denied(value)
2172    }
2173}
2174
2175#[cfg(test)]
2176mod tests {
2177    use super::*;
2178    use async_trait::async_trait;
2179    use serde_json::json;
2180
2181    #[test]
2182    fn command_policy_can_deny_unknown_executables_without_approval() {
2183        let policy = CommandPolicy::new()
2184            .allow_executable("pwd")
2185            .require_approval_for_unknown(false);
2186        let request = ShellPermissionRequest {
2187            executable: "rm".into(),
2188            argv: vec!["-rf".into(), "/tmp/demo".into()],
2189            cwd: None,
2190            env_keys: Vec::new(),
2191            metadata: MetadataMap::new(),
2192        };
2193
2194        match policy.evaluate(&request) {
2195            PolicyMatch::Deny(denial) => {
2196                assert_eq!(denial.code, PermissionCode::CommandNotAllowed);
2197            }
2198            other => panic!("unexpected policy match: {other:?}"),
2199        }
2200    }
2201
2202    #[test]
2203    fn path_policy_allows_reads_under_read_only_roots() {
2204        let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2205        let request = FileSystemPermissionRequest::Read {
2206            path: PathBuf::from("/workspace/vendor/lib.rs"),
2207            metadata: MetadataMap::new(),
2208        };
2209
2210        match policy.evaluate(&request) {
2211            PolicyMatch::NoOpinion | PolicyMatch::Allow => {}
2212            other => panic!("unexpected policy match: {other:?}"),
2213        }
2214    }
2215
2216    #[test]
2217    fn path_policy_denies_mutations_under_read_only_roots() {
2218        let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2219        let request = FileSystemPermissionRequest::Edit {
2220            path: PathBuf::from("/workspace/vendor/lib.rs"),
2221            metadata: MetadataMap::new(),
2222        };
2223
2224        match policy.evaluate(&request) {
2225            PolicyMatch::Deny(denial) => {
2226                assert_eq!(denial.code, PermissionCode::PathNotAllowed);
2227                assert!(denial.message.contains("read-only"));
2228            }
2229            other => panic!("unexpected policy match: {other:?}"),
2230        }
2231    }
2232
2233    #[test]
2234    fn path_policy_denies_moves_into_read_only_roots() {
2235        let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2236        let request = FileSystemPermissionRequest::Move {
2237            from: PathBuf::from("/workspace/src/lib.rs"),
2238            to: PathBuf::from("/workspace/vendor/lib.rs"),
2239            metadata: MetadataMap::new(),
2240        };
2241
2242        match policy.evaluate(&request) {
2243            PolicyMatch::Deny(denial) => {
2244                assert_eq!(denial.code, PermissionCode::PathNotAllowed);
2245                assert!(denial.message.contains("read-only"));
2246            }
2247            other => panic!("unexpected policy match: {other:?}"),
2248        }
2249    }
2250
2251    #[derive(Clone)]
2252    struct HiddenTool {
2253        spec: ToolSpec,
2254    }
2255
2256    impl HiddenTool {
2257        fn new() -> Self {
2258            Self {
2259                spec: ToolSpec {
2260                    name: ToolName::new("hidden"),
2261                    description: "hidden".into(),
2262                    input_schema: json!({"type": "object"}),
2263                    annotations: ToolAnnotations::default(),
2264                    metadata: MetadataMap::new(),
2265                },
2266            }
2267        }
2268    }
2269
2270    #[async_trait]
2271    impl Tool for HiddenTool {
2272        fn spec(&self) -> &ToolSpec {
2273            &self.spec
2274        }
2275
2276        fn current_spec(&self) -> Option<ToolSpec> {
2277            None
2278        }
2279
2280        async fn invoke(
2281            &self,
2282            request: ToolRequest,
2283            _ctx: &mut ToolContext<'_>,
2284        ) -> Result<ToolResult, ToolError> {
2285            Ok(ToolResult {
2286                result: ToolResultPart {
2287                    call_id: request.call_id,
2288                    output: ToolOutput::Text("hidden".into()),
2289                    is_error: false,
2290                    metadata: MetadataMap::new(),
2291                },
2292                duration: None,
2293                metadata: MetadataMap::new(),
2294            })
2295        }
2296    }
2297
2298    #[test]
2299    fn hidden_tools_are_omitted_from_specs_and_capabilities() {
2300        let registry = ToolRegistry::new().with(HiddenTool::new());
2301
2302        assert!(registry.specs().is_empty());
2303
2304        let provider = ToolCapabilityProvider::from_registry(
2305            &registry,
2306            Arc::new(AllowAllPermissionChecker),
2307            Arc::new(()),
2308        );
2309        assert!(provider.invocables().is_empty());
2310    }
2311
2312    struct AllowAllPermissionChecker;
2313
2314    impl PermissionChecker for AllowAllPermissionChecker {
2315        fn evaluate(&self, _request: &dyn PermissionRequest) -> PermissionDecision {
2316            PermissionDecision::Allow
2317        }
2318    }
2319}