1use std::any::Any;
2use std::collections::{BTreeMap, BTreeSet};
3use std::fmt;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::Duration;
8
9use agentkit_capabilities::{
10 CapabilityContext, CapabilityError, CapabilityName, CapabilityProvider, Invocable,
11 InvocableOutput, InvocableRequest, InvocableResult, InvocableSpec, PromptProvider,
12 ResourceProvider,
13};
14use agentkit_core::{
15 ApprovalId, Item, ItemKind, MetadataMap, Part, SessionId, ToolCallId, ToolOutput,
16 ToolResultPart, TurnCancellation, TurnId,
17};
18use async_trait::async_trait;
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use thiserror::Error;
22
23#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
24pub struct ToolName(pub String);
25
26impl ToolName {
27 pub fn new(value: impl Into<String>) -> Self {
28 Self(value.into())
29 }
30}
31
32impl fmt::Display for ToolName {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 self.0.fmt(f)
35 }
36}
37
38impl From<&str> for ToolName {
39 fn from(value: &str) -> Self {
40 Self::new(value)
41 }
42}
43
44#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub struct ToolAnnotations {
46 pub read_only_hint: bool,
47 pub destructive_hint: bool,
48 pub idempotent_hint: bool,
49 pub needs_approval_hint: bool,
50 pub supports_streaming_hint: bool,
51}
52
53#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
54pub struct ToolSpec {
55 pub name: ToolName,
56 pub description: String,
57 pub input_schema: Value,
58 pub annotations: ToolAnnotations,
59 pub metadata: MetadataMap,
60}
61
62#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
63pub struct ToolRequest {
64 pub call_id: ToolCallId,
65 pub tool_name: ToolName,
66 pub input: Value,
67 pub session_id: SessionId,
68 pub turn_id: TurnId,
69 pub metadata: MetadataMap,
70}
71
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73pub struct ToolResult {
74 pub result: ToolResultPart,
75 pub duration: Option<Duration>,
76 pub metadata: MetadataMap,
77}
78
79pub trait ToolResources: Send + Sync {
80 fn as_any(&self) -> &dyn Any;
81}
82
83impl ToolResources for () {
84 fn as_any(&self) -> &dyn Any {
85 self
86 }
87}
88
89pub struct ToolContext<'a> {
90 pub capability: CapabilityContext<'a>,
91 pub permissions: &'a dyn PermissionChecker,
92 pub resources: &'a dyn ToolResources,
93 pub cancellation: Option<TurnCancellation>,
94}
95
96pub trait PermissionRequest: Send + Sync {
97 fn kind(&self) -> &'static str;
98 fn summary(&self) -> String;
99 fn metadata(&self) -> &MetadataMap;
100 fn as_any(&self) -> &dyn Any;
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
104pub enum PermissionCode {
105 PathNotAllowed,
106 CommandNotAllowed,
107 NetworkNotAllowed,
108 ServerNotTrusted,
109 AuthScopeNotAllowed,
110 CustomPolicyDenied,
111 UnknownRequest,
112}
113
114#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
115pub struct PermissionDenial {
116 pub code: PermissionCode,
117 pub message: String,
118 pub metadata: MetadataMap,
119}
120
121#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
122pub enum ApprovalReason {
123 PolicyRequiresConfirmation,
124 EscalatedRisk,
125 UnknownTarget,
126 SensitivePath,
127 SensitiveCommand,
128 SensitiveServer,
129 SensitiveAuthScope,
130}
131
132#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
133pub struct ApprovalRequest {
134 pub id: ApprovalId,
135 pub request_kind: String,
136 pub reason: ApprovalReason,
137 pub summary: String,
138 pub metadata: MetadataMap,
139}
140
141#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
142pub enum ApprovalDecision {
143 Approve,
144 Deny { reason: Option<String> },
145}
146
147#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
148pub struct AuthRequest {
149 pub id: String,
150 pub provider: String,
151 pub operation: AuthOperation,
152 pub challenge: MetadataMap,
153}
154
155#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
156pub enum AuthOperation {
157 ToolCall {
158 tool_name: String,
159 input: Value,
160 call_id: Option<ToolCallId>,
161 session_id: Option<SessionId>,
162 turn_id: Option<TurnId>,
163 metadata: MetadataMap,
164 },
165 McpConnect {
166 server_id: String,
167 metadata: MetadataMap,
168 },
169 McpToolCall {
170 server_id: String,
171 tool_name: String,
172 input: Value,
173 metadata: MetadataMap,
174 },
175 McpResourceRead {
176 server_id: String,
177 resource_id: String,
178 metadata: MetadataMap,
179 },
180 McpPromptGet {
181 server_id: String,
182 prompt_id: String,
183 args: Value,
184 metadata: MetadataMap,
185 },
186 Custom {
187 kind: String,
188 payload: Value,
189 metadata: MetadataMap,
190 },
191}
192
193impl AuthOperation {
194 pub fn server_id(&self) -> Option<&str> {
195 match self {
196 Self::McpConnect { server_id, .. }
197 | Self::McpToolCall { server_id, .. }
198 | Self::McpResourceRead { server_id, .. }
199 | Self::McpPromptGet { server_id, .. } => Some(server_id.as_str()),
200 Self::ToolCall { metadata, .. } | Self::Custom { metadata, .. } => {
201 metadata.get("server_id").and_then(Value::as_str)
202 }
203 }
204 }
205}
206
207#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
208pub enum AuthResolution {
209 Provided {
210 request: AuthRequest,
211 credentials: MetadataMap,
212 },
213 Cancelled {
214 request: AuthRequest,
215 },
216}
217
218impl AuthResolution {
219 pub fn request(&self) -> &AuthRequest {
220 match self {
221 Self::Provided { request, .. } | Self::Cancelled { request } => request,
222 }
223 }
224}
225
226impl AuthRequest {
227 pub fn server_id(&self) -> Option<&str> {
228 self.operation.server_id()
229 }
230}
231
232#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
233pub enum ToolInterruption {
234 ApprovalRequired(ApprovalRequest),
235 AuthRequired(AuthRequest),
236}
237
238#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
239pub enum PermissionDecision {
240 Allow,
241 Deny(PermissionDenial),
242 RequireApproval(ApprovalRequest),
243}
244
245pub trait PermissionChecker: Send + Sync {
246 fn evaluate(&self, request: &dyn PermissionRequest) -> PermissionDecision;
247}
248
249#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
250pub enum PolicyMatch {
251 NoOpinion,
252 Allow,
253 Deny(PermissionDenial),
254 RequireApproval(ApprovalRequest),
255}
256
257pub trait PermissionPolicy: Send + Sync {
258 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch;
259}
260
261pub struct CompositePermissionChecker {
262 policies: Vec<Box<dyn PermissionPolicy>>,
263 fallback: PermissionDecision,
264}
265
266impl CompositePermissionChecker {
267 pub fn new(fallback: PermissionDecision) -> Self {
268 Self {
269 policies: Vec::new(),
270 fallback,
271 }
272 }
273
274 pub fn with_policy(mut self, policy: impl PermissionPolicy + 'static) -> Self {
275 self.policies.push(Box::new(policy));
276 self
277 }
278}
279
280impl PermissionChecker for CompositePermissionChecker {
281 fn evaluate(&self, request: &dyn PermissionRequest) -> PermissionDecision {
282 let mut saw_allow = false;
283 let mut approval = None;
284
285 for policy in &self.policies {
286 match policy.evaluate(request) {
287 PolicyMatch::NoOpinion => {}
288 PolicyMatch::Allow => saw_allow = true,
289 PolicyMatch::Deny(denial) => return PermissionDecision::Deny(denial),
290 PolicyMatch::RequireApproval(req) => approval = Some(req),
291 }
292 }
293
294 if let Some(req) = approval {
295 PermissionDecision::RequireApproval(req)
296 } else if saw_allow {
297 PermissionDecision::Allow
298 } else {
299 self.fallback.clone()
300 }
301 }
302}
303
304#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
305pub struct ShellPermissionRequest {
306 pub executable: String,
307 pub argv: Vec<String>,
308 pub cwd: Option<PathBuf>,
309 pub env_keys: Vec<String>,
310 pub metadata: MetadataMap,
311}
312
313impl PermissionRequest for ShellPermissionRequest {
314 fn kind(&self) -> &'static str {
315 "shell.command"
316 }
317
318 fn summary(&self) -> String {
319 if self.argv.is_empty() {
320 self.executable.clone()
321 } else {
322 format!("{} {}", self.executable, self.argv.join(" "))
323 }
324 }
325
326 fn metadata(&self) -> &MetadataMap {
327 &self.metadata
328 }
329
330 fn as_any(&self) -> &dyn Any {
331 self
332 }
333}
334
335#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
336pub enum FileSystemPermissionRequest {
337 Read {
338 path: PathBuf,
339 metadata: MetadataMap,
340 },
341 Write {
342 path: PathBuf,
343 metadata: MetadataMap,
344 },
345 Edit {
346 path: PathBuf,
347 metadata: MetadataMap,
348 },
349 Delete {
350 path: PathBuf,
351 metadata: MetadataMap,
352 },
353 Move {
354 from: PathBuf,
355 to: PathBuf,
356 metadata: MetadataMap,
357 },
358 List {
359 path: PathBuf,
360 metadata: MetadataMap,
361 },
362 CreateDir {
363 path: PathBuf,
364 metadata: MetadataMap,
365 },
366}
367
368impl FileSystemPermissionRequest {
369 fn metadata_map(&self) -> &MetadataMap {
370 match self {
371 Self::Read { metadata, .. }
372 | Self::Write { metadata, .. }
373 | Self::Edit { metadata, .. }
374 | Self::Delete { metadata, .. }
375 | Self::Move { metadata, .. }
376 | Self::List { metadata, .. }
377 | Self::CreateDir { metadata, .. } => metadata,
378 }
379 }
380}
381
382impl PermissionRequest for FileSystemPermissionRequest {
383 fn kind(&self) -> &'static str {
384 match self {
385 Self::Read { .. } => "filesystem.read",
386 Self::Write { .. } => "filesystem.write",
387 Self::Edit { .. } => "filesystem.edit",
388 Self::Delete { .. } => "filesystem.delete",
389 Self::Move { .. } => "filesystem.move",
390 Self::List { .. } => "filesystem.list",
391 Self::CreateDir { .. } => "filesystem.mkdir",
392 }
393 }
394
395 fn summary(&self) -> String {
396 match self {
397 Self::Read { path, .. } => format!("Read {}", path.display()),
398 Self::Write { path, .. } => format!("Write {}", path.display()),
399 Self::Edit { path, .. } => format!("Edit {}", path.display()),
400 Self::Delete { path, .. } => format!("Delete {}", path.display()),
401 Self::Move { from, to, .. } => {
402 format!("Move {} to {}", from.display(), to.display())
403 }
404 Self::List { path, .. } => format!("List {}", path.display()),
405 Self::CreateDir { path, .. } => format!("Create directory {}", path.display()),
406 }
407 }
408
409 fn metadata(&self) -> &MetadataMap {
410 self.metadata_map()
411 }
412
413 fn as_any(&self) -> &dyn Any {
414 self
415 }
416}
417
418#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
419pub enum McpPermissionRequest {
420 Connect {
421 server_id: String,
422 metadata: MetadataMap,
423 },
424 InvokeTool {
425 server_id: String,
426 tool_name: String,
427 metadata: MetadataMap,
428 },
429 ReadResource {
430 server_id: String,
431 resource_id: String,
432 metadata: MetadataMap,
433 },
434 FetchPrompt {
435 server_id: String,
436 prompt_id: String,
437 metadata: MetadataMap,
438 },
439 UseAuthScope {
440 server_id: String,
441 scope: String,
442 metadata: MetadataMap,
443 },
444}
445
446impl McpPermissionRequest {
447 fn metadata_map(&self) -> &MetadataMap {
448 match self {
449 Self::Connect { metadata, .. }
450 | Self::InvokeTool { metadata, .. }
451 | Self::ReadResource { metadata, .. }
452 | Self::FetchPrompt { metadata, .. }
453 | Self::UseAuthScope { metadata, .. } => metadata,
454 }
455 }
456}
457
458impl PermissionRequest for McpPermissionRequest {
459 fn kind(&self) -> &'static str {
460 match self {
461 Self::Connect { .. } => "mcp.connect",
462 Self::InvokeTool { .. } => "mcp.invoke_tool",
463 Self::ReadResource { .. } => "mcp.read_resource",
464 Self::FetchPrompt { .. } => "mcp.fetch_prompt",
465 Self::UseAuthScope { .. } => "mcp.use_auth_scope",
466 }
467 }
468
469 fn summary(&self) -> String {
470 match self {
471 Self::Connect { server_id, .. } => format!("Connect MCP server {server_id}"),
472 Self::InvokeTool {
473 server_id,
474 tool_name,
475 ..
476 } => format!("Invoke MCP tool {server_id}.{tool_name}"),
477 Self::ReadResource {
478 server_id,
479 resource_id,
480 ..
481 } => format!("Read MCP resource {server_id}:{resource_id}"),
482 Self::FetchPrompt {
483 server_id,
484 prompt_id,
485 ..
486 } => format!("Fetch MCP prompt {server_id}:{prompt_id}"),
487 Self::UseAuthScope {
488 server_id, scope, ..
489 } => format!("Use MCP auth scope {server_id}:{scope}"),
490 }
491 }
492
493 fn metadata(&self) -> &MetadataMap {
494 self.metadata_map()
495 }
496
497 fn as_any(&self) -> &dyn Any {
498 self
499 }
500}
501
502pub struct CustomKindPolicy {
503 allowed_kinds: BTreeSet<String>,
504 denied_kinds: BTreeSet<String>,
505 require_approval_by_default: bool,
506}
507
508impl CustomKindPolicy {
509 pub fn new(require_approval_by_default: bool) -> Self {
510 Self {
511 allowed_kinds: BTreeSet::new(),
512 denied_kinds: BTreeSet::new(),
513 require_approval_by_default,
514 }
515 }
516
517 pub fn allow_kind(mut self, kind: impl Into<String>) -> Self {
518 self.allowed_kinds.insert(kind.into());
519 self
520 }
521
522 pub fn deny_kind(mut self, kind: impl Into<String>) -> Self {
523 self.denied_kinds.insert(kind.into());
524 self
525 }
526}
527
528impl PermissionPolicy for CustomKindPolicy {
529 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
530 let kind = request.kind();
531 if !kind.starts_with("custom.") {
532 return PolicyMatch::NoOpinion;
533 }
534 if self.denied_kinds.contains(kind) {
535 return PolicyMatch::Deny(PermissionDenial {
536 code: PermissionCode::CustomPolicyDenied,
537 message: format!("custom permission kind {kind} is denied"),
538 metadata: request.metadata().clone(),
539 });
540 }
541 if self.allowed_kinds.contains(kind) {
542 return PolicyMatch::Allow;
543 }
544 if self.require_approval_by_default {
545 PolicyMatch::RequireApproval(ApprovalRequest {
546 id: ApprovalId::new(format!("approval:{kind}")),
547 request_kind: kind.to_string(),
548 reason: ApprovalReason::PolicyRequiresConfirmation,
549 summary: request.summary(),
550 metadata: request.metadata().clone(),
551 })
552 } else {
553 PolicyMatch::NoOpinion
554 }
555 }
556}
557
558pub struct PathPolicy {
559 allowed_roots: Vec<PathBuf>,
560 protected_roots: Vec<PathBuf>,
561 require_approval_outside_allowed: bool,
562}
563
564impl PathPolicy {
565 pub fn new() -> Self {
566 Self {
567 allowed_roots: Vec::new(),
568 protected_roots: Vec::new(),
569 require_approval_outside_allowed: true,
570 }
571 }
572
573 pub fn allow_root(mut self, root: impl Into<PathBuf>) -> Self {
574 self.allowed_roots.push(root.into());
575 self
576 }
577
578 pub fn protect_root(mut self, root: impl Into<PathBuf>) -> Self {
579 self.protected_roots.push(root.into());
580 self
581 }
582
583 pub fn require_approval_outside_allowed(mut self, value: bool) -> Self {
584 self.require_approval_outside_allowed = value;
585 self
586 }
587}
588
589impl Default for PathPolicy {
590 fn default() -> Self {
591 Self::new()
592 }
593}
594
595impl PermissionPolicy for PathPolicy {
596 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
597 let Some(fs) = request
598 .as_any()
599 .downcast_ref::<FileSystemPermissionRequest>()
600 else {
601 return PolicyMatch::NoOpinion;
602 };
603
604 let candidate_paths: Vec<&Path> = match fs {
605 FileSystemPermissionRequest::Move { from, to, .. } => {
606 vec![from.as_path(), to.as_path()]
607 }
608 FileSystemPermissionRequest::Read { path, .. }
609 | FileSystemPermissionRequest::Write { path, .. }
610 | FileSystemPermissionRequest::Edit { path, .. }
611 | FileSystemPermissionRequest::Delete { path, .. }
612 | FileSystemPermissionRequest::List { path, .. }
613 | FileSystemPermissionRequest::CreateDir { path, .. } => vec![path.as_path()],
614 };
615
616 if candidate_paths.iter().any(|path| {
617 self.protected_roots
618 .iter()
619 .any(|root| path.starts_with(root))
620 }) {
621 return PolicyMatch::Deny(PermissionDenial {
622 code: PermissionCode::PathNotAllowed,
623 message: format!("path access denied for {}", fs.summary()),
624 metadata: fs.metadata().clone(),
625 });
626 }
627
628 if self.allowed_roots.is_empty() {
629 return PolicyMatch::NoOpinion;
630 }
631
632 let all_allowed = candidate_paths
633 .iter()
634 .all(|path| self.allowed_roots.iter().any(|root| path.starts_with(root)));
635
636 if all_allowed {
637 PolicyMatch::Allow
638 } else if self.require_approval_outside_allowed {
639 PolicyMatch::RequireApproval(ApprovalRequest {
640 id: ApprovalId::new(format!("approval:{}", fs.kind())),
641 request_kind: fs.kind().to_string(),
642 reason: ApprovalReason::SensitivePath,
643 summary: fs.summary(),
644 metadata: fs.metadata().clone(),
645 })
646 } else {
647 PolicyMatch::Deny(PermissionDenial {
648 code: PermissionCode::PathNotAllowed,
649 message: format!("path outside allowed roots for {}", fs.summary()),
650 metadata: fs.metadata().clone(),
651 })
652 }
653 }
654}
655
656pub struct CommandPolicy {
657 allowed_executables: BTreeSet<String>,
658 denied_executables: BTreeSet<String>,
659 allowed_cwds: Vec<PathBuf>,
660 denied_env_keys: BTreeSet<String>,
661 require_approval_for_unknown: bool,
662}
663
664impl CommandPolicy {
665 pub fn new() -> Self {
666 Self {
667 allowed_executables: BTreeSet::new(),
668 denied_executables: BTreeSet::new(),
669 allowed_cwds: Vec::new(),
670 denied_env_keys: BTreeSet::new(),
671 require_approval_for_unknown: true,
672 }
673 }
674
675 pub fn allow_executable(mut self, executable: impl Into<String>) -> Self {
676 self.allowed_executables.insert(executable.into());
677 self
678 }
679
680 pub fn deny_executable(mut self, executable: impl Into<String>) -> Self {
681 self.denied_executables.insert(executable.into());
682 self
683 }
684
685 pub fn allow_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
686 self.allowed_cwds.push(cwd.into());
687 self
688 }
689
690 pub fn deny_env_key(mut self, key: impl Into<String>) -> Self {
691 self.denied_env_keys.insert(key.into());
692 self
693 }
694
695 pub fn require_approval_for_unknown(mut self, value: bool) -> Self {
696 self.require_approval_for_unknown = value;
697 self
698 }
699}
700
701impl Default for CommandPolicy {
702 fn default() -> Self {
703 Self::new()
704 }
705}
706
707impl PermissionPolicy for CommandPolicy {
708 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
709 let Some(shell) = request.as_any().downcast_ref::<ShellPermissionRequest>() else {
710 return PolicyMatch::NoOpinion;
711 };
712
713 if self.denied_executables.contains(&shell.executable)
714 || shell
715 .env_keys
716 .iter()
717 .any(|key| self.denied_env_keys.contains(key))
718 {
719 return PolicyMatch::Deny(PermissionDenial {
720 code: PermissionCode::CommandNotAllowed,
721 message: format!("command denied for {}", shell.summary()),
722 metadata: shell.metadata().clone(),
723 });
724 }
725
726 if let Some(cwd) = &shell.cwd
727 && !self.allowed_cwds.is_empty()
728 && !self.allowed_cwds.iter().any(|root| cwd.starts_with(root))
729 {
730 return PolicyMatch::RequireApproval(ApprovalRequest {
731 id: ApprovalId::new("approval:shell.cwd"),
732 request_kind: shell.kind().to_string(),
733 reason: ApprovalReason::SensitiveCommand,
734 summary: shell.summary(),
735 metadata: shell.metadata().clone(),
736 });
737 }
738
739 if self.allowed_executables.is_empty()
740 || self.allowed_executables.contains(&shell.executable)
741 {
742 PolicyMatch::Allow
743 } else if self.require_approval_for_unknown {
744 PolicyMatch::RequireApproval(ApprovalRequest {
745 id: ApprovalId::new("approval:shell.command"),
746 request_kind: shell.kind().to_string(),
747 reason: ApprovalReason::SensitiveCommand,
748 summary: shell.summary(),
749 metadata: shell.metadata().clone(),
750 })
751 } else {
752 PolicyMatch::Deny(PermissionDenial {
753 code: PermissionCode::CommandNotAllowed,
754 message: format!("executable {} is not allowed", shell.executable),
755 metadata: shell.metadata().clone(),
756 })
757 }
758 }
759}
760
761pub struct McpServerPolicy {
762 trusted_servers: BTreeSet<String>,
763 allowed_auth_scopes: BTreeSet<String>,
764 require_approval_for_untrusted: bool,
765}
766
767impl McpServerPolicy {
768 pub fn new() -> Self {
769 Self {
770 trusted_servers: BTreeSet::new(),
771 allowed_auth_scopes: BTreeSet::new(),
772 require_approval_for_untrusted: true,
773 }
774 }
775
776 pub fn trust_server(mut self, server_id: impl Into<String>) -> Self {
777 self.trusted_servers.insert(server_id.into());
778 self
779 }
780
781 pub fn allow_auth_scope(mut self, scope: impl Into<String>) -> Self {
782 self.allowed_auth_scopes.insert(scope.into());
783 self
784 }
785}
786
787impl Default for McpServerPolicy {
788 fn default() -> Self {
789 Self::new()
790 }
791}
792
793impl PermissionPolicy for McpServerPolicy {
794 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
795 let Some(mcp) = request.as_any().downcast_ref::<McpPermissionRequest>() else {
796 return PolicyMatch::NoOpinion;
797 };
798
799 let server_id = match mcp {
800 McpPermissionRequest::Connect { server_id, .. }
801 | McpPermissionRequest::InvokeTool { server_id, .. }
802 | McpPermissionRequest::ReadResource { server_id, .. }
803 | McpPermissionRequest::FetchPrompt { server_id, .. }
804 | McpPermissionRequest::UseAuthScope { server_id, .. } => server_id,
805 };
806
807 if !self.trusted_servers.is_empty() && !self.trusted_servers.contains(server_id) {
808 return if self.require_approval_for_untrusted {
809 PolicyMatch::RequireApproval(ApprovalRequest {
810 id: ApprovalId::new(format!("approval:mcp:{server_id}")),
811 request_kind: mcp.kind().to_string(),
812 reason: ApprovalReason::SensitiveServer,
813 summary: mcp.summary(),
814 metadata: mcp.metadata().clone(),
815 })
816 } else {
817 PolicyMatch::Deny(PermissionDenial {
818 code: PermissionCode::ServerNotTrusted,
819 message: format!("MCP server {server_id} is not trusted"),
820 metadata: mcp.metadata().clone(),
821 })
822 };
823 }
824
825 if let McpPermissionRequest::UseAuthScope { scope, .. } = mcp
826 && !self.allowed_auth_scopes.is_empty()
827 && !self.allowed_auth_scopes.contains(scope)
828 {
829 return PolicyMatch::Deny(PermissionDenial {
830 code: PermissionCode::AuthScopeNotAllowed,
831 message: format!("MCP auth scope {scope} is not allowed"),
832 metadata: mcp.metadata().clone(),
833 });
834 }
835
836 PolicyMatch::Allow
837 }
838}
839
840#[async_trait]
841pub trait Tool: Send + Sync {
842 fn spec(&self) -> &ToolSpec;
843
844 fn proposed_requests(
845 &self,
846 _request: &ToolRequest,
847 ) -> Result<Vec<Box<dyn PermissionRequest>>, ToolError> {
848 Ok(Vec::new())
849 }
850
851 async fn invoke(
852 &self,
853 request: ToolRequest,
854 ctx: &mut ToolContext<'_>,
855 ) -> Result<ToolResult, ToolError>;
856}
857
858#[derive(Clone, Default)]
859pub struct ToolRegistry {
860 tools: BTreeMap<ToolName, Arc<dyn Tool>>,
861}
862
863impl ToolRegistry {
864 pub fn new() -> Self {
865 Self::default()
866 }
867
868 pub fn register<T>(&mut self, tool: T) -> &mut Self
869 where
870 T: Tool + 'static,
871 {
872 self.tools.insert(tool.spec().name.clone(), Arc::new(tool));
873 self
874 }
875
876 pub fn with<T>(mut self, tool: T) -> Self
877 where
878 T: Tool + 'static,
879 {
880 self.register(tool);
881 self
882 }
883
884 pub fn register_arc(&mut self, tool: Arc<dyn Tool>) -> &mut Self {
885 self.tools.insert(tool.spec().name.clone(), tool);
886 self
887 }
888
889 pub fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
890 self.tools.get(name).cloned()
891 }
892
893 pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
894 self.tools.values().cloned().collect()
895 }
896
897 pub fn specs(&self) -> Vec<ToolSpec> {
898 self.tools
899 .values()
900 .map(|tool| tool.spec().clone())
901 .collect()
902 }
903}
904
905impl ToolSpec {
906 pub fn as_invocable_spec(&self) -> InvocableSpec {
907 InvocableSpec {
908 name: CapabilityName::new(self.name.0.clone()),
909 description: self.description.clone(),
910 input_schema: self.input_schema.clone(),
911 metadata: self.metadata.clone(),
912 }
913 }
914}
915
916pub struct ToolInvocableAdapter {
917 spec: InvocableSpec,
918 tool: Arc<dyn Tool>,
919 permissions: Arc<dyn PermissionChecker>,
920 resources: Arc<dyn ToolResources>,
921 next_call_id: AtomicU64,
922}
923
924impl ToolInvocableAdapter {
925 pub fn new(
926 tool: Arc<dyn Tool>,
927 permissions: Arc<dyn PermissionChecker>,
928 resources: Arc<dyn ToolResources>,
929 ) -> Self {
930 let spec = tool.spec().as_invocable_spec();
931 Self {
932 spec,
933 tool,
934 permissions,
935 resources,
936 next_call_id: AtomicU64::new(1),
937 }
938 }
939}
940
941#[async_trait]
942impl Invocable for ToolInvocableAdapter {
943 fn spec(&self) -> &InvocableSpec {
944 &self.spec
945 }
946
947 async fn invoke(
948 &self,
949 request: InvocableRequest,
950 ctx: &mut CapabilityContext<'_>,
951 ) -> Result<InvocableResult, CapabilityError> {
952 let tool_request = ToolRequest {
953 call_id: ToolCallId::new(format!(
954 "tool-call-{}",
955 self.next_call_id.fetch_add(1, Ordering::Relaxed)
956 )),
957 tool_name: self.tool.spec().name.clone(),
958 input: request.input,
959 session_id: ctx
960 .session_id
961 .cloned()
962 .unwrap_or_else(|| SessionId::new("capability-session")),
963 turn_id: ctx
964 .turn_id
965 .cloned()
966 .unwrap_or_else(|| TurnId::new("capability-turn")),
967 metadata: request.metadata,
968 };
969
970 for permission_request in self
971 .tool
972 .proposed_requests(&tool_request)
973 .map_err(|error| CapabilityError::InvalidInput(error.to_string()))?
974 {
975 match self.permissions.evaluate(permission_request.as_ref()) {
976 PermissionDecision::Allow => {}
977 PermissionDecision::Deny(denial) => {
978 return Err(CapabilityError::ExecutionFailed(format!(
979 "tool permission denied: {denial:?}"
980 )));
981 }
982 PermissionDecision::RequireApproval(req) => {
983 return Err(CapabilityError::Unavailable(format!(
984 "tool invocation requires approval: {}",
985 req.summary
986 )));
987 }
988 }
989 }
990
991 let mut tool_ctx = ToolContext {
992 capability: CapabilityContext {
993 session_id: ctx.session_id,
994 turn_id: ctx.turn_id,
995 metadata: ctx.metadata,
996 },
997 permissions: self.permissions.as_ref(),
998 resources: self.resources.as_ref(),
999 cancellation: None,
1000 };
1001
1002 let result = self
1003 .tool
1004 .invoke(tool_request, &mut tool_ctx)
1005 .await
1006 .map_err(|error| CapabilityError::ExecutionFailed(error.to_string()))?;
1007
1008 Ok(InvocableResult {
1009 output: match result.result.output {
1010 ToolOutput::Text(text) => InvocableOutput::Text(text),
1011 ToolOutput::Structured(value) => InvocableOutput::Structured(value),
1012 ToolOutput::Parts(parts) => InvocableOutput::Items(vec![Item {
1013 id: None,
1014 kind: ItemKind::Tool,
1015 parts,
1016 metadata: MetadataMap::new(),
1017 }]),
1018 ToolOutput::Files(files) => {
1019 let parts = files.into_iter().map(Part::File).collect();
1020 InvocableOutput::Items(vec![Item {
1021 id: None,
1022 kind: ItemKind::Tool,
1023 parts,
1024 metadata: MetadataMap::new(),
1025 }])
1026 }
1027 },
1028 metadata: result.metadata,
1029 })
1030 }
1031}
1032
1033pub struct ToolCapabilityProvider {
1034 invocables: Vec<Arc<dyn Invocable>>,
1035}
1036
1037impl ToolCapabilityProvider {
1038 pub fn from_registry(
1039 registry: &ToolRegistry,
1040 permissions: Arc<dyn PermissionChecker>,
1041 resources: Arc<dyn ToolResources>,
1042 ) -> Self {
1043 let invocables = registry
1044 .tools()
1045 .into_iter()
1046 .map(|tool| {
1047 Arc::new(ToolInvocableAdapter::new(
1048 tool,
1049 permissions.clone(),
1050 resources.clone(),
1051 )) as Arc<dyn Invocable>
1052 })
1053 .collect();
1054
1055 Self { invocables }
1056 }
1057}
1058
1059impl CapabilityProvider for ToolCapabilityProvider {
1060 fn invocables(&self) -> Vec<Arc<dyn Invocable>> {
1061 self.invocables.clone()
1062 }
1063
1064 fn resources(&self) -> Vec<Arc<dyn ResourceProvider>> {
1065 Vec::new()
1066 }
1067
1068 fn prompts(&self) -> Vec<Arc<dyn PromptProvider>> {
1069 Vec::new()
1070 }
1071}
1072
1073#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1074pub enum ToolExecutionOutcome {
1075 Completed(ToolResult),
1076 Interrupted(ToolInterruption),
1077 Failed(ToolError),
1078}
1079
1080#[async_trait]
1081pub trait ToolExecutor: Send + Sync {
1082 async fn execute(
1083 &self,
1084 request: ToolRequest,
1085 ctx: &mut ToolContext<'_>,
1086 ) -> ToolExecutionOutcome;
1087
1088 async fn execute_approved(
1089 &self,
1090 request: ToolRequest,
1091 approved_request: &ApprovalRequest,
1092 ctx: &mut ToolContext<'_>,
1093 ) -> ToolExecutionOutcome {
1094 let _ = approved_request;
1095 self.execute(request, ctx).await
1096 }
1097}
1098
1099pub struct BasicToolExecutor {
1100 registry: ToolRegistry,
1101}
1102
1103impl BasicToolExecutor {
1104 pub fn new(registry: ToolRegistry) -> Self {
1105 Self { registry }
1106 }
1107
1108 pub fn specs(&self) -> Vec<ToolSpec> {
1109 self.registry.specs()
1110 }
1111
1112 async fn execute_inner(
1113 &self,
1114 request: ToolRequest,
1115 approved_request_id: Option<&ApprovalId>,
1116 ctx: &mut ToolContext<'_>,
1117 ) -> ToolExecutionOutcome {
1118 let Some(tool) = self.registry.get(&request.tool_name) else {
1119 return ToolExecutionOutcome::Failed(ToolError::NotFound(request.tool_name));
1120 };
1121
1122 match tool.proposed_requests(&request) {
1123 Ok(requests) => {
1124 for permission_request in requests {
1125 match ctx.permissions.evaluate(permission_request.as_ref()) {
1126 PermissionDecision::Allow => {}
1127 PermissionDecision::Deny(denial) => {
1128 return ToolExecutionOutcome::Failed(ToolError::PermissionDenied(
1129 denial,
1130 ));
1131 }
1132 PermissionDecision::RequireApproval(req) => {
1133 if approved_request_id != Some(&req.id) {
1134 return ToolExecutionOutcome::Interrupted(
1135 ToolInterruption::ApprovalRequired(req),
1136 );
1137 }
1138 }
1139 }
1140 }
1141 }
1142 Err(error) => return ToolExecutionOutcome::Failed(error),
1143 }
1144
1145 match tool.invoke(request, ctx).await {
1146 Ok(result) => ToolExecutionOutcome::Completed(result),
1147 Err(ToolError::AuthRequired(request)) => {
1148 ToolExecutionOutcome::Interrupted(ToolInterruption::AuthRequired(*request))
1149 }
1150 Err(error) => ToolExecutionOutcome::Failed(error),
1151 }
1152 }
1153}
1154
1155#[async_trait]
1156impl ToolExecutor for BasicToolExecutor {
1157 async fn execute(
1158 &self,
1159 request: ToolRequest,
1160 ctx: &mut ToolContext<'_>,
1161 ) -> ToolExecutionOutcome {
1162 self.execute_inner(request, None, ctx).await
1163 }
1164
1165 async fn execute_approved(
1166 &self,
1167 request: ToolRequest,
1168 approved_request: &ApprovalRequest,
1169 ctx: &mut ToolContext<'_>,
1170 ) -> ToolExecutionOutcome {
1171 self.execute_inner(request, Some(&approved_request.id), ctx)
1172 .await
1173 }
1174}
1175
1176#[derive(Debug, Error, Clone, PartialEq, Serialize, Deserialize)]
1177pub enum ToolError {
1178 #[error("tool not found: {0}")]
1179 NotFound(ToolName),
1180 #[error("invalid tool input: {0}")]
1181 InvalidInput(String),
1182 #[error("tool permission denied: {0:?}")]
1183 PermissionDenied(PermissionDenial),
1184 #[error("tool execution failed: {0}")]
1185 ExecutionFailed(String),
1186 #[error("tool auth required: {0:?}")]
1187 AuthRequired(Box<AuthRequest>),
1188 #[error("tool unavailable: {0}")]
1189 Unavailable(String),
1190 #[error("tool execution cancelled")]
1191 Cancelled,
1192 #[error("internal tool error: {0}")]
1193 Internal(String),
1194}
1195
1196impl ToolError {
1197 pub fn permission_denied(denial: PermissionDenial) -> Self {
1198 Self::PermissionDenied(denial)
1199 }
1200}
1201
1202impl From<PermissionDenial> for ToolError {
1203 fn from(value: PermissionDenial) -> Self {
1204 Self::permission_denied(value)
1205 }
1206}
1207
1208#[cfg(test)]
1209mod tests {
1210 use super::*;
1211
1212 #[test]
1213 fn command_policy_can_deny_unknown_executables_without_approval() {
1214 let policy = CommandPolicy::new()
1215 .allow_executable("pwd")
1216 .require_approval_for_unknown(false);
1217 let request = ShellPermissionRequest {
1218 executable: "rm".into(),
1219 argv: vec!["-rf".into(), "/tmp/demo".into()],
1220 cwd: None,
1221 env_keys: Vec::new(),
1222 metadata: MetadataMap::new(),
1223 };
1224
1225 match policy.evaluate(&request) {
1226 PolicyMatch::Deny(denial) => {
1227 assert_eq!(denial.code, PermissionCode::CommandNotAllowed);
1228 }
1229 other => panic!("unexpected policy match: {other:?}"),
1230 }
1231 }
1232}