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 ®istry,
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}