1use 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#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
61pub struct ToolName(pub String);
62
63impl ToolName {
64 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#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
89pub struct ToolAnnotations {
90 pub read_only_hint: bool,
92 pub destructive_hint: bool,
94 pub idempotent_hint: bool,
96 pub needs_approval_hint: bool,
98 pub supports_streaming_hint: bool,
100}
101
102impl ToolAnnotations {
103 pub fn new() -> Self {
105 Self::default()
106 }
107
108 pub fn read_only() -> Self {
110 Self::default().with_read_only(true)
111 }
112
113 pub fn destructive() -> Self {
115 Self::default().with_destructive(true)
116 }
117
118 pub fn needs_approval() -> Self {
120 Self::default().with_needs_approval(true)
121 }
122
123 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
180pub struct ToolSpec {
181 pub name: ToolName,
183 pub description: String,
185 pub input_schema: Value,
187 pub annotations: ToolAnnotations,
189 pub metadata: MetadataMap,
191}
192
193impl ToolSpec {
194 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 pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
211 self.annotations = annotations;
212 self
213 }
214
215 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
217 self.metadata = metadata;
218 self
219 }
220}
221
222#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
228pub struct ToolRequest {
229 pub call_id: ToolCallId,
231 pub tool_name: ToolName,
233 pub input: Value,
235 pub session_id: SessionId,
237 pub turn_id: TurnId,
239 pub metadata: MetadataMap,
241}
242
243impl ToolRequest {
244 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 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
264 self.metadata = metadata;
265 self
266 }
267}
268
269#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
274pub struct ToolResult {
275 pub result: ToolResultPart,
277 pub duration: Option<Duration>,
279 pub metadata: MetadataMap,
281}
282
283impl ToolResult {
284 pub fn new(result: ToolResultPart) -> Self {
286 Self {
287 result,
288 duration: None,
289 metadata: MetadataMap::new(),
290 }
291 }
292
293 pub fn with_duration(mut self, duration: Duration) -> Self {
295 self.duration = Some(duration);
296 self
297 }
298
299 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
301 self.metadata = metadata;
302 self
303 }
304}
305
306pub trait ToolResources: Send + Sync {
332 fn as_any(&self) -> &dyn Any;
335}
336
337impl ToolResources for () {
338 fn as_any(&self) -> &dyn Any {
339 self
340 }
341}
342
343pub struct ToolContext<'a> {
349 pub capability: CapabilityContext<'a>,
351 pub permissions: &'a dyn PermissionChecker,
353 pub resources: &'a dyn ToolResources,
355 pub cancellation: Option<TurnCancellation>,
357}
358
359#[derive(Clone)]
365pub struct OwnedToolContext {
366 pub session_id: SessionId,
368 pub turn_id: TurnId,
370 pub metadata: MetadataMap,
372 pub permissions: Arc<dyn PermissionChecker>,
374 pub resources: Arc<dyn ToolResources>,
376 pub cancellation: Option<TurnCancellation>,
378}
379
380impl OwnedToolContext {
381 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
396pub trait PermissionRequest: Send + Sync {
425 fn kind(&self) -> &'static str;
427 fn summary(&self) -> String;
429 fn metadata(&self) -> &MetadataMap;
431 fn as_any(&self) -> &dyn Any;
433}
434
435#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
440pub enum PermissionCode {
441 PathNotAllowed,
443 CommandNotAllowed,
445 NetworkNotAllowed,
447 ServerNotTrusted,
449 AuthScopeNotAllowed,
451 CustomPolicyDenied,
453 UnknownRequest,
455}
456
457#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
462pub struct PermissionDenial {
463 pub code: PermissionCode,
465 pub message: String,
467 pub metadata: MetadataMap,
469}
470
471#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
476pub enum ApprovalReason {
477 PolicyRequiresConfirmation,
479 EscalatedRisk,
481 UnknownTarget,
483 SensitivePath,
485 SensitiveCommand,
487 SensitiveServer,
489 SensitiveAuthScope,
491}
492
493#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
498pub struct ApprovalRequest {
499 pub task_id: Option<TaskId>,
501 pub call_id: Option<ToolCallId>,
504 pub id: ApprovalId,
506 pub request_kind: String,
508 pub reason: ApprovalReason,
510 pub summary: String,
512 pub metadata: MetadataMap,
514}
515
516impl ApprovalRequest {
517 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 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 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 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
549 self.metadata = metadata;
550 self
551 }
552}
553
554#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
556pub enum ApprovalDecision {
557 Approve,
559 Deny {
561 reason: Option<String>,
563 },
564}
565
566#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
572pub struct AuthRequest {
573 pub task_id: Option<TaskId>,
575 pub id: String,
577 pub provider: String,
579 pub operation: AuthOperation,
581 pub challenge: MetadataMap,
583}
584
585impl AuthRequest {
586 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 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 pub fn with_challenge(mut self, challenge: MetadataMap) -> Self {
609 self.challenge = challenge;
610 self
611 }
612}
613
614#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
619pub enum AuthOperation {
620 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 McpConnect {
631 server_id: String,
632 metadata: MetadataMap,
633 },
634 McpToolCall {
636 server_id: String,
637 tool_name: String,
638 input: Value,
639 metadata: MetadataMap,
640 },
641 McpResourceRead {
643 server_id: String,
644 resource_id: String,
645 metadata: MetadataMap,
646 },
647 McpPromptGet {
649 server_id: String,
650 prompt_id: String,
651 args: Value,
652 metadata: MetadataMap,
653 },
654 Custom {
656 kind: String,
657 payload: Value,
658 metadata: MetadataMap,
659 },
660}
661
662impl AuthOperation {
663 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
680pub enum AuthResolution {
681 Provided {
683 request: AuthRequest,
685 credentials: MetadataMap,
687 },
688 Cancelled {
690 request: AuthRequest,
692 },
693}
694
695impl AuthResolution {
696 pub fn provided(request: AuthRequest, credentials: MetadataMap) -> Self {
698 Self::Provided {
699 request,
700 credentials,
701 }
702 }
703
704 pub fn cancelled(request: AuthRequest) -> Self {
706 Self::Cancelled { request }
707 }
708
709 pub fn request(&self) -> &AuthRequest {
712 match self {
713 Self::Provided { request, .. } | Self::Cancelled { request } => request,
714 }
715 }
716}
717
718impl AuthRequest {
719 pub fn server_id(&self) -> Option<&str> {
721 self.operation.server_id()
722 }
723}
724
725#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
730pub enum ToolInterruption {
731 ApprovalRequired(ApprovalRequest),
733 AuthRequired(AuthRequest),
735}
736
737#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
739pub enum PermissionDecision {
740 Allow,
742 Deny(PermissionDenial),
744 RequireApproval(ApprovalRequest),
746}
747
748pub trait PermissionChecker: Send + Sync {
772 fn evaluate(&self, request: &dyn PermissionRequest) -> PermissionDecision;
774}
775
776#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
782pub enum PolicyMatch {
783 NoOpinion,
785 Allow,
787 Deny(PermissionDenial),
789 RequireApproval(ApprovalRequest),
791}
792
793pub trait PermissionPolicy: Send + Sync {
802 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch;
804}
805
806pub struct CompositePermissionChecker {
826 policies: Vec<Box<dyn PermissionPolicy>>,
827 fallback: PermissionDecision,
828}
829
830impl CompositePermissionChecker {
831 pub fn new(fallback: PermissionDecision) -> Self {
839 Self {
840 policies: Vec::new(),
841 fallback,
842 }
843 }
844
845 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
881pub struct ShellPermissionRequest {
882 pub executable: String,
884 pub argv: Vec<String>,
886 pub cwd: Option<PathBuf>,
888 pub env_keys: Vec<String>,
890 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
921pub enum FileSystemPermissionRequest {
922 Read {
924 path: PathBuf,
925 metadata: MetadataMap,
926 },
927 Write {
929 path: PathBuf,
930 metadata: MetadataMap,
931 },
932 Edit {
934 path: PathBuf,
935 metadata: MetadataMap,
936 },
937 Delete {
939 path: PathBuf,
940 metadata: MetadataMap,
941 },
942 Move {
944 from: PathBuf,
945 to: PathBuf,
946 metadata: MetadataMap,
947 },
948 List {
950 path: PathBuf,
951 metadata: MetadataMap,
952 },
953 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1015pub enum McpPermissionRequest {
1016 Connect {
1018 server_id: String,
1019 metadata: MetadataMap,
1020 },
1021 InvokeTool {
1023 server_id: String,
1024 tool_name: String,
1025 metadata: MetadataMap,
1026 },
1027 ReadResource {
1029 server_id: String,
1030 resource_id: String,
1031 metadata: MetadataMap,
1032 },
1033 FetchPrompt {
1035 server_id: String,
1036 prompt_id: String,
1037 metadata: MetadataMap,
1038 },
1039 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
1103pub struct CustomKindPolicy {
1119 allowed_kinds: BTreeSet<String>,
1120 denied_kinds: BTreeSet<String>,
1121 require_approval_by_default: bool,
1122}
1123
1124impl CustomKindPolicy {
1125 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 pub fn allow_kind(mut self, kind: impl Into<String>) -> Self {
1141 self.allowed_kinds.insert(kind.into());
1142 self
1143 }
1144
1145 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
1184pub struct PathPolicy {
1204 allowed_roots: Vec<PathBuf>,
1205 read_only_roots: Vec<PathBuf>,
1206 protected_roots: Vec<PathBuf>,
1207 require_approval_outside_allowed: bool,
1208}
1209
1210impl PathPolicy {
1211 pub fn new() -> Self {
1214 Self {
1215 allowed_roots: Vec::new(),
1216 read_only_roots: Vec::new(),
1217 protected_roots: Vec::new(),
1218 require_approval_outside_allowed: true,
1219 }
1220 }
1221
1222 pub fn allow_root(mut self, root: impl Into<PathBuf>) -> Self {
1224 self.allowed_roots.push(root.into());
1225 self
1226 }
1227
1228 pub fn read_only_root(mut self, root: impl Into<PathBuf>) -> Self {
1230 self.read_only_roots.push(root.into());
1231 self
1232 }
1233
1234 pub fn protect_root(mut self, root: impl Into<PathBuf>) -> Self {
1236 self.protected_roots.push(root.into());
1237 self
1238 }
1239
1240 pub fn require_approval_outside_allowed(mut self, value: bool) -> Self {
1243 self.require_approval_outside_allowed = value;
1244 self
1245 }
1246}
1247
1248impl Default for PathPolicy {
1249 fn default() -> Self {
1250 Self::new()
1251 }
1252}
1253
1254impl PermissionPolicy for PathPolicy {
1255 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1256 let Some(fs) = request
1257 .as_any()
1258 .downcast_ref::<FileSystemPermissionRequest>()
1259 else {
1260 return PolicyMatch::NoOpinion;
1261 };
1262
1263 let raw_paths: Vec<&Path> = match fs {
1264 FileSystemPermissionRequest::Move { from, to, .. } => {
1265 vec![from.as_path(), to.as_path()]
1266 }
1267 FileSystemPermissionRequest::Read { path, .. }
1268 | FileSystemPermissionRequest::Write { path, .. }
1269 | FileSystemPermissionRequest::Edit { path, .. }
1270 | FileSystemPermissionRequest::Delete { path, .. }
1271 | FileSystemPermissionRequest::List { path, .. }
1272 | FileSystemPermissionRequest::CreateDir { path, .. } => vec![path.as_path()],
1273 };
1274
1275 let candidate_paths: Vec<PathBuf> = raw_paths
1276 .iter()
1277 .map(|p| std::path::absolute(p).unwrap_or_else(|_| p.to_path_buf()))
1278 .collect();
1279
1280 let mutates = matches!(
1281 fs,
1282 FileSystemPermissionRequest::Write { .. }
1283 | FileSystemPermissionRequest::Edit { .. }
1284 | FileSystemPermissionRequest::Delete { .. }
1285 | FileSystemPermissionRequest::Move { .. }
1286 | FileSystemPermissionRequest::CreateDir { .. }
1287 );
1288
1289 if candidate_paths.iter().any(|path| {
1290 self.protected_roots
1291 .iter()
1292 .any(|root| path.starts_with(root))
1293 }) {
1294 return PolicyMatch::Deny(PermissionDenial {
1295 code: PermissionCode::PathNotAllowed,
1296 message: format!("path access denied for {}", fs.summary()),
1297 metadata: fs.metadata().clone(),
1298 });
1299 }
1300
1301 if mutates
1302 && candidate_paths.iter().any(|path| {
1303 self.read_only_roots
1304 .iter()
1305 .any(|root| path.starts_with(root))
1306 })
1307 {
1308 return PolicyMatch::Deny(PermissionDenial {
1309 code: PermissionCode::PathNotAllowed,
1310 message: format!("path is read-only for {}", fs.summary()),
1311 metadata: fs.metadata().clone(),
1312 });
1313 }
1314
1315 if self.allowed_roots.is_empty() {
1316 return PolicyMatch::NoOpinion;
1317 }
1318
1319 let all_allowed = candidate_paths
1320 .iter()
1321 .all(|path| self.allowed_roots.iter().any(|root| path.starts_with(root)));
1322
1323 if all_allowed {
1324 PolicyMatch::Allow
1325 } else if self.require_approval_outside_allowed {
1326 PolicyMatch::RequireApproval(ApprovalRequest {
1327 task_id: None,
1328 call_id: None,
1329 id: ApprovalId::new(format!("approval:{}", fs.kind())),
1330 request_kind: fs.kind().to_string(),
1331 reason: ApprovalReason::SensitivePath,
1332 summary: fs.summary(),
1333 metadata: fs.metadata().clone(),
1334 })
1335 } else {
1336 PolicyMatch::Deny(PermissionDenial {
1337 code: PermissionCode::PathNotAllowed,
1338 message: format!("path outside allowed roots for {}", fs.summary()),
1339 metadata: fs.metadata().clone(),
1340 })
1341 }
1342 }
1343}
1344
1345pub struct CommandPolicy {
1366 allowed_executables: BTreeSet<String>,
1367 denied_executables: BTreeSet<String>,
1368 allowed_cwds: Vec<PathBuf>,
1369 denied_env_keys: BTreeSet<String>,
1370 require_approval_for_unknown: bool,
1371}
1372
1373impl CommandPolicy {
1374 pub fn new() -> Self {
1377 Self {
1378 allowed_executables: BTreeSet::new(),
1379 denied_executables: BTreeSet::new(),
1380 allowed_cwds: Vec::new(),
1381 denied_env_keys: BTreeSet::new(),
1382 require_approval_for_unknown: true,
1383 }
1384 }
1385
1386 pub fn allow_executable(mut self, executable: impl Into<String>) -> Self {
1388 self.allowed_executables.insert(executable.into());
1389 self
1390 }
1391
1392 pub fn deny_executable(mut self, executable: impl Into<String>) -> Self {
1394 self.denied_executables.insert(executable.into());
1395 self
1396 }
1397
1398 pub fn allow_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
1400 self.allowed_cwds.push(cwd.into());
1401 self
1402 }
1403
1404 pub fn deny_env_key(mut self, key: impl Into<String>) -> Self {
1406 self.denied_env_keys.insert(key.into());
1407 self
1408 }
1409
1410 pub fn require_approval_for_unknown(mut self, value: bool) -> Self {
1413 self.require_approval_for_unknown = value;
1414 self
1415 }
1416}
1417
1418impl Default for CommandPolicy {
1419 fn default() -> Self {
1420 Self::new()
1421 }
1422}
1423
1424impl PermissionPolicy for CommandPolicy {
1425 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1426 let Some(shell) = request.as_any().downcast_ref::<ShellPermissionRequest>() else {
1427 return PolicyMatch::NoOpinion;
1428 };
1429
1430 if self.denied_executables.contains(&shell.executable)
1431 || shell
1432 .env_keys
1433 .iter()
1434 .any(|key| self.denied_env_keys.contains(key))
1435 {
1436 return PolicyMatch::Deny(PermissionDenial {
1437 code: PermissionCode::CommandNotAllowed,
1438 message: format!("command denied for {}", shell.summary()),
1439 metadata: shell.metadata().clone(),
1440 });
1441 }
1442
1443 if let Some(cwd) = &shell.cwd
1444 && !self.allowed_cwds.is_empty()
1445 && !self.allowed_cwds.iter().any(|root| cwd.starts_with(root))
1446 {
1447 return PolicyMatch::RequireApproval(ApprovalRequest {
1448 task_id: None,
1449 call_id: None,
1450 id: ApprovalId::new("approval:shell.cwd"),
1451 request_kind: shell.kind().to_string(),
1452 reason: ApprovalReason::SensitiveCommand,
1453 summary: shell.summary(),
1454 metadata: shell.metadata().clone(),
1455 });
1456 }
1457
1458 if self.allowed_executables.is_empty()
1459 || self.allowed_executables.contains(&shell.executable)
1460 {
1461 PolicyMatch::Allow
1462 } else if self.require_approval_for_unknown {
1463 PolicyMatch::RequireApproval(ApprovalRequest {
1464 task_id: None,
1465 call_id: None,
1466 id: ApprovalId::new("approval:shell.command"),
1467 request_kind: shell.kind().to_string(),
1468 reason: ApprovalReason::SensitiveCommand,
1469 summary: shell.summary(),
1470 metadata: shell.metadata().clone(),
1471 })
1472 } else {
1473 PolicyMatch::Deny(PermissionDenial {
1474 code: PermissionCode::CommandNotAllowed,
1475 message: format!("executable {} is not allowed", shell.executable),
1476 metadata: shell.metadata().clone(),
1477 })
1478 }
1479 }
1480}
1481
1482pub struct McpServerPolicy {
1496 trusted_servers: BTreeSet<String>,
1497 allowed_auth_scopes: BTreeSet<String>,
1498 require_approval_for_untrusted: bool,
1499}
1500
1501impl McpServerPolicy {
1502 pub fn new() -> Self {
1505 Self {
1506 trusted_servers: BTreeSet::new(),
1507 allowed_auth_scopes: BTreeSet::new(),
1508 require_approval_for_untrusted: true,
1509 }
1510 }
1511
1512 pub fn trust_server(mut self, server_id: impl Into<String>) -> Self {
1514 self.trusted_servers.insert(server_id.into());
1515 self
1516 }
1517
1518 pub fn allow_auth_scope(mut self, scope: impl Into<String>) -> Self {
1520 self.allowed_auth_scopes.insert(scope.into());
1521 self
1522 }
1523}
1524
1525impl Default for McpServerPolicy {
1526 fn default() -> Self {
1527 Self::new()
1528 }
1529}
1530
1531impl PermissionPolicy for McpServerPolicy {
1532 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1533 let Some(mcp) = request.as_any().downcast_ref::<McpPermissionRequest>() else {
1534 return PolicyMatch::NoOpinion;
1535 };
1536
1537 let server_id = match mcp {
1538 McpPermissionRequest::Connect { server_id, .. }
1539 | McpPermissionRequest::InvokeTool { server_id, .. }
1540 | McpPermissionRequest::ReadResource { server_id, .. }
1541 | McpPermissionRequest::FetchPrompt { server_id, .. }
1542 | McpPermissionRequest::UseAuthScope { server_id, .. } => server_id,
1543 };
1544
1545 if !self.trusted_servers.is_empty() && !self.trusted_servers.contains(server_id) {
1546 return if self.require_approval_for_untrusted {
1547 PolicyMatch::RequireApproval(ApprovalRequest {
1548 task_id: None,
1549 call_id: None,
1550 id: ApprovalId::new(format!("approval:mcp:{server_id}")),
1551 request_kind: mcp.kind().to_string(),
1552 reason: ApprovalReason::SensitiveServer,
1553 summary: mcp.summary(),
1554 metadata: mcp.metadata().clone(),
1555 })
1556 } else {
1557 PolicyMatch::Deny(PermissionDenial {
1558 code: PermissionCode::ServerNotTrusted,
1559 message: format!("MCP server {server_id} is not trusted"),
1560 metadata: mcp.metadata().clone(),
1561 })
1562 };
1563 }
1564
1565 if let McpPermissionRequest::UseAuthScope { scope, .. } = mcp
1566 && !self.allowed_auth_scopes.is_empty()
1567 && !self.allowed_auth_scopes.contains(scope)
1568 {
1569 return PolicyMatch::Deny(PermissionDenial {
1570 code: PermissionCode::AuthScopeNotAllowed,
1571 message: format!("MCP auth scope {scope} is not allowed"),
1572 metadata: mcp.metadata().clone(),
1573 });
1574 }
1575
1576 PolicyMatch::Allow
1577 }
1578}
1579
1580#[async_trait]
1632pub trait Tool: Send + Sync {
1633 fn spec(&self) -> &ToolSpec;
1635
1636 fn current_spec(&self) -> Option<ToolSpec> {
1644 Some(self.spec().clone())
1645 }
1646
1647 fn proposed_requests(
1659 &self,
1660 _request: &ToolRequest,
1661 ) -> Result<Vec<Box<dyn PermissionRequest>>, ToolError> {
1662 Ok(Vec::new())
1663 }
1664
1665 async fn invoke(
1674 &self,
1675 request: ToolRequest,
1676 ctx: &mut ToolContext<'_>,
1677 ) -> Result<ToolResult, ToolError>;
1678}
1679
1680#[derive(Clone, Default)]
1711pub struct ToolRegistry {
1712 tools: BTreeMap<ToolName, Arc<dyn Tool>>,
1713}
1714
1715impl ToolRegistry {
1716 pub fn new() -> Self {
1718 Self::default()
1719 }
1720
1721 pub fn register<T>(&mut self, tool: T) -> &mut Self
1723 where
1724 T: Tool + 'static,
1725 {
1726 self.tools.insert(tool.spec().name.clone(), Arc::new(tool));
1727 self
1728 }
1729
1730 pub fn with<T>(mut self, tool: T) -> Self
1732 where
1733 T: Tool + 'static,
1734 {
1735 self.register(tool);
1736 self
1737 }
1738
1739 pub fn register_arc(&mut self, tool: Arc<dyn Tool>) -> &mut Self {
1741 self.tools.insert(tool.spec().name.clone(), tool);
1742 self
1743 }
1744
1745 pub fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1747 self.tools.get(name).cloned()
1748 }
1749
1750 pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
1752 self.tools.values().cloned().collect()
1753 }
1754
1755 pub fn merge(mut self, other: Self) -> Self {
1764 self.tools.extend(other.tools);
1765 self
1766 }
1767
1768 pub fn specs(&self) -> Vec<ToolSpec> {
1770 self.tools
1771 .values()
1772 .filter_map(|tool| tool.current_spec())
1773 .collect()
1774 }
1775}
1776
1777impl ToolSpec {
1778 pub fn as_invocable_spec(&self) -> InvocableSpec {
1781 InvocableSpec::new(
1782 CapabilityName::new(self.name.0.clone()),
1783 self.description.clone(),
1784 self.input_schema.clone(),
1785 )
1786 .with_metadata(self.metadata.clone())
1787 }
1788}
1789
1790pub struct ToolInvocableAdapter {
1796 spec: InvocableSpec,
1797 tool: Arc<dyn Tool>,
1798 permissions: Arc<dyn PermissionChecker>,
1799 resources: Arc<dyn ToolResources>,
1800 next_call_id: AtomicU64,
1801}
1802
1803impl ToolInvocableAdapter {
1804 pub fn new(
1807 tool: Arc<dyn Tool>,
1808 permissions: Arc<dyn PermissionChecker>,
1809 resources: Arc<dyn ToolResources>,
1810 ) -> Option<Self> {
1811 let spec = tool.current_spec()?.as_invocable_spec();
1812 Some(Self {
1813 spec,
1814 tool,
1815 permissions,
1816 resources,
1817 next_call_id: AtomicU64::new(1),
1818 })
1819 }
1820}
1821
1822#[async_trait]
1823impl Invocable for ToolInvocableAdapter {
1824 fn spec(&self) -> &InvocableSpec {
1825 &self.spec
1826 }
1827
1828 async fn invoke(
1829 &self,
1830 request: InvocableRequest,
1831 ctx: &mut CapabilityContext<'_>,
1832 ) -> Result<InvocableResult, CapabilityError> {
1833 let tool_request = ToolRequest {
1834 call_id: ToolCallId::new(format!(
1835 "tool-call-{}",
1836 self.next_call_id.fetch_add(1, Ordering::Relaxed)
1837 )),
1838 tool_name: self.tool.spec().name.clone(),
1839 input: request.input,
1840 session_id: ctx
1841 .session_id
1842 .cloned()
1843 .unwrap_or_else(|| SessionId::new("capability-session")),
1844 turn_id: ctx
1845 .turn_id
1846 .cloned()
1847 .unwrap_or_else(|| TurnId::new("capability-turn")),
1848 metadata: request.metadata,
1849 };
1850
1851 for permission_request in self
1852 .tool
1853 .proposed_requests(&tool_request)
1854 .map_err(|error| CapabilityError::InvalidInput(error.to_string()))?
1855 {
1856 match self.permissions.evaluate(permission_request.as_ref()) {
1857 PermissionDecision::Allow => {}
1858 PermissionDecision::Deny(denial) => {
1859 return Err(CapabilityError::ExecutionFailed(format!(
1860 "tool permission denied: {denial:?}"
1861 )));
1862 }
1863 PermissionDecision::RequireApproval(req) => {
1864 return Err(CapabilityError::Unavailable(format!(
1865 "tool invocation requires approval: {}",
1866 req.summary
1867 )));
1868 }
1869 }
1870 }
1871
1872 let mut tool_ctx = ToolContext {
1873 capability: CapabilityContext {
1874 session_id: ctx.session_id,
1875 turn_id: ctx.turn_id,
1876 metadata: ctx.metadata,
1877 },
1878 permissions: self.permissions.as_ref(),
1879 resources: self.resources.as_ref(),
1880 cancellation: None,
1881 };
1882
1883 let result = self
1884 .tool
1885 .invoke(tool_request, &mut tool_ctx)
1886 .await
1887 .map_err(|error| CapabilityError::ExecutionFailed(error.to_string()))?;
1888
1889 Ok(InvocableResult {
1890 output: match result.result.output {
1891 ToolOutput::Text(text) => InvocableOutput::Text(text),
1892 ToolOutput::Structured(value) => InvocableOutput::Structured(value),
1893 ToolOutput::Parts(parts) => InvocableOutput::Items(vec![Item {
1894 id: None,
1895 kind: ItemKind::Tool,
1896 parts,
1897 metadata: MetadataMap::new(),
1898 }]),
1899 ToolOutput::Files(files) => {
1900 let parts = files.into_iter().map(Part::File).collect();
1901 InvocableOutput::Items(vec![Item {
1902 id: None,
1903 kind: ItemKind::Tool,
1904 parts,
1905 metadata: MetadataMap::new(),
1906 }])
1907 }
1908 },
1909 metadata: result.metadata,
1910 })
1911 }
1912}
1913
1914pub struct ToolCapabilityProvider {
1920 invocables: Vec<Arc<dyn Invocable>>,
1921}
1922
1923impl ToolCapabilityProvider {
1924 pub fn from_registry(
1927 registry: &ToolRegistry,
1928 permissions: Arc<dyn PermissionChecker>,
1929 resources: Arc<dyn ToolResources>,
1930 ) -> Self {
1931 let invocables = registry
1932 .tools()
1933 .into_iter()
1934 .filter_map(|tool| {
1935 ToolInvocableAdapter::new(tool, permissions.clone(), resources.clone())
1936 .map(|adapter| Arc::new(adapter) as Arc<dyn Invocable>)
1937 })
1938 .collect();
1939
1940 Self { invocables }
1941 }
1942}
1943
1944impl CapabilityProvider for ToolCapabilityProvider {
1945 fn invocables(&self) -> Vec<Arc<dyn Invocable>> {
1946 self.invocables.clone()
1947 }
1948
1949 fn resources(&self) -> Vec<Arc<dyn ResourceProvider>> {
1950 Vec::new()
1951 }
1952
1953 fn prompts(&self) -> Vec<Arc<dyn PromptProvider>> {
1954 Vec::new()
1955 }
1956}
1957
1958#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1964pub enum ToolExecutionOutcome {
1965 Completed(ToolResult),
1967 Interrupted(ToolInterruption),
1969 Failed(ToolError),
1971}
1972
1973#[async_trait]
1981pub trait ToolExecutor: Send + Sync {
1982 fn specs(&self) -> Vec<ToolSpec>;
1984
1985 async fn execute(
1987 &self,
1988 request: ToolRequest,
1989 ctx: &mut ToolContext<'_>,
1990 ) -> ToolExecutionOutcome;
1991
1992 async fn execute_owned(
1995 &self,
1996 request: ToolRequest,
1997 ctx: OwnedToolContext,
1998 ) -> ToolExecutionOutcome {
1999 let mut borrowed = ctx.borrowed();
2000 self.execute(request, &mut borrowed).await
2001 }
2002
2003 async fn execute_approved(
2009 &self,
2010 request: ToolRequest,
2011 approved_request: &ApprovalRequest,
2012 ctx: &mut ToolContext<'_>,
2013 ) -> ToolExecutionOutcome {
2014 let _ = approved_request;
2015 self.execute(request, ctx).await
2016 }
2017
2018 async fn execute_approved_owned(
2021 &self,
2022 request: ToolRequest,
2023 approved_request: &ApprovalRequest,
2024 ctx: OwnedToolContext,
2025 ) -> ToolExecutionOutcome {
2026 let mut borrowed = ctx.borrowed();
2027 self.execute_approved(request, approved_request, &mut borrowed)
2028 .await
2029 }
2030}
2031
2032pub struct BasicToolExecutor {
2045 registry: ToolRegistry,
2046}
2047
2048impl BasicToolExecutor {
2049 pub fn new(registry: ToolRegistry) -> Self {
2051 Self { registry }
2052 }
2053
2054 pub fn specs(&self) -> Vec<ToolSpec> {
2056 self.registry.specs()
2057 }
2058
2059 async fn execute_inner(
2060 &self,
2061 request: ToolRequest,
2062 approved_request_id: Option<&ApprovalId>,
2063 ctx: &mut ToolContext<'_>,
2064 ) -> ToolExecutionOutcome {
2065 let Some(tool) = self.registry.get(&request.tool_name) else {
2066 return ToolExecutionOutcome::Failed(ToolError::NotFound(request.tool_name));
2067 };
2068
2069 match tool.proposed_requests(&request) {
2070 Ok(requests) => {
2071 for permission_request in requests {
2072 match ctx.permissions.evaluate(permission_request.as_ref()) {
2073 PermissionDecision::Allow => {}
2074 PermissionDecision::Deny(denial) => {
2075 return ToolExecutionOutcome::Failed(ToolError::PermissionDenied(
2076 denial,
2077 ));
2078 }
2079 PermissionDecision::RequireApproval(mut req) => {
2080 req.call_id = Some(request.call_id.clone());
2081 if approved_request_id != Some(&req.id) {
2082 return ToolExecutionOutcome::Interrupted(
2083 ToolInterruption::ApprovalRequired(req),
2084 );
2085 }
2086 }
2087 }
2088 }
2089 }
2090 Err(error) => return ToolExecutionOutcome::Failed(error),
2091 }
2092
2093 match tool.invoke(request, ctx).await {
2094 Ok(result) => ToolExecutionOutcome::Completed(result),
2095 Err(ToolError::AuthRequired(request)) => {
2096 ToolExecutionOutcome::Interrupted(ToolInterruption::AuthRequired(*request))
2097 }
2098 Err(error) => ToolExecutionOutcome::Failed(error),
2099 }
2100 }
2101}
2102
2103#[async_trait]
2104impl ToolExecutor for BasicToolExecutor {
2105 fn specs(&self) -> Vec<ToolSpec> {
2106 self.registry.specs()
2107 }
2108
2109 async fn execute(
2110 &self,
2111 request: ToolRequest,
2112 ctx: &mut ToolContext<'_>,
2113 ) -> ToolExecutionOutcome {
2114 self.execute_inner(request, None, ctx).await
2115 }
2116
2117 async fn execute_approved(
2118 &self,
2119 request: ToolRequest,
2120 approved_request: &ApprovalRequest,
2121 ctx: &mut ToolContext<'_>,
2122 ) -> ToolExecutionOutcome {
2123 self.execute_inner(request, Some(&approved_request.id), ctx)
2124 .await
2125 }
2126}
2127
2128#[derive(Debug, Error, Clone, PartialEq, Serialize, Deserialize)]
2133pub enum ToolError {
2134 #[error("tool not found: {0}")]
2136 NotFound(ToolName),
2137 #[error("invalid tool input: {0}")]
2139 InvalidInput(String),
2140 #[error("tool permission denied: {0:?}")]
2142 PermissionDenied(PermissionDenial),
2143 #[error("tool execution failed: {0}")]
2145 ExecutionFailed(String),
2146 #[error("tool auth required: {0:?}")]
2150 AuthRequired(Box<AuthRequest>),
2151 #[error("tool unavailable: {0}")]
2153 Unavailable(String),
2154 #[error("tool execution cancelled")]
2156 Cancelled,
2157 #[error("internal tool error: {0}")]
2159 Internal(String),
2160}
2161
2162impl ToolError {
2163 pub fn permission_denied(denial: PermissionDenial) -> Self {
2165 Self::PermissionDenied(denial)
2166 }
2167}
2168
2169impl From<PermissionDenial> for ToolError {
2170 fn from(value: PermissionDenial) -> Self {
2171 Self::permission_denied(value)
2172 }
2173}
2174
2175#[cfg(test)]
2176mod tests {
2177 use super::*;
2178 use async_trait::async_trait;
2179 use serde_json::json;
2180
2181 #[test]
2182 fn command_policy_can_deny_unknown_executables_without_approval() {
2183 let policy = CommandPolicy::new()
2184 .allow_executable("pwd")
2185 .require_approval_for_unknown(false);
2186 let request = ShellPermissionRequest {
2187 executable: "rm".into(),
2188 argv: vec!["-rf".into(), "/tmp/demo".into()],
2189 cwd: None,
2190 env_keys: Vec::new(),
2191 metadata: MetadataMap::new(),
2192 };
2193
2194 match policy.evaluate(&request) {
2195 PolicyMatch::Deny(denial) => {
2196 assert_eq!(denial.code, PermissionCode::CommandNotAllowed);
2197 }
2198 other => panic!("unexpected policy match: {other:?}"),
2199 }
2200 }
2201
2202 #[test]
2203 fn path_policy_allows_reads_under_read_only_roots() {
2204 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2205 let request = FileSystemPermissionRequest::Read {
2206 path: PathBuf::from("/workspace/vendor/lib.rs"),
2207 metadata: MetadataMap::new(),
2208 };
2209
2210 match policy.evaluate(&request) {
2211 PolicyMatch::NoOpinion | PolicyMatch::Allow => {}
2212 other => panic!("unexpected policy match: {other:?}"),
2213 }
2214 }
2215
2216 #[test]
2217 fn path_policy_denies_mutations_under_read_only_roots() {
2218 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2219 let request = FileSystemPermissionRequest::Edit {
2220 path: PathBuf::from("/workspace/vendor/lib.rs"),
2221 metadata: MetadataMap::new(),
2222 };
2223
2224 match policy.evaluate(&request) {
2225 PolicyMatch::Deny(denial) => {
2226 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
2227 assert!(denial.message.contains("read-only"));
2228 }
2229 other => panic!("unexpected policy match: {other:?}"),
2230 }
2231 }
2232
2233 #[test]
2234 fn path_policy_denies_moves_into_read_only_roots() {
2235 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2236 let request = FileSystemPermissionRequest::Move {
2237 from: PathBuf::from("/workspace/src/lib.rs"),
2238 to: PathBuf::from("/workspace/vendor/lib.rs"),
2239 metadata: MetadataMap::new(),
2240 };
2241
2242 match policy.evaluate(&request) {
2243 PolicyMatch::Deny(denial) => {
2244 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
2245 assert!(denial.message.contains("read-only"));
2246 }
2247 other => panic!("unexpected policy match: {other:?}"),
2248 }
2249 }
2250
2251 #[derive(Clone)]
2252 struct HiddenTool {
2253 spec: ToolSpec,
2254 }
2255
2256 impl HiddenTool {
2257 fn new() -> Self {
2258 Self {
2259 spec: ToolSpec {
2260 name: ToolName::new("hidden"),
2261 description: "hidden".into(),
2262 input_schema: json!({"type": "object"}),
2263 annotations: ToolAnnotations::default(),
2264 metadata: MetadataMap::new(),
2265 },
2266 }
2267 }
2268 }
2269
2270 #[async_trait]
2271 impl Tool for HiddenTool {
2272 fn spec(&self) -> &ToolSpec {
2273 &self.spec
2274 }
2275
2276 fn current_spec(&self) -> Option<ToolSpec> {
2277 None
2278 }
2279
2280 async fn invoke(
2281 &self,
2282 request: ToolRequest,
2283 _ctx: &mut ToolContext<'_>,
2284 ) -> Result<ToolResult, ToolError> {
2285 Ok(ToolResult {
2286 result: ToolResultPart {
2287 call_id: request.call_id,
2288 output: ToolOutput::Text("hidden".into()),
2289 is_error: false,
2290 metadata: MetadataMap::new(),
2291 },
2292 duration: None,
2293 metadata: MetadataMap::new(),
2294 })
2295 }
2296 }
2297
2298 #[test]
2299 fn hidden_tools_are_omitted_from_specs_and_capabilities() {
2300 let registry = ToolRegistry::new().with(HiddenTool::new());
2301
2302 assert!(registry.specs().is_empty());
2303
2304 let provider = ToolCapabilityProvider::from_registry(
2305 ®istry,
2306 Arc::new(AllowAllPermissionChecker),
2307 Arc::new(()),
2308 );
2309 assert!(provider.invocables().is_empty());
2310 }
2311
2312 struct AllowAllPermissionChecker;
2313
2314 impl PermissionChecker for AllowAllPermissionChecker {
2315 fn evaluate(&self, _request: &dyn PermissionRequest) -> PermissionDecision {
2316 PermissionDecision::Allow
2317 }
2318 }
2319}