1use std::any::Any;
22use std::collections::{BTreeMap, BTreeSet};
23use std::fmt;
24use std::path::{Path, PathBuf};
25use std::sync::{Arc, OnceLock};
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#[doc(hidden)]
47pub mod __private_async_trait {
48 pub use async_trait::async_trait;
49}
50
51#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
69pub struct ToolName(pub String);
70
71impl ToolName {
72 pub fn new(value: impl Into<String>) -> Self {
74 Self(value.into())
75 }
76}
77
78impl fmt::Display for ToolName {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 self.0.fmt(f)
81 }
82}
83
84impl From<&str> for ToolName {
85 fn from(value: &str) -> Self {
86 Self::new(value)
87 }
88}
89
90#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
97pub struct ToolAnnotations {
98 pub read_only_hint: bool,
100 pub destructive_hint: bool,
102 pub idempotent_hint: bool,
104 pub needs_approval_hint: bool,
106 pub supports_streaming_hint: bool,
108}
109
110impl ToolAnnotations {
111 pub fn new() -> Self {
113 Self::default()
114 }
115
116 pub fn read_only() -> Self {
118 Self::default().with_read_only(true)
119 }
120
121 pub fn destructive() -> Self {
123 Self::default().with_destructive(true)
124 }
125
126 pub fn needs_approval() -> Self {
128 Self::default().with_needs_approval(true)
129 }
130
131 pub fn streaming() -> Self {
133 Self::default().with_supports_streaming(true)
134 }
135
136 pub fn with_read_only(mut self, read_only_hint: bool) -> Self {
137 self.read_only_hint = read_only_hint;
138 self
139 }
140
141 pub fn with_destructive(mut self, destructive_hint: bool) -> Self {
142 self.destructive_hint = destructive_hint;
143 self
144 }
145
146 pub fn with_idempotent(mut self, idempotent_hint: bool) -> Self {
147 self.idempotent_hint = idempotent_hint;
148 self
149 }
150
151 pub fn with_needs_approval(mut self, needs_approval_hint: bool) -> Self {
152 self.needs_approval_hint = needs_approval_hint;
153 self
154 }
155
156 pub fn with_supports_streaming(mut self, supports_streaming_hint: bool) -> Self {
157 self.supports_streaming_hint = supports_streaming_hint;
158 self
159 }
160}
161
162#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
188pub struct ToolSpec {
189 pub name: ToolName,
191 pub description: String,
193 pub input_schema: Value,
195 pub annotations: ToolAnnotations,
197 pub metadata: MetadataMap,
199}
200
201#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
207pub struct ToolCatalogEvent {
208 pub source: String,
210 pub added: Vec<String>,
212 pub removed: Vec<String>,
214 pub changed: Vec<String>,
216}
217
218impl ToolCatalogEvent {
219 pub fn new(source: impl Into<String>) -> Self {
221 Self {
222 source: source.into(),
223 added: Vec::new(),
224 removed: Vec::new(),
225 changed: Vec::new(),
226 }
227 }
228
229 pub fn for_each_name_mut(&mut self, mut f: impl FnMut(&mut String)) {
231 for vec in [&mut self.added, &mut self.removed, &mut self.changed] {
232 for name in vec.iter_mut() {
233 f(name);
234 }
235 }
236 }
237
238 pub fn retain_names(&mut self, mut predicate: impl FnMut(&str) -> bool) {
241 self.added.retain(|n| predicate(n));
242 self.removed.retain(|n| predicate(n));
243 self.changed.retain(|n| predicate(n));
244 }
245}
246
247impl ToolSpec {
248 pub fn new(
250 name: impl Into<ToolName>,
251 description: impl Into<String>,
252 input_schema: Value,
253 ) -> Self {
254 Self {
255 name: name.into(),
256 description: description.into(),
257 input_schema,
258 annotations: ToolAnnotations::default(),
259 metadata: MetadataMap::new(),
260 }
261 }
262
263 pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
265 self.annotations = annotations;
266 self
267 }
268
269 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
271 self.metadata = metadata;
272 self
273 }
274}
275
276#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
282pub struct ToolRequest {
283 pub call_id: ToolCallId,
285 pub tool_name: ToolName,
287 pub input: Value,
289 pub session_id: SessionId,
291 pub turn_id: TurnId,
293 pub metadata: MetadataMap,
295}
296
297impl ToolRequest {
298 pub fn new(
300 call_id: impl Into<ToolCallId>,
301 tool_name: impl Into<ToolName>,
302 input: Value,
303 session_id: impl Into<SessionId>,
304 turn_id: impl Into<TurnId>,
305 ) -> Self {
306 Self {
307 call_id: call_id.into(),
308 tool_name: tool_name.into(),
309 input,
310 session_id: session_id.into(),
311 turn_id: turn_id.into(),
312 metadata: MetadataMap::new(),
313 }
314 }
315
316 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
318 self.metadata = metadata;
319 self
320 }
321}
322
323#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
328pub struct ToolResult {
329 pub result: ToolResultPart,
331 pub duration: Option<Duration>,
333 pub metadata: MetadataMap,
335}
336
337impl ToolResult {
338 pub fn new(result: ToolResultPart) -> Self {
340 Self {
341 result,
342 duration: None,
343 metadata: MetadataMap::new(),
344 }
345 }
346
347 pub fn with_duration(mut self, duration: Duration) -> Self {
349 self.duration = Some(duration);
350 self
351 }
352
353 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
355 self.metadata = metadata;
356 self
357 }
358}
359
360pub trait ToolResources: Send + Sync {
386 fn as_any(&self) -> &dyn Any;
389}
390
391impl ToolResources for () {
392 fn as_any(&self) -> &dyn Any {
393 self
394 }
395}
396
397pub struct ToolContext<'a> {
403 pub capability: CapabilityContext<'a>,
405 pub permissions: &'a dyn PermissionChecker,
407 pub resources: &'a dyn ToolResources,
409 pub cancellation: Option<TurnCancellation>,
411}
412
413#[derive(Clone)]
419pub struct OwnedToolContext {
420 pub session_id: SessionId,
422 pub turn_id: TurnId,
424 pub metadata: MetadataMap,
426 pub permissions: Arc<dyn PermissionChecker>,
428 pub resources: Arc<dyn ToolResources>,
430 pub cancellation: Option<TurnCancellation>,
432}
433
434impl OwnedToolContext {
435 pub fn borrowed(&self) -> ToolContext<'_> {
437 ToolContext {
438 capability: CapabilityContext {
439 session_id: Some(&self.session_id),
440 turn_id: Some(&self.turn_id),
441 metadata: &self.metadata,
442 },
443 permissions: self.permissions.as_ref(),
444 resources: self.resources.as_ref(),
445 cancellation: self.cancellation.clone(),
446 }
447 }
448}
449
450pub trait PermissionRequest: Send + Sync {
479 fn kind(&self) -> &'static str;
481 fn summary(&self) -> String;
483 fn metadata(&self) -> &MetadataMap;
485 fn as_any(&self) -> &dyn Any;
487}
488
489#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
494pub enum PermissionCode {
495 PathNotAllowed,
497 CommandNotAllowed,
499 NetworkNotAllowed,
501 ServerNotTrusted,
503 AuthScopeNotAllowed,
505 CustomPolicyDenied,
507 UnknownRequest,
509}
510
511#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
516pub struct PermissionDenial {
517 pub code: PermissionCode,
519 pub message: String,
521 pub metadata: MetadataMap,
523}
524
525#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
530pub enum ApprovalReason {
531 PolicyRequiresConfirmation,
533 EscalatedRisk,
535 UnknownTarget,
537 SensitivePath,
539 SensitiveCommand,
541 SensitiveServer,
543 SensitiveAuthScope,
545}
546
547#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
552pub struct ApprovalRequest {
553 pub task_id: Option<TaskId>,
555 pub call_id: Option<ToolCallId>,
558 pub id: ApprovalId,
560 pub request_kind: String,
562 pub reason: ApprovalReason,
564 pub summary: String,
566 pub metadata: MetadataMap,
568}
569
570impl ApprovalRequest {
571 pub fn new(
573 id: impl Into<ApprovalId>,
574 request_kind: impl Into<String>,
575 reason: ApprovalReason,
576 summary: impl Into<String>,
577 ) -> Self {
578 Self {
579 task_id: None,
580 call_id: None,
581 id: id.into(),
582 request_kind: request_kind.into(),
583 reason,
584 summary: summary.into(),
585 metadata: MetadataMap::new(),
586 }
587 }
588
589 pub fn with_task_id(mut self, task_id: impl Into<TaskId>) -> Self {
591 self.task_id = Some(task_id.into());
592 self
593 }
594
595 pub fn with_call_id(mut self, call_id: impl Into<ToolCallId>) -> Self {
597 self.call_id = Some(call_id.into());
598 self
599 }
600
601 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
603 self.metadata = metadata;
604 self
605 }
606}
607
608#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
610pub enum ApprovalDecision {
611 Approve,
613 Deny {
615 reason: Option<String>,
617 },
618}
619
620#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
627pub enum ToolInterruption {
628 ApprovalRequired(ApprovalRequest),
630}
631
632#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
634pub enum PermissionDecision {
635 Allow,
637 Deny(PermissionDenial),
639 RequireApproval(ApprovalRequest),
641}
642
643pub trait PermissionChecker: Send + Sync {
667 fn evaluate(&self, request: &dyn PermissionRequest) -> PermissionDecision;
669}
670
671#[derive(Copy, Clone, Debug, Default)]
676pub struct AllowAllPermissions;
677
678impl PermissionChecker for AllowAllPermissions {
679 fn evaluate(&self, _request: &dyn PermissionRequest) -> PermissionDecision {
680 PermissionDecision::Allow
681 }
682}
683
684#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
690pub enum PolicyMatch {
691 NoOpinion,
693 Allow,
695 Deny(PermissionDenial),
697 RequireApproval(ApprovalRequest),
699}
700
701pub trait PermissionPolicy: Send + Sync {
710 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch;
712}
713
714pub struct CompositePermissionChecker {
734 policies: Vec<Box<dyn PermissionPolicy>>,
735 fallback: PermissionDecision,
736}
737
738impl CompositePermissionChecker {
739 pub fn new(fallback: PermissionDecision) -> Self {
747 Self {
748 policies: Vec::new(),
749 fallback,
750 }
751 }
752
753 pub fn with_policy(mut self, policy: impl PermissionPolicy + 'static) -> Self {
755 self.policies.push(Box::new(policy));
756 self
757 }
758}
759
760impl PermissionChecker for CompositePermissionChecker {
761 fn evaluate(&self, request: &dyn PermissionRequest) -> PermissionDecision {
762 let mut saw_allow = false;
763 let mut approval = None;
764
765 for policy in &self.policies {
766 match policy.evaluate(request) {
767 PolicyMatch::NoOpinion => {}
768 PolicyMatch::Allow => saw_allow = true,
769 PolicyMatch::Deny(denial) => return PermissionDecision::Deny(denial),
770 PolicyMatch::RequireApproval(req) => approval = Some(req),
771 }
772 }
773
774 if let Some(req) = approval {
775 PermissionDecision::RequireApproval(req)
776 } else if saw_allow {
777 PermissionDecision::Allow
778 } else {
779 self.fallback.clone()
780 }
781 }
782}
783
784#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
789pub struct ShellPermissionRequest {
790 pub executable: String,
792 pub argv: Vec<String>,
794 pub cwd: Option<PathBuf>,
796 pub env_keys: Vec<String>,
798 pub metadata: MetadataMap,
800}
801
802impl PermissionRequest for ShellPermissionRequest {
803 fn kind(&self) -> &'static str {
804 "shell.command"
805 }
806
807 fn summary(&self) -> String {
808 if self.argv.is_empty() {
809 self.executable.clone()
810 } else {
811 format!("{} {}", self.executable, self.argv.join(" "))
812 }
813 }
814
815 fn metadata(&self) -> &MetadataMap {
816 &self.metadata
817 }
818
819 fn as_any(&self) -> &dyn Any {
820 self
821 }
822}
823
824#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
829pub enum FileSystemPermissionRequest {
830 Read {
832 path: PathBuf,
833 metadata: MetadataMap,
834 },
835 Write {
837 path: PathBuf,
838 metadata: MetadataMap,
839 },
840 Edit {
842 path: PathBuf,
843 metadata: MetadataMap,
844 },
845 Delete {
847 path: PathBuf,
848 metadata: MetadataMap,
849 },
850 Move {
852 from: PathBuf,
853 to: PathBuf,
854 metadata: MetadataMap,
855 },
856 List {
858 path: PathBuf,
859 metadata: MetadataMap,
860 },
861 CreateDir {
863 path: PathBuf,
864 metadata: MetadataMap,
865 },
866}
867
868impl FileSystemPermissionRequest {
869 fn metadata_map(&self) -> &MetadataMap {
870 match self {
871 Self::Read { metadata, .. }
872 | Self::Write { metadata, .. }
873 | Self::Edit { metadata, .. }
874 | Self::Delete { metadata, .. }
875 | Self::Move { metadata, .. }
876 | Self::List { metadata, .. }
877 | Self::CreateDir { metadata, .. } => metadata,
878 }
879 }
880}
881
882impl PermissionRequest for FileSystemPermissionRequest {
883 fn kind(&self) -> &'static str {
884 match self {
885 Self::Read { .. } => "filesystem.read",
886 Self::Write { .. } => "filesystem.write",
887 Self::Edit { .. } => "filesystem.edit",
888 Self::Delete { .. } => "filesystem.delete",
889 Self::Move { .. } => "filesystem.move",
890 Self::List { .. } => "filesystem.list",
891 Self::CreateDir { .. } => "filesystem.mkdir",
892 }
893 }
894
895 fn summary(&self) -> String {
896 match self {
897 Self::Read { path, .. } => format!("Read {}", path.display()),
898 Self::Write { path, .. } => format!("Write {}", path.display()),
899 Self::Edit { path, .. } => format!("Edit {}", path.display()),
900 Self::Delete { path, .. } => format!("Delete {}", path.display()),
901 Self::Move { from, to, .. } => {
902 format!("Move {} to {}", from.display(), to.display())
903 }
904 Self::List { path, .. } => format!("List {}", path.display()),
905 Self::CreateDir { path, .. } => format!("Create directory {}", path.display()),
906 }
907 }
908
909 fn metadata(&self) -> &MetadataMap {
910 self.metadata_map()
911 }
912
913 fn as_any(&self) -> &dyn Any {
914 self
915 }
916}
917
918#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
923pub enum McpPermissionRequest {
924 Connect {
926 server_id: String,
927 metadata: MetadataMap,
928 },
929 InvokeTool {
931 server_id: String,
932 tool_name: String,
933 metadata: MetadataMap,
934 },
935 ReadResource {
937 server_id: String,
938 resource_id: String,
939 metadata: MetadataMap,
940 },
941 FetchPrompt {
943 server_id: String,
944 prompt_id: String,
945 metadata: MetadataMap,
946 },
947 UseAuthScope {
949 server_id: String,
950 scope: String,
951 metadata: MetadataMap,
952 },
953}
954
955impl McpPermissionRequest {
956 fn metadata_map(&self) -> &MetadataMap {
957 match self {
958 Self::Connect { metadata, .. }
959 | Self::InvokeTool { metadata, .. }
960 | Self::ReadResource { metadata, .. }
961 | Self::FetchPrompt { metadata, .. }
962 | Self::UseAuthScope { metadata, .. } => metadata,
963 }
964 }
965}
966
967impl PermissionRequest for McpPermissionRequest {
968 fn kind(&self) -> &'static str {
969 match self {
970 Self::Connect { .. } => "mcp.connect",
971 Self::InvokeTool { .. } => "mcp.invoke_tool",
972 Self::ReadResource { .. } => "mcp.read_resource",
973 Self::FetchPrompt { .. } => "mcp.fetch_prompt",
974 Self::UseAuthScope { .. } => "mcp.use_auth_scope",
975 }
976 }
977
978 fn summary(&self) -> String {
979 match self {
980 Self::Connect { server_id, .. } => format!("Connect MCP server {server_id}"),
981 Self::InvokeTool {
982 server_id,
983 tool_name,
984 ..
985 } => format!("Invoke MCP tool {server_id}.{tool_name}"),
986 Self::ReadResource {
987 server_id,
988 resource_id,
989 ..
990 } => format!("Read MCP resource {server_id}:{resource_id}"),
991 Self::FetchPrompt {
992 server_id,
993 prompt_id,
994 ..
995 } => format!("Fetch MCP prompt {server_id}:{prompt_id}"),
996 Self::UseAuthScope {
997 server_id, scope, ..
998 } => format!("Use MCP auth scope {server_id}:{scope}"),
999 }
1000 }
1001
1002 fn metadata(&self) -> &MetadataMap {
1003 self.metadata_map()
1004 }
1005
1006 fn as_any(&self) -> &dyn Any {
1007 self
1008 }
1009}
1010
1011pub struct CustomKindPolicy {
1027 allowed_kinds: BTreeSet<String>,
1028 denied_kinds: BTreeSet<String>,
1029 require_approval_by_default: bool,
1030}
1031
1032impl CustomKindPolicy {
1033 pub fn new(require_approval_by_default: bool) -> Self {
1040 Self {
1041 allowed_kinds: BTreeSet::new(),
1042 denied_kinds: BTreeSet::new(),
1043 require_approval_by_default,
1044 }
1045 }
1046
1047 pub fn allow_kind(mut self, kind: impl Into<String>) -> Self {
1049 self.allowed_kinds.insert(kind.into());
1050 self
1051 }
1052
1053 pub fn deny_kind(mut self, kind: impl Into<String>) -> Self {
1055 self.denied_kinds.insert(kind.into());
1056 self
1057 }
1058}
1059
1060impl PermissionPolicy for CustomKindPolicy {
1061 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1062 let kind = request.kind();
1063 if !kind.starts_with("custom.") {
1064 return PolicyMatch::NoOpinion;
1065 }
1066 if self.denied_kinds.contains(kind) {
1067 return PolicyMatch::Deny(PermissionDenial {
1068 code: PermissionCode::CustomPolicyDenied,
1069 message: format!("custom permission kind {kind} is denied"),
1070 metadata: request.metadata().clone(),
1071 });
1072 }
1073 if self.allowed_kinds.contains(kind) {
1074 return PolicyMatch::Allow;
1075 }
1076 if self.require_approval_by_default {
1077 PolicyMatch::RequireApproval(ApprovalRequest {
1078 task_id: None,
1079 call_id: None,
1080 id: ApprovalId::new(format!("approval:{kind}")),
1081 request_kind: kind.to_string(),
1082 reason: ApprovalReason::PolicyRequiresConfirmation,
1083 summary: request.summary(),
1084 metadata: request.metadata().clone(),
1085 })
1086 } else {
1087 PolicyMatch::NoOpinion
1088 }
1089 }
1090}
1091
1092pub struct PathPolicy {
1112 allowed_roots: Vec<CanonicalRoot>,
1113 read_only_roots: Vec<CanonicalRoot>,
1114 protected_roots: Vec<CanonicalRoot>,
1115 require_approval_outside_allowed: bool,
1116}
1117
1118impl PathPolicy {
1119 pub fn new() -> Self {
1122 Self {
1123 allowed_roots: Vec::new(),
1124 read_only_roots: Vec::new(),
1125 protected_roots: Vec::new(),
1126 require_approval_outside_allowed: true,
1127 }
1128 }
1129
1130 pub fn allow_root(mut self, root: impl Into<PathBuf>) -> Self {
1132 self.allowed_roots.push(CanonicalRoot::new(root.into()));
1133 self
1134 }
1135
1136 pub fn read_only_root(mut self, root: impl Into<PathBuf>) -> Self {
1138 self.read_only_roots.push(CanonicalRoot::new(root.into()));
1139 self
1140 }
1141
1142 pub fn protect_root(mut self, root: impl Into<PathBuf>) -> Self {
1144 self.protected_roots.push(CanonicalRoot::new(root.into()));
1145 self
1146 }
1147
1148 pub fn require_approval_outside_allowed(mut self, value: bool) -> Self {
1151 self.require_approval_outside_allowed = value;
1152 self
1153 }
1154}
1155
1156impl Default for PathPolicy {
1157 fn default() -> Self {
1158 Self::new()
1159 }
1160}
1161
1162fn resolve_canonical(path: &Path) -> PathBuf {
1166 let abs = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
1167 canonicalize_with_partial_fallback(&abs).unwrap_or(abs)
1168}
1169
1170fn canonicalize_with_partial_fallback(abs: &Path) -> Option<PathBuf> {
1171 if let Ok(canonical) = std::fs::canonicalize(abs) {
1172 return Some(canonical);
1173 }
1174 let mut tail: Vec<std::ffi::OsString> = Vec::new();
1175 let mut current = abs.to_path_buf();
1176 loop {
1177 let name = current.file_name().map(|n| n.to_os_string())?;
1178 tail.push(name);
1179 if !current.pop() {
1180 return None;
1181 }
1182 if let Ok(canonical) = std::fs::canonicalize(¤t) {
1183 let mut out = canonical;
1184 for seg in tail.iter().rev() {
1185 out.push(seg);
1186 }
1187 return Some(out);
1188 }
1189 }
1190}
1191
1192struct CanonicalRoot {
1198 lexical: PathBuf,
1199 canonical: OnceLock<PathBuf>,
1200}
1201
1202impl CanonicalRoot {
1203 fn new(lexical: PathBuf) -> Self {
1204 Self {
1205 lexical,
1206 canonical: OnceLock::new(),
1207 }
1208 }
1209
1210 fn resolve(&self) -> std::borrow::Cow<'_, Path> {
1211 if let Some(canonical) = self.canonical.get() {
1212 return std::borrow::Cow::Borrowed(canonical);
1213 }
1214 let abs =
1215 std::path::absolute(&self.lexical).unwrap_or_else(|_| self.lexical.clone());
1216 if let Ok(canonical) = std::fs::canonicalize(&abs) {
1217 let _ = self.canonical.set(canonical);
1218 return std::borrow::Cow::Borrowed(self.canonical.get().unwrap());
1219 }
1220 std::borrow::Cow::Owned(canonicalize_with_partial_fallback(&abs).unwrap_or(abs))
1221 }
1222}
1223
1224impl PermissionPolicy for PathPolicy {
1225 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1226 let Some(fs) = request
1227 .as_any()
1228 .downcast_ref::<FileSystemPermissionRequest>()
1229 else {
1230 return PolicyMatch::NoOpinion;
1231 };
1232
1233 let raw_paths: Vec<&Path> = match fs {
1234 FileSystemPermissionRequest::Move { from, to, .. } => {
1235 vec![from.as_path(), to.as_path()]
1236 }
1237 FileSystemPermissionRequest::Read { path, .. }
1238 | FileSystemPermissionRequest::Write { path, .. }
1239 | FileSystemPermissionRequest::Edit { path, .. }
1240 | FileSystemPermissionRequest::Delete { path, .. }
1241 | FileSystemPermissionRequest::List { path, .. }
1242 | FileSystemPermissionRequest::CreateDir { path, .. } => vec![path.as_path()],
1243 };
1244
1245 let candidate_paths: Vec<PathBuf> =
1246 raw_paths.iter().map(|p| resolve_canonical(p)).collect();
1247
1248 let mutates = matches!(
1249 fs,
1250 FileSystemPermissionRequest::Write { .. }
1251 | FileSystemPermissionRequest::Edit { .. }
1252 | FileSystemPermissionRequest::Delete { .. }
1253 | FileSystemPermissionRequest::Move { .. }
1254 | FileSystemPermissionRequest::CreateDir { .. }
1255 );
1256
1257 if candidate_paths.iter().any(|path| {
1258 self.protected_roots
1259 .iter()
1260 .any(|root| path.starts_with(root.resolve().as_ref()))
1261 }) {
1262 return PolicyMatch::Deny(PermissionDenial {
1263 code: PermissionCode::PathNotAllowed,
1264 message: format!("path access denied for {}", fs.summary()),
1265 metadata: fs.metadata().clone(),
1266 });
1267 }
1268
1269 if mutates
1270 && candidate_paths.iter().any(|path| {
1271 self.read_only_roots
1272 .iter()
1273 .any(|root| path.starts_with(root.resolve().as_ref()))
1274 })
1275 {
1276 return PolicyMatch::Deny(PermissionDenial {
1277 code: PermissionCode::PathNotAllowed,
1278 message: format!("path is read-only 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.iter().all(|path| {
1288 self.allowed_roots
1289 .iter()
1290 .any(|root| path.starts_with(root.resolve().as_ref()))
1291 });
1292
1293 if all_allowed {
1294 PolicyMatch::Allow
1295 } else if self.require_approval_outside_allowed {
1296 PolicyMatch::RequireApproval(ApprovalRequest {
1297 task_id: None,
1298 call_id: None,
1299 id: ApprovalId::new(format!("approval:{}", fs.kind())),
1300 request_kind: fs.kind().to_string(),
1301 reason: ApprovalReason::SensitivePath,
1302 summary: fs.summary(),
1303 metadata: fs.metadata().clone(),
1304 })
1305 } else {
1306 PolicyMatch::Deny(PermissionDenial {
1307 code: PermissionCode::PathNotAllowed,
1308 message: format!("path outside allowed roots for {}", fs.summary()),
1309 metadata: fs.metadata().clone(),
1310 })
1311 }
1312 }
1313}
1314
1315pub struct CommandPolicy {
1336 allowed_executables: BTreeSet<String>,
1337 denied_executables: BTreeSet<String>,
1338 allowed_cwds: Vec<PathBuf>,
1339 denied_env_keys: BTreeSet<String>,
1340 require_approval_for_unknown: bool,
1341}
1342
1343impl CommandPolicy {
1344 pub fn new() -> Self {
1347 Self {
1348 allowed_executables: BTreeSet::new(),
1349 denied_executables: BTreeSet::new(),
1350 allowed_cwds: Vec::new(),
1351 denied_env_keys: BTreeSet::new(),
1352 require_approval_for_unknown: true,
1353 }
1354 }
1355
1356 pub fn allow_executable(mut self, executable: impl Into<String>) -> Self {
1358 self.allowed_executables.insert(executable.into());
1359 self
1360 }
1361
1362 pub fn deny_executable(mut self, executable: impl Into<String>) -> Self {
1364 self.denied_executables.insert(executable.into());
1365 self
1366 }
1367
1368 pub fn allow_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
1370 self.allowed_cwds.push(cwd.into());
1371 self
1372 }
1373
1374 pub fn deny_env_key(mut self, key: impl Into<String>) -> Self {
1376 self.denied_env_keys.insert(key.into());
1377 self
1378 }
1379
1380 pub fn require_approval_for_unknown(mut self, value: bool) -> Self {
1383 self.require_approval_for_unknown = value;
1384 self
1385 }
1386}
1387
1388impl Default for CommandPolicy {
1389 fn default() -> Self {
1390 Self::new()
1391 }
1392}
1393
1394impl PermissionPolicy for CommandPolicy {
1395 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1396 let Some(shell) = request.as_any().downcast_ref::<ShellPermissionRequest>() else {
1397 return PolicyMatch::NoOpinion;
1398 };
1399
1400 if self.denied_executables.contains(&shell.executable)
1401 || shell
1402 .env_keys
1403 .iter()
1404 .any(|key| self.denied_env_keys.contains(key))
1405 {
1406 return PolicyMatch::Deny(PermissionDenial {
1407 code: PermissionCode::CommandNotAllowed,
1408 message: format!("command denied for {}", shell.summary()),
1409 metadata: shell.metadata().clone(),
1410 });
1411 }
1412
1413 if let Some(cwd) = &shell.cwd
1414 && !self.allowed_cwds.is_empty()
1415 && !self.allowed_cwds.iter().any(|root| cwd.starts_with(root))
1416 {
1417 return PolicyMatch::RequireApproval(ApprovalRequest {
1418 task_id: None,
1419 call_id: None,
1420 id: ApprovalId::new("approval:shell.cwd"),
1421 request_kind: shell.kind().to_string(),
1422 reason: ApprovalReason::SensitiveCommand,
1423 summary: shell.summary(),
1424 metadata: shell.metadata().clone(),
1425 });
1426 }
1427
1428 if self.allowed_executables.is_empty()
1429 || self.allowed_executables.contains(&shell.executable)
1430 {
1431 PolicyMatch::Allow
1432 } else if self.require_approval_for_unknown {
1433 PolicyMatch::RequireApproval(ApprovalRequest {
1434 task_id: None,
1435 call_id: None,
1436 id: ApprovalId::new("approval:shell.command"),
1437 request_kind: shell.kind().to_string(),
1438 reason: ApprovalReason::SensitiveCommand,
1439 summary: shell.summary(),
1440 metadata: shell.metadata().clone(),
1441 })
1442 } else {
1443 PolicyMatch::Deny(PermissionDenial {
1444 code: PermissionCode::CommandNotAllowed,
1445 message: format!("executable {} is not allowed", shell.executable),
1446 metadata: shell.metadata().clone(),
1447 })
1448 }
1449 }
1450}
1451
1452pub struct McpServerPolicy {
1466 trusted_servers: BTreeSet<String>,
1467 allowed_auth_scopes: BTreeSet<String>,
1468 require_approval_for_untrusted: bool,
1469}
1470
1471impl McpServerPolicy {
1472 pub fn new() -> Self {
1475 Self {
1476 trusted_servers: BTreeSet::new(),
1477 allowed_auth_scopes: BTreeSet::new(),
1478 require_approval_for_untrusted: true,
1479 }
1480 }
1481
1482 pub fn trust_server(mut self, server_id: impl Into<String>) -> Self {
1484 self.trusted_servers.insert(server_id.into());
1485 self
1486 }
1487
1488 pub fn allow_auth_scope(mut self, scope: impl Into<String>) -> Self {
1490 self.allowed_auth_scopes.insert(scope.into());
1491 self
1492 }
1493}
1494
1495impl Default for McpServerPolicy {
1496 fn default() -> Self {
1497 Self::new()
1498 }
1499}
1500
1501impl PermissionPolicy for McpServerPolicy {
1502 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1503 let Some(mcp) = request.as_any().downcast_ref::<McpPermissionRequest>() else {
1504 return PolicyMatch::NoOpinion;
1505 };
1506
1507 let server_id = match mcp {
1508 McpPermissionRequest::Connect { server_id, .. }
1509 | McpPermissionRequest::InvokeTool { server_id, .. }
1510 | McpPermissionRequest::ReadResource { server_id, .. }
1511 | McpPermissionRequest::FetchPrompt { server_id, .. }
1512 | McpPermissionRequest::UseAuthScope { server_id, .. } => server_id,
1513 };
1514
1515 if !self.trusted_servers.is_empty() && !self.trusted_servers.contains(server_id) {
1516 return if self.require_approval_for_untrusted {
1517 PolicyMatch::RequireApproval(ApprovalRequest {
1518 task_id: None,
1519 call_id: None,
1520 id: ApprovalId::new(format!("approval:mcp:{server_id}")),
1521 request_kind: mcp.kind().to_string(),
1522 reason: ApprovalReason::SensitiveServer,
1523 summary: mcp.summary(),
1524 metadata: mcp.metadata().clone(),
1525 })
1526 } else {
1527 PolicyMatch::Deny(PermissionDenial {
1528 code: PermissionCode::ServerNotTrusted,
1529 message: format!("MCP server {server_id} is not trusted"),
1530 metadata: mcp.metadata().clone(),
1531 })
1532 };
1533 }
1534
1535 if let McpPermissionRequest::UseAuthScope { scope, .. } = mcp
1536 && !self.allowed_auth_scopes.is_empty()
1537 && !self.allowed_auth_scopes.contains(scope)
1538 {
1539 return PolicyMatch::Deny(PermissionDenial {
1540 code: PermissionCode::AuthScopeNotAllowed,
1541 message: format!("MCP auth scope {scope} is not allowed"),
1542 metadata: mcp.metadata().clone(),
1543 });
1544 }
1545
1546 PolicyMatch::Allow
1547 }
1548}
1549
1550#[async_trait]
1602pub trait Tool: Send + Sync {
1603 fn spec(&self) -> &ToolSpec;
1605
1606 fn current_spec(&self) -> Option<ToolSpec> {
1614 Some(self.spec().clone())
1615 }
1616
1617 fn proposed_requests(
1629 &self,
1630 _request: &ToolRequest,
1631 ) -> Result<Vec<Box<dyn PermissionRequest>>, ToolError> {
1632 Ok(Vec::new())
1633 }
1634
1635 async fn invoke(
1644 &self,
1645 request: ToolRequest,
1646 ctx: &mut ToolContext<'_>,
1647 ) -> Result<ToolResult, ToolError>;
1648}
1649
1650#[derive(Clone, Default)]
1681pub struct ToolRegistry {
1682 tools: BTreeMap<ToolName, Arc<dyn Tool>>,
1683}
1684
1685impl ToolRegistry {
1686 pub fn new() -> Self {
1688 Self::default()
1689 }
1690
1691 pub fn register<T>(&mut self, tool: T) -> &mut Self
1693 where
1694 T: Tool + 'static,
1695 {
1696 self.tools.insert(tool.spec().name.clone(), Arc::new(tool));
1697 self
1698 }
1699
1700 pub fn with<T>(mut self, tool: T) -> Self
1702 where
1703 T: Tool + 'static,
1704 {
1705 self.register(tool);
1706 self
1707 }
1708
1709 pub fn register_arc(&mut self, tool: Arc<dyn Tool>) -> &mut Self {
1711 self.tools.insert(tool.spec().name.clone(), tool);
1712 self
1713 }
1714
1715 pub fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1717 self.tools.get(name).cloned()
1718 }
1719
1720 pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
1722 self.tools.values().cloned().collect()
1723 }
1724
1725 pub fn merge(mut self, other: Self) -> Self {
1734 self.tools.extend(other.tools);
1735 self
1736 }
1737
1738 pub fn specs(&self) -> Vec<ToolSpec> {
1740 self.tools
1741 .values()
1742 .filter_map(|tool| tool.current_spec())
1743 .collect()
1744 }
1745}
1746
1747pub trait ToolSource: Send + Sync {
1755 fn specs(&self) -> Vec<ToolSpec>;
1757
1758 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>>;
1760
1761 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
1765 Vec::new()
1766 }
1767
1768 fn prefixed(self, prefix: impl Into<String>) -> Prefixed<Self>
1778 where
1779 Self: Sized,
1780 {
1781 Prefixed::new(self, prefix)
1782 }
1783
1784 fn filtered<F>(self, predicate: F) -> Filtered<Self, F>
1790 where
1791 Self: Sized,
1792 F: Fn(&ToolName) -> bool + Send + Sync + 'static,
1793 {
1794 Filtered::new(self, predicate)
1795 }
1796
1797 fn renamed<I>(self, mapping: I) -> Renamed<Self>
1804 where
1805 Self: Sized,
1806 I: IntoIterator<Item = (ToolName, ToolName)>,
1807 {
1808 Renamed::new(self, mapping)
1809 }
1810}
1811
1812impl ToolSource for ToolRegistry {
1813 fn specs(&self) -> Vec<ToolSpec> {
1814 ToolRegistry::specs(self)
1815 }
1816
1817 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1818 ToolRegistry::get(self, name)
1819 }
1820}
1821
1822impl<S> ToolSource for Arc<S>
1823where
1824 S: ToolSource + ?Sized,
1825{
1826 fn specs(&self) -> Vec<ToolSpec> {
1827 (**self).specs()
1828 }
1829
1830 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1831 (**self).get(name)
1832 }
1833
1834 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
1835 (**self).drain_catalog_events()
1836 }
1837}
1838
1839pub struct Prefixed<S> {
1842 inner: S,
1843 prefix: String,
1844}
1845
1846impl<S> Prefixed<S> {
1847 pub fn new(inner: S, prefix: impl Into<String>) -> Self {
1849 Self {
1850 inner,
1851 prefix: prefix.into(),
1852 }
1853 }
1854
1855 fn rewrite(&self, name: &str) -> String {
1856 format!("{}_{}", self.prefix, name)
1857 }
1858
1859 fn strip<'a>(&self, name: &'a str) -> Option<&'a str> {
1860 name.strip_prefix(self.prefix.as_str())
1861 .and_then(|rest| rest.strip_prefix('_'))
1862 }
1863}
1864
1865impl<S> ToolSource for Prefixed<S>
1866where
1867 S: ToolSource,
1868{
1869 fn specs(&self) -> Vec<ToolSpec> {
1870 self.inner
1871 .specs()
1872 .into_iter()
1873 .map(|mut spec| {
1874 spec.name = ToolName::new(self.rewrite(spec.name.0.as_str()));
1875 spec
1876 })
1877 .collect()
1878 }
1879
1880 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1881 let original = self.strip(name.0.as_str())?;
1882 let inner_name = ToolName::new(original);
1883 let inner_tool = self.inner.get(&inner_name)?;
1884 let mut public_spec = inner_tool.spec().clone();
1885 public_spec.name = name.clone();
1886 Some(Arc::new(RewrittenTool {
1887 inner: inner_tool,
1888 inner_name,
1889 public_spec,
1890 }))
1891 }
1892
1893 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
1894 self.inner
1895 .drain_catalog_events()
1896 .into_iter()
1897 .map(|mut event| {
1898 event.for_each_name_mut(|name| *name = self.rewrite(name.as_str()));
1899 event
1900 })
1901 .collect()
1902 }
1903}
1904
1905pub struct Filtered<S, F> {
1908 inner: S,
1909 predicate: F,
1910}
1911
1912impl<S, F> Filtered<S, F> {
1913 pub fn new(inner: S, predicate: F) -> Self {
1915 Self { inner, predicate }
1916 }
1917}
1918
1919impl<S, F> ToolSource for Filtered<S, F>
1920where
1921 S: ToolSource,
1922 F: Fn(&ToolName) -> bool + Send + Sync + 'static,
1923{
1924 fn specs(&self) -> Vec<ToolSpec> {
1925 self.inner
1926 .specs()
1927 .into_iter()
1928 .filter(|spec| (self.predicate)(&spec.name))
1929 .collect()
1930 }
1931
1932 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1933 if !(self.predicate)(name) {
1934 return None;
1935 }
1936 self.inner.get(name)
1937 }
1938
1939 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
1940 self.inner
1941 .drain_catalog_events()
1942 .into_iter()
1943 .map(|mut event| {
1944 event.retain_names(|n| (self.predicate)(&ToolName::new(n)));
1945 event
1946 })
1947 .collect()
1948 }
1949}
1950
1951pub struct Renamed<S> {
1958 inner: S,
1959 forward: BTreeMap<ToolName, ToolName>,
1960 backward: BTreeMap<ToolName, ToolName>,
1961}
1962
1963impl<S> Renamed<S> {
1964 pub fn new<I>(inner: S, mapping: I) -> Self
1966 where
1967 I: IntoIterator<Item = (ToolName, ToolName)>,
1968 {
1969 let forward: BTreeMap<ToolName, ToolName> = mapping.into_iter().collect();
1970 let backward = forward
1971 .iter()
1972 .map(|(k, v)| (v.clone(), k.clone()))
1973 .collect();
1974 Self {
1975 inner,
1976 forward,
1977 backward,
1978 }
1979 }
1980}
1981
1982impl<S> ToolSource for Renamed<S>
1983where
1984 S: ToolSource,
1985{
1986 fn specs(&self) -> Vec<ToolSpec> {
1987 self.inner
1988 .specs()
1989 .into_iter()
1990 .map(|mut spec| {
1991 if let Some(new_name) = self.forward.get(&spec.name) {
1992 spec.name = new_name.clone();
1993 }
1994 spec
1995 })
1996 .collect()
1997 }
1998
1999 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2000 if let Some(original) = self.backward.get(name) {
2001 let inner_tool = self.inner.get(original)?;
2002 let mut public_spec = inner_tool.spec().clone();
2003 public_spec.name = name.clone();
2004 Some(Arc::new(RewrittenTool {
2005 inner: inner_tool,
2006 inner_name: original.clone(),
2007 public_spec,
2008 }))
2009 } else if self.forward.contains_key(name) {
2010 None
2012 } else {
2013 self.inner.get(name)
2014 }
2015 }
2016
2017 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2018 self.inner
2019 .drain_catalog_events()
2020 .into_iter()
2021 .map(|mut event| {
2022 event.for_each_name_mut(|name| {
2023 if let Some(new) = self.forward.get(&ToolName::new(name.as_str())) {
2024 *name = new.0.clone();
2025 }
2026 });
2027 event
2028 })
2029 .collect()
2030 }
2031}
2032
2033#[cfg(feature = "schemars")]
2059pub fn schema_for<T: schemars::JsonSchema>() -> Value {
2060 let schema = schemars::schema_for!(T);
2061 serde_json::to_value(schema)
2062 .expect("schemars produces valid JSON; this conversion is infallible")
2063}
2064
2065#[cfg(feature = "schemars")]
2083pub fn tool_spec_for<T: schemars::JsonSchema>(
2084 name: impl Into<ToolName>,
2085 description: impl Into<String>,
2086) -> ToolSpec {
2087 ToolSpec::new(name, description, schema_for::<T>())
2088}
2089
2090struct RewrittenTool {
2096 inner: Arc<dyn Tool>,
2097 inner_name: ToolName,
2098 public_spec: ToolSpec,
2099}
2100
2101#[async_trait]
2102impl Tool for RewrittenTool {
2103 fn spec(&self) -> &ToolSpec {
2104 &self.public_spec
2105 }
2106
2107 fn current_spec(&self) -> Option<ToolSpec> {
2108 let inner_current = self.inner.current_spec()?;
2109 Some(ToolSpec {
2110 name: self.public_spec.name.clone(),
2111 description: inner_current.description,
2112 input_schema: inner_current.input_schema,
2113 annotations: inner_current.annotations,
2114 metadata: inner_current.metadata,
2115 })
2116 }
2117
2118 fn proposed_requests(
2119 &self,
2120 request: &ToolRequest,
2121 ) -> Result<Vec<Box<dyn PermissionRequest>>, ToolError> {
2122 let mut inner_request = request.clone();
2123 inner_request.tool_name = self.inner_name.clone();
2124 self.inner.proposed_requests(&inner_request)
2125 }
2126
2127 async fn invoke(
2128 &self,
2129 mut request: ToolRequest,
2130 ctx: &mut ToolContext<'_>,
2131 ) -> Result<ToolResult, ToolError> {
2132 request.tool_name = self.inner_name.clone();
2133 self.inner.invoke(request, ctx).await
2134 }
2135}
2136
2137struct ToolMap {
2155 inner: std::sync::RwLock<BTreeMap<ToolName, Arc<dyn Tool>>>,
2156}
2157
2158impl ToolMap {
2159 fn new() -> Self {
2160 Self {
2161 inner: std::sync::RwLock::new(BTreeMap::new()),
2162 }
2163 }
2164
2165 fn read(&self) -> std::sync::RwLockReadGuard<'_, BTreeMap<ToolName, Arc<dyn Tool>>> {
2166 self.inner.read().unwrap_or_else(|e| e.into_inner())
2167 }
2168
2169 fn write(&self) -> std::sync::RwLockWriteGuard<'_, BTreeMap<ToolName, Arc<dyn Tool>>> {
2170 self.inner.write().unwrap_or_else(|e| e.into_inner())
2171 }
2172}
2173
2174struct DynamicCatalogInner {
2177 source_id: String,
2178 tools: ToolMap,
2179 events_tx: tokio::sync::broadcast::Sender<ToolCatalogEvent>,
2180}
2181
2182pub fn dynamic_catalog(source_id: impl Into<String>) -> (CatalogWriter, CatalogReader) {
2200 let (events_tx, events_rx) = tokio::sync::broadcast::channel(128);
2201 let inner = Arc::new(DynamicCatalogInner {
2202 source_id: source_id.into(),
2203 tools: ToolMap::new(),
2204 events_tx,
2205 });
2206 (
2207 CatalogWriter {
2208 inner: Arc::clone(&inner),
2209 },
2210 CatalogReader {
2211 inner,
2212 events_rx: std::sync::Mutex::new(events_rx),
2213 },
2214 )
2215}
2216
2217pub struct CatalogWriter {
2224 inner: Arc<DynamicCatalogInner>,
2225}
2226
2227impl CatalogWriter {
2228 pub fn source_id(&self) -> &str {
2230 &self.inner.source_id
2231 }
2232
2233 pub fn reader(&self) -> CatalogReader {
2237 CatalogReader {
2238 inner: Arc::clone(&self.inner),
2239 events_rx: std::sync::Mutex::new(self.inner.events_tx.subscribe()),
2240 }
2241 }
2242
2243 pub fn upsert(&self, tool: Arc<dyn Tool>) {
2246 let name = tool.spec().name.clone();
2247 let mut guard = self.inner.tools.write();
2248 let existed = guard.insert(name.clone(), tool).is_some();
2249 drop(guard);
2250 let mut event = ToolCatalogEvent::new(self.inner.source_id.clone());
2251 if existed {
2252 event.changed.push(name.0);
2253 } else {
2254 event.added.push(name.0);
2255 }
2256 let _ = self.inner.events_tx.send(event);
2257 }
2258
2259 pub fn remove(&self, name: &ToolName) -> bool {
2262 let mut guard = self.inner.tools.write();
2263 let removed = guard.remove(name).is_some();
2264 drop(guard);
2265 if removed {
2266 let mut event = ToolCatalogEvent::new(self.inner.source_id.clone());
2267 event.removed.push(name.0.clone());
2268 let _ = self.inner.events_tx.send(event);
2269 }
2270 removed
2271 }
2272
2273 pub fn replace_all(&self, tools: impl IntoIterator<Item = Arc<dyn Tool>>) {
2276 let new_map: BTreeMap<ToolName, Arc<dyn Tool>> = tools
2277 .into_iter()
2278 .map(|tool| (tool.spec().name.clone(), tool))
2279 .collect();
2280
2281 let mut guard = self.inner.tools.write();
2282 let mut event = ToolCatalogEvent::new(self.inner.source_id.clone());
2283
2284 for (name, new_tool) in new_map.iter() {
2285 match guard.get(name) {
2286 None => event.added.push(name.0.clone()),
2287 Some(existing)
2288 if !Arc::ptr_eq(existing, new_tool)
2289 && existing.current_spec() != new_tool.current_spec() =>
2290 {
2291 event.changed.push(name.0.clone());
2292 }
2293 Some(_) => {}
2294 }
2295 }
2296 for name in guard.keys() {
2297 if !new_map.contains_key(name) {
2298 event.removed.push(name.0.clone());
2299 }
2300 }
2301
2302 *guard = new_map;
2303 drop(guard);
2304
2305 if !event.added.is_empty() || !event.removed.is_empty() || !event.changed.is_empty() {
2306 let _ = self.inner.events_tx.send(event);
2307 }
2308 }
2309
2310 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ToolCatalogEvent> {
2314 self.inner.events_tx.subscribe()
2315 }
2316}
2317
2318pub struct CatalogReader {
2323 inner: Arc<DynamicCatalogInner>,
2324 events_rx: std::sync::Mutex<tokio::sync::broadcast::Receiver<ToolCatalogEvent>>,
2325}
2326
2327impl CatalogReader {
2328 pub fn source_id(&self) -> &str {
2330 &self.inner.source_id
2331 }
2332
2333 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ToolCatalogEvent> {
2336 self.inner.events_tx.subscribe()
2337 }
2338}
2339
2340impl Clone for CatalogReader {
2341 fn clone(&self) -> Self {
2342 Self {
2343 inner: Arc::clone(&self.inner),
2344 events_rx: std::sync::Mutex::new(self.inner.events_tx.subscribe()),
2345 }
2346 }
2347}
2348
2349impl ToolSource for CatalogReader {
2350 fn specs(&self) -> Vec<ToolSpec> {
2351 self.inner
2352 .tools
2353 .read()
2354 .values()
2355 .filter_map(|tool| tool.current_spec())
2356 .collect()
2357 }
2358
2359 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2360 self.inner.tools.read().get(name).cloned()
2361 }
2362
2363 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2364 let mut rx = self.events_rx.lock().unwrap_or_else(|e| e.into_inner());
2369 let mut out = Vec::new();
2370 loop {
2371 match rx.try_recv() {
2372 Ok(event) => out.push(event),
2373 Err(tokio::sync::broadcast::error::TryRecvError::Empty) => break,
2374 Err(tokio::sync::broadcast::error::TryRecvError::Closed) => break,
2375 Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => continue,
2376 }
2377 }
2378 out
2379 }
2380}
2381
2382impl ToolSpec {
2383 pub fn as_invocable_spec(&self) -> InvocableSpec {
2386 InvocableSpec::new(
2387 CapabilityName::new(self.name.0.clone()),
2388 self.description.clone(),
2389 self.input_schema.clone(),
2390 )
2391 .with_metadata(self.metadata.clone())
2392 }
2393}
2394
2395pub struct ToolInvocableAdapter {
2401 spec: InvocableSpec,
2402 tool: Arc<dyn Tool>,
2403 permissions: Arc<dyn PermissionChecker>,
2404 resources: Arc<dyn ToolResources>,
2405 next_call_id: AtomicU64,
2406}
2407
2408impl ToolInvocableAdapter {
2409 pub fn new(
2412 tool: Arc<dyn Tool>,
2413 permissions: Arc<dyn PermissionChecker>,
2414 resources: Arc<dyn ToolResources>,
2415 ) -> Option<Self> {
2416 let spec = tool.current_spec()?.as_invocable_spec();
2417 Some(Self {
2418 spec,
2419 tool,
2420 permissions,
2421 resources,
2422 next_call_id: AtomicU64::new(1),
2423 })
2424 }
2425}
2426
2427#[async_trait]
2428impl Invocable for ToolInvocableAdapter {
2429 fn spec(&self) -> &InvocableSpec {
2430 &self.spec
2431 }
2432
2433 async fn invoke(
2434 &self,
2435 request: InvocableRequest,
2436 ctx: &mut CapabilityContext<'_>,
2437 ) -> Result<InvocableResult, CapabilityError> {
2438 let tool_request = ToolRequest {
2439 call_id: ToolCallId::new(format!(
2440 "tool-call-{}",
2441 self.next_call_id.fetch_add(1, Ordering::Relaxed)
2442 )),
2443 tool_name: self.tool.spec().name.clone(),
2444 input: request.input,
2445 session_id: ctx
2446 .session_id
2447 .cloned()
2448 .unwrap_or_else(|| SessionId::new("capability-session")),
2449 turn_id: ctx
2450 .turn_id
2451 .cloned()
2452 .unwrap_or_else(|| TurnId::new("capability-turn")),
2453 metadata: request.metadata,
2454 };
2455
2456 for permission_request in self
2457 .tool
2458 .proposed_requests(&tool_request)
2459 .map_err(|error| CapabilityError::InvalidInput(error.to_string()))?
2460 {
2461 match self.permissions.evaluate(permission_request.as_ref()) {
2462 PermissionDecision::Allow => {}
2463 PermissionDecision::Deny(denial) => {
2464 return Err(CapabilityError::ExecutionFailed(format!(
2465 "tool permission denied: {denial:?}"
2466 )));
2467 }
2468 PermissionDecision::RequireApproval(req) => {
2469 return Err(CapabilityError::Unavailable(format!(
2470 "tool invocation requires approval: {}",
2471 req.summary
2472 )));
2473 }
2474 }
2475 }
2476
2477 let mut tool_ctx = ToolContext {
2478 capability: CapabilityContext {
2479 session_id: ctx.session_id,
2480 turn_id: ctx.turn_id,
2481 metadata: ctx.metadata,
2482 },
2483 permissions: self.permissions.as_ref(),
2484 resources: self.resources.as_ref(),
2485 cancellation: None,
2486 };
2487
2488 let result = self
2489 .tool
2490 .invoke(tool_request, &mut tool_ctx)
2491 .await
2492 .map_err(|error| CapabilityError::ExecutionFailed(error.to_string()))?;
2493
2494 Ok(InvocableResult {
2495 output: match result.result.output {
2496 ToolOutput::Text(text) => InvocableOutput::Text(text),
2497 ToolOutput::Structured(value) => InvocableOutput::Structured(value),
2498 ToolOutput::Parts(parts) => InvocableOutput::Items(vec![Item {
2499 id: None,
2500 kind: ItemKind::Tool,
2501 parts,
2502 metadata: MetadataMap::new(),
2503 }]),
2504 ToolOutput::Files(files) => {
2505 let parts = files.into_iter().map(Part::File).collect();
2506 InvocableOutput::Items(vec![Item {
2507 id: None,
2508 kind: ItemKind::Tool,
2509 parts,
2510 metadata: MetadataMap::new(),
2511 }])
2512 }
2513 },
2514 metadata: result.metadata,
2515 })
2516 }
2517}
2518
2519pub struct ToolCapabilityProvider {
2525 invocables: Vec<Arc<dyn Invocable>>,
2526}
2527
2528impl ToolCapabilityProvider {
2529 pub fn from_registry(
2532 registry: &ToolRegistry,
2533 permissions: Arc<dyn PermissionChecker>,
2534 resources: Arc<dyn ToolResources>,
2535 ) -> Self {
2536 let invocables = registry
2537 .tools()
2538 .into_iter()
2539 .filter_map(|tool| {
2540 ToolInvocableAdapter::new(tool, permissions.clone(), resources.clone())
2541 .map(|adapter| Arc::new(adapter) as Arc<dyn Invocable>)
2542 })
2543 .collect();
2544
2545 Self { invocables }
2546 }
2547}
2548
2549impl CapabilityProvider for ToolCapabilityProvider {
2550 fn invocables(&self) -> Vec<Arc<dyn Invocable>> {
2551 self.invocables.clone()
2552 }
2553
2554 fn resources(&self) -> Vec<Arc<dyn ResourceProvider>> {
2555 Vec::new()
2556 }
2557
2558 fn prompts(&self) -> Vec<Arc<dyn PromptProvider>> {
2559 Vec::new()
2560 }
2561}
2562
2563#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2569pub enum ToolExecutionOutcome {
2570 Completed(ToolResult),
2572 Interrupted(ToolInterruption),
2574 Failed(ToolError),
2576}
2577
2578#[async_trait]
2586pub trait ToolExecutor: Send + Sync {
2587 fn specs(&self) -> Vec<ToolSpec>;
2589
2590 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2595 Vec::new()
2596 }
2597
2598 async fn execute(
2600 &self,
2601 request: ToolRequest,
2602 ctx: &mut ToolContext<'_>,
2603 ) -> ToolExecutionOutcome;
2604
2605 async fn execute_owned(
2608 &self,
2609 request: ToolRequest,
2610 ctx: OwnedToolContext,
2611 ) -> ToolExecutionOutcome {
2612 let mut borrowed = ctx.borrowed();
2613 self.execute(request, &mut borrowed).await
2614 }
2615
2616 async fn execute_approved(
2622 &self,
2623 request: ToolRequest,
2624 approved_request: &ApprovalRequest,
2625 ctx: &mut ToolContext<'_>,
2626 ) -> ToolExecutionOutcome {
2627 let _ = approved_request;
2628 self.execute(request, ctx).await
2629 }
2630
2631 async fn execute_approved_owned(
2634 &self,
2635 request: ToolRequest,
2636 approved_request: &ApprovalRequest,
2637 ctx: OwnedToolContext,
2638 ) -> ToolExecutionOutcome {
2639 let mut borrowed = ctx.borrowed();
2640 self.execute_approved(request, approved_request, &mut borrowed)
2641 .await
2642 }
2643}
2644
2645#[async_trait]
2646impl<T> ToolExecutor for Arc<T>
2647where
2648 T: ToolExecutor + ?Sized,
2649{
2650 fn specs(&self) -> Vec<ToolSpec> {
2651 (**self).specs()
2652 }
2653
2654 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2655 (**self).drain_catalog_events()
2656 }
2657
2658 async fn execute(
2659 &self,
2660 request: ToolRequest,
2661 ctx: &mut ToolContext<'_>,
2662 ) -> ToolExecutionOutcome {
2663 (**self).execute(request, ctx).await
2664 }
2665
2666 async fn execute_approved(
2667 &self,
2668 request: ToolRequest,
2669 approved_request: &ApprovalRequest,
2670 ctx: &mut ToolContext<'_>,
2671 ) -> ToolExecutionOutcome {
2672 (**self)
2673 .execute_approved(request, approved_request, ctx)
2674 .await
2675 }
2676}
2677
2678#[derive(Clone, Debug, Default, PartialEq, Eq)]
2681pub enum CollisionPolicy {
2682 #[default]
2685 FirstWins,
2686 LastWins,
2688}
2689
2690pub struct BasicToolExecutor {
2709 sources: Vec<Arc<dyn ToolSource>>,
2710 collision: CollisionPolicy,
2711}
2712
2713impl BasicToolExecutor {
2714 pub fn new(sources: impl IntoIterator<Item = Arc<dyn ToolSource>>) -> Self {
2716 Self {
2717 sources: sources.into_iter().collect(),
2718 collision: CollisionPolicy::default(),
2719 }
2720 }
2721
2722 pub fn from_registry(registry: ToolRegistry) -> Self {
2724 Self::new([Arc::new(registry) as Arc<dyn ToolSource>])
2725 }
2726
2727 pub fn with_collision_policy(mut self, policy: CollisionPolicy) -> Self {
2730 self.collision = policy;
2731 self
2732 }
2733
2734 pub fn specs(&self) -> Vec<ToolSpec> {
2737 let mut seen = BTreeSet::new();
2738 let mut out = Vec::new();
2739 let iter: Box<dyn Iterator<Item = &Arc<dyn ToolSource>>> = match self.collision {
2740 CollisionPolicy::FirstWins => Box::new(self.sources.iter()),
2741 CollisionPolicy::LastWins => Box::new(self.sources.iter().rev()),
2742 };
2743 for source in iter {
2744 for spec in source.specs() {
2745 if seen.insert(spec.name.clone()) {
2746 out.push(spec);
2747 }
2748 }
2749 }
2750 out
2751 }
2752
2753 fn lookup(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2754 match self.collision {
2755 CollisionPolicy::FirstWins => self.sources.iter().find_map(|s| s.get(name)),
2756 CollisionPolicy::LastWins => self.sources.iter().rev().find_map(|s| s.get(name)),
2757 }
2758 }
2759
2760 async fn execute_inner(
2761 &self,
2762 request: ToolRequest,
2763 approved_request_id: Option<&ApprovalId>,
2764 ctx: &mut ToolContext<'_>,
2765 ) -> ToolExecutionOutcome {
2766 let Some(tool) = self.lookup(&request.tool_name) else {
2767 return ToolExecutionOutcome::Failed(ToolError::NotFound(request.tool_name));
2768 };
2769
2770 match tool.proposed_requests(&request) {
2771 Ok(requests) => {
2772 for permission_request in requests {
2773 match ctx.permissions.evaluate(permission_request.as_ref()) {
2774 PermissionDecision::Allow => {}
2775 PermissionDecision::Deny(denial) => {
2776 return ToolExecutionOutcome::Failed(ToolError::PermissionDenied(
2777 denial,
2778 ));
2779 }
2780 PermissionDecision::RequireApproval(mut req) => {
2781 req.call_id = Some(request.call_id.clone());
2782 if approved_request_id != Some(&req.id) {
2783 return ToolExecutionOutcome::Interrupted(
2784 ToolInterruption::ApprovalRequired(req),
2785 );
2786 }
2787 }
2788 }
2789 }
2790 }
2791 Err(error) => return ToolExecutionOutcome::Failed(error),
2792 }
2793
2794 match tool.invoke(request, ctx).await {
2795 Ok(result) => ToolExecutionOutcome::Completed(result),
2796 Err(error) => ToolExecutionOutcome::Failed(error),
2797 }
2798 }
2799}
2800
2801#[async_trait]
2802impl ToolExecutor for BasicToolExecutor {
2803 fn specs(&self) -> Vec<ToolSpec> {
2804 BasicToolExecutor::specs(self)
2805 }
2806
2807 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2808 self.sources
2809 .iter()
2810 .flat_map(|s| s.drain_catalog_events())
2811 .collect()
2812 }
2813
2814 async fn execute(
2815 &self,
2816 request: ToolRequest,
2817 ctx: &mut ToolContext<'_>,
2818 ) -> ToolExecutionOutcome {
2819 self.execute_inner(request, None, ctx).await
2820 }
2821
2822 async fn execute_approved(
2823 &self,
2824 request: ToolRequest,
2825 approved_request: &ApprovalRequest,
2826 ctx: &mut ToolContext<'_>,
2827 ) -> ToolExecutionOutcome {
2828 self.execute_inner(request, Some(&approved_request.id), ctx)
2829 .await
2830 }
2831}
2832
2833#[derive(Debug, Error, Clone, PartialEq, Serialize, Deserialize)]
2838pub enum ToolError {
2839 #[error("tool not found: {0}")]
2841 NotFound(ToolName),
2842 #[error("invalid tool input: {0}")]
2844 InvalidInput(String),
2845 #[error("tool permission denied: {0:?}")]
2847 PermissionDenied(PermissionDenial),
2848 #[error("tool execution failed: {0}")]
2850 ExecutionFailed(String),
2851 #[error("tool unavailable: {0}")]
2853 Unavailable(String),
2854 #[error("tool execution cancelled")]
2856 Cancelled,
2857 #[error("internal tool error: {0}")]
2859 Internal(String),
2860}
2861
2862impl ToolError {
2863 pub fn permission_denied(denial: PermissionDenial) -> Self {
2865 Self::PermissionDenied(denial)
2866 }
2867}
2868
2869impl From<PermissionDenial> for ToolError {
2870 fn from(value: PermissionDenial) -> Self {
2871 Self::permission_denied(value)
2872 }
2873}
2874
2875#[cfg(test)]
2876mod tests {
2877 use super::*;
2878 use async_trait::async_trait;
2879 use serde_json::json;
2880
2881 #[test]
2882 fn command_policy_can_deny_unknown_executables_without_approval() {
2883 let policy = CommandPolicy::new()
2884 .allow_executable("pwd")
2885 .require_approval_for_unknown(false);
2886 let request = ShellPermissionRequest {
2887 executable: "rm".into(),
2888 argv: vec!["-rf".into(), "/tmp/demo".into()],
2889 cwd: None,
2890 env_keys: Vec::new(),
2891 metadata: MetadataMap::new(),
2892 };
2893
2894 match policy.evaluate(&request) {
2895 PolicyMatch::Deny(denial) => {
2896 assert_eq!(denial.code, PermissionCode::CommandNotAllowed);
2897 }
2898 other => panic!("unexpected policy match: {other:?}"),
2899 }
2900 }
2901
2902 #[test]
2903 fn path_policy_allows_reads_under_read_only_roots() {
2904 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2905 let request = FileSystemPermissionRequest::Read {
2906 path: PathBuf::from("/workspace/vendor/lib.rs"),
2907 metadata: MetadataMap::new(),
2908 };
2909
2910 match policy.evaluate(&request) {
2911 PolicyMatch::NoOpinion | PolicyMatch::Allow => {}
2912 other => panic!("unexpected policy match: {other:?}"),
2913 }
2914 }
2915
2916 #[test]
2917 fn path_policy_denies_mutations_under_read_only_roots() {
2918 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2919 let request = FileSystemPermissionRequest::Edit {
2920 path: PathBuf::from("/workspace/vendor/lib.rs"),
2921 metadata: MetadataMap::new(),
2922 };
2923
2924 match policy.evaluate(&request) {
2925 PolicyMatch::Deny(denial) => {
2926 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
2927 assert!(denial.message.contains("read-only"));
2928 }
2929 other => panic!("unexpected policy match: {other:?}"),
2930 }
2931 }
2932
2933 #[test]
2934 fn path_policy_denies_moves_into_read_only_roots() {
2935 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2936 let request = FileSystemPermissionRequest::Move {
2937 from: PathBuf::from("/workspace/src/lib.rs"),
2938 to: PathBuf::from("/workspace/vendor/lib.rs"),
2939 metadata: MetadataMap::new(),
2940 };
2941
2942 match policy.evaluate(&request) {
2943 PolicyMatch::Deny(denial) => {
2944 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
2945 assert!(denial.message.contains("read-only"));
2946 }
2947 other => panic!("unexpected policy match: {other:?}"),
2948 }
2949 }
2950
2951 #[cfg(unix)]
2952 struct SymlinkTmpDir(PathBuf);
2953
2954 #[cfg(unix)]
2955 impl SymlinkTmpDir {
2956 fn new(label: &str) -> Self {
2957 use std::time::{SystemTime, UNIX_EPOCH};
2958 let nanos = SystemTime::now()
2959 .duration_since(UNIX_EPOCH)
2960 .unwrap()
2961 .as_nanos();
2962 let dir = std::env::temp_dir().join(format!(
2963 "agentkit-pathpolicy-{}-{}-{}",
2964 label,
2965 std::process::id(),
2966 nanos
2967 ));
2968 std::fs::create_dir_all(&dir).unwrap();
2969 Self(std::fs::canonicalize(&dir).unwrap())
2972 }
2973
2974 fn path(&self) -> &Path {
2975 &self.0
2976 }
2977 }
2978
2979 #[cfg(unix)]
2980 impl Drop for SymlinkTmpDir {
2981 fn drop(&mut self) {
2982 let _ = std::fs::remove_dir_all(&self.0);
2983 }
2984 }
2985
2986 #[cfg(unix)]
2987 fn assert_path_denied(
2988 policy: &PathPolicy,
2989 request: FileSystemPermissionRequest,
2990 ) -> PermissionDenial {
2991 match policy.evaluate(&request) {
2992 PolicyMatch::Deny(denial) => denial,
2993 other => panic!("expected deny, got: {other:?}"),
2994 }
2995 }
2996
2997 #[cfg(unix)]
2998 #[test]
2999 fn path_policy_blocks_symlink_escape_from_allowed_root() {
3000 let tmp = SymlinkTmpDir::new("allow-escape");
3001 let allowed = tmp.path().join("workspace");
3002 let outside = tmp.path().join("outside");
3003 std::fs::create_dir_all(&allowed).unwrap();
3004 std::fs::create_dir_all(&outside).unwrap();
3005 let secret = outside.join("secret.txt");
3006 std::fs::write(&secret, b"top-secret").unwrap();
3007 let escape = allowed.join("leak");
3008 std::os::unix::fs::symlink(&secret, &escape).unwrap();
3009
3010 let policy = PathPolicy::new()
3011 .allow_root(&allowed)
3012 .require_approval_outside_allowed(false);
3013 let denial = assert_path_denied(
3014 &policy,
3015 FileSystemPermissionRequest::Read {
3016 path: escape,
3017 metadata: MetadataMap::new(),
3018 },
3019 );
3020 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3021 }
3022
3023 #[cfg(unix)]
3024 #[test]
3025 fn path_policy_blocks_symlink_into_protected_root() {
3026 let tmp = SymlinkTmpDir::new("protect-bypass");
3027 let workspace = tmp.path().join("workspace");
3028 std::fs::create_dir_all(&workspace).unwrap();
3029 let secret = workspace.join(".env");
3030 std::fs::write(&secret, b"API_KEY=xxx").unwrap();
3031 let alias = workspace.join("config");
3032 std::os::unix::fs::symlink(&secret, &alias).unwrap();
3033
3034 let policy = PathPolicy::new()
3035 .allow_root(&workspace)
3036 .protect_root(&secret);
3037 let denial = assert_path_denied(
3038 &policy,
3039 FileSystemPermissionRequest::Read {
3040 path: alias,
3041 metadata: MetadataMap::new(),
3042 },
3043 );
3044 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3045 assert!(denial.message.contains("denied"));
3046 }
3047
3048 #[cfg(unix)]
3049 #[test]
3050 fn path_policy_blocks_symlink_write_into_read_only_root() {
3051 let tmp = SymlinkTmpDir::new("readonly-bypass");
3052 let workspace = tmp.path().join("workspace");
3053 let vendor = workspace.join("vendor");
3054 std::fs::create_dir_all(&vendor).unwrap();
3055 let target = vendor.join("lib.rs");
3056 std::fs::write(&target, b"// vendored").unwrap();
3057 let writable_alias = workspace.join("writable");
3058 std::os::unix::fs::symlink(&target, &writable_alias).unwrap();
3059
3060 let policy = PathPolicy::new()
3061 .allow_root(&workspace)
3062 .read_only_root(&vendor);
3063 let denial = assert_path_denied(
3064 &policy,
3065 FileSystemPermissionRequest::Edit {
3066 path: writable_alias,
3067 metadata: MetadataMap::new(),
3068 },
3069 );
3070 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3071 assert!(denial.message.contains("read-only"));
3072 }
3073
3074 #[cfg(unix)]
3075 #[test]
3076 fn path_policy_resolves_symlink_parent_for_nonexistent_leaf() {
3077 let tmp = SymlinkTmpDir::new("create-escape");
3078 let allowed = tmp.path().join("workspace");
3079 let outside = tmp.path().join("outside");
3080 std::fs::create_dir_all(&allowed).unwrap();
3081 std::fs::create_dir_all(&outside).unwrap();
3082 let escape_dir = allowed.join("escape");
3083 std::os::unix::fs::symlink(&outside, &escape_dir).unwrap();
3084 let new_file = escape_dir.join("new.txt");
3085
3086 let policy = PathPolicy::new()
3087 .allow_root(&allowed)
3088 .require_approval_outside_allowed(false);
3089 let denial = assert_path_denied(
3090 &policy,
3091 FileSystemPermissionRequest::Write {
3092 path: new_file,
3093 metadata: MetadataMap::new(),
3094 },
3095 );
3096 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3097 }
3098
3099 #[derive(Clone)]
3100 struct HiddenTool {
3101 spec: ToolSpec,
3102 }
3103
3104 impl HiddenTool {
3105 fn new() -> Self {
3106 Self {
3107 spec: ToolSpec {
3108 name: ToolName::new("hidden"),
3109 description: "hidden".into(),
3110 input_schema: json!({"type": "object"}),
3111 annotations: ToolAnnotations::default(),
3112 metadata: MetadataMap::new(),
3113 },
3114 }
3115 }
3116 }
3117
3118 #[async_trait]
3119 impl Tool for HiddenTool {
3120 fn spec(&self) -> &ToolSpec {
3121 &self.spec
3122 }
3123
3124 fn current_spec(&self) -> Option<ToolSpec> {
3125 None
3126 }
3127
3128 async fn invoke(
3129 &self,
3130 request: ToolRequest,
3131 _ctx: &mut ToolContext<'_>,
3132 ) -> Result<ToolResult, ToolError> {
3133 Ok(ToolResult {
3134 result: ToolResultPart {
3135 call_id: request.call_id,
3136 output: ToolOutput::Text("hidden".into()),
3137 is_error: false,
3138 metadata: MetadataMap::new(),
3139 },
3140 duration: None,
3141 metadata: MetadataMap::new(),
3142 })
3143 }
3144 }
3145
3146 #[test]
3147 fn hidden_tools_are_omitted_from_specs_and_capabilities() {
3148 let registry = ToolRegistry::new().with(HiddenTool::new());
3149
3150 assert!(registry.specs().is_empty());
3151
3152 let provider = ToolCapabilityProvider::from_registry(
3153 ®istry,
3154 Arc::new(AllowAllPermissionChecker),
3155 Arc::new(()),
3156 );
3157 assert!(provider.invocables().is_empty());
3158 }
3159
3160 struct AllowAllPermissionChecker;
3161
3162 impl PermissionChecker for AllowAllPermissionChecker {
3163 fn evaluate(&self, _request: &dyn PermissionRequest) -> PermissionDecision {
3164 PermissionDecision::Allow
3165 }
3166 }
3167
3168 #[derive(Clone)]
3171 struct PanickingSpecTool {
3172 spec: ToolSpec,
3173 }
3174
3175 impl PanickingSpecTool {
3176 fn new(name: &str) -> Self {
3177 Self {
3178 spec: ToolSpec {
3179 name: ToolName::new(name),
3180 description: "panics on current_spec".into(),
3181 input_schema: json!({"type": "object"}),
3182 annotations: ToolAnnotations::default(),
3183 metadata: MetadataMap::new(),
3184 },
3185 }
3186 }
3187 }
3188
3189 #[async_trait]
3190 impl Tool for PanickingSpecTool {
3191 fn spec(&self) -> &ToolSpec {
3192 &self.spec
3193 }
3194
3195 fn current_spec(&self) -> Option<ToolSpec> {
3196 panic!("PanickingSpecTool::current_spec");
3197 }
3198
3199 async fn invoke(
3200 &self,
3201 request: ToolRequest,
3202 _ctx: &mut ToolContext<'_>,
3203 ) -> Result<ToolResult, ToolError> {
3204 Ok(ToolResult {
3205 result: ToolResultPart {
3206 call_id: request.call_id,
3207 output: ToolOutput::Text("never".into()),
3208 is_error: false,
3209 metadata: MetadataMap::new(),
3210 },
3211 duration: None,
3212 metadata: MetadataMap::new(),
3213 })
3214 }
3215 }
3216
3217 #[test]
3228 fn catalog_recovers_from_panicked_writer() {
3229 let (writer, reader) = dynamic_catalog("test");
3230
3231 writer.upsert(Arc::new(PanickingSpecTool::new("boom")));
3235 let _ = reader.drain_catalog_events();
3236
3237 let panic_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3242 writer.replace_all(vec![
3243 Arc::new(PanickingSpecTool::new("boom")) as Arc<dyn Tool>
3244 ]);
3245 }));
3246 assert!(
3247 panic_result.is_err(),
3248 "PanickingSpecTool::current_spec must propagate"
3249 );
3250
3251 assert!(
3255 reader.get(&ToolName::new("boom")).is_some(),
3256 "catalog still readable after poisoning panic"
3257 );
3258
3259 assert!(writer.remove(&ToolName::new("boom")));
3262
3263 writer.upsert(Arc::new(HiddenTool::new()));
3267 assert!(
3268 reader.get(&ToolName::new("hidden")).is_some(),
3269 "catalog usable for further writes + reads"
3270 );
3271 }
3272
3273 #[derive(Clone)]
3274 struct EchoTool {
3275 spec: ToolSpec,
3276 }
3277
3278 impl EchoTool {
3279 fn new(name: &str) -> Self {
3280 Self {
3281 spec: ToolSpec {
3282 name: ToolName::new(name),
3283 description: format!("echo {name}"),
3284 input_schema: json!({"type": "object"}),
3285 annotations: ToolAnnotations::default(),
3286 metadata: MetadataMap::new(),
3287 },
3288 }
3289 }
3290 }
3291
3292 #[async_trait]
3293 impl Tool for EchoTool {
3294 fn spec(&self) -> &ToolSpec {
3295 &self.spec
3296 }
3297
3298 async fn invoke(
3299 &self,
3300 request: ToolRequest,
3301 _ctx: &mut ToolContext<'_>,
3302 ) -> Result<ToolResult, ToolError> {
3303 Ok(ToolResult::new(ToolResultPart::success(
3304 request.call_id,
3305 ToolOutput::text(request.tool_name.0.clone()),
3306 )))
3307 }
3308 }
3309
3310 fn registry_with(names: &[&str]) -> ToolRegistry {
3311 names.iter().fold(ToolRegistry::new(), |reg, name| {
3312 reg.with(EchoTool::new(name))
3313 })
3314 }
3315
3316 #[test]
3317 fn prefixed_rewrites_specs_and_resolves_lookups() {
3318 let source = registry_with(&["get_temp", "get_humidity"]).prefixed("weather");
3319 let names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
3320 assert_eq!(names, vec!["weather_get_humidity", "weather_get_temp"]);
3321
3322 assert!(source.get(&ToolName::new("weather_get_temp")).is_some());
3323 assert!(
3324 source.get(&ToolName::new("get_temp")).is_none(),
3325 "original name must not resolve when prefixed"
3326 );
3327 assert!(source.get(&ToolName::new("unknown")).is_none());
3328 }
3329
3330 #[tokio::test]
3331 async fn prefixed_invoke_sees_inner_name_on_request() {
3332 let source = registry_with(&["get_temp"]).prefixed("weather");
3333 let tool = source.get(&ToolName::new("weather_get_temp")).unwrap();
3334
3335 assert_eq!(tool.spec().name.0, "weather_get_temp");
3337
3338 let owned = OwnedToolContext {
3340 session_id: SessionId::new("s"),
3341 turn_id: TurnId::new("t"),
3342 metadata: MetadataMap::new(),
3343 permissions: Arc::new(AllowAllPermissions),
3344 resources: Arc::new(()),
3345 cancellation: None,
3346 };
3347 let mut ctx = owned.borrowed();
3348 let request = ToolRequest {
3349 call_id: ToolCallId::new("c"),
3350 tool_name: ToolName::new("weather_get_temp"),
3351 input: json!({}),
3352 session_id: SessionId::new("s"),
3353 turn_id: TurnId::new("t"),
3354 metadata: MetadataMap::new(),
3355 };
3356 let result = tool.invoke(request, &mut ctx).await.unwrap();
3357 match result.result.output {
3358 ToolOutput::Text(text) => assert_eq!(text, "get_temp"),
3359 other => panic!("unexpected output: {other:?}"),
3360 }
3361 }
3362
3363 #[test]
3364 fn filtered_hides_tools_rejected_by_predicate() {
3365 let source = registry_with(&["safe", "danger_drop", "danger_delete"])
3366 .filtered(|name| !name.0.starts_with("danger_"));
3367 let names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
3368 assert_eq!(names, vec!["safe"]);
3369
3370 assert!(source.get(&ToolName::new("safe")).is_some());
3371 assert!(source.get(&ToolName::new("danger_drop")).is_none());
3372 }
3373
3374 #[test]
3375 fn renamed_remaps_specs_and_lookups() {
3376 let source = registry_with(&["legacy_name", "passthrough"])
3377 .renamed([(ToolName::new("legacy_name"), ToolName::new("modern_name"))]);
3378 let mut names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
3379 names.sort();
3380 assert_eq!(names, vec!["modern_name", "passthrough"]);
3381
3382 assert!(source.get(&ToolName::new("modern_name")).is_some());
3383 assert!(
3384 source.get(&ToolName::new("legacy_name")).is_none(),
3385 "original name is hidden after renaming"
3386 );
3387 assert!(source.get(&ToolName::new("passthrough")).is_some());
3388 }
3389
3390 #[cfg(feature = "schemars")]
3391 mod schemars_helpers {
3392 use super::*;
3393 use schemars::JsonSchema;
3394 use serde::Deserialize;
3395
3396 #[derive(JsonSchema, Deserialize)]
3397 #[allow(dead_code)]
3398 struct WeatherInput {
3399 location: String,
3401 #[serde(default)]
3403 celsius: bool,
3404 }
3405
3406 #[test]
3407 fn schema_for_emits_object_schema_with_typed_fields() {
3408 let schema = schema_for::<WeatherInput>();
3409 let obj = schema.as_object().expect("schema is a JSON object");
3410 assert_eq!(
3411 obj.get("type").and_then(|v| v.as_str()),
3412 Some("object"),
3413 "root type should be object"
3414 );
3415 let properties = obj
3416 .get("properties")
3417 .and_then(|v| v.as_object())
3418 .expect("properties block");
3419 assert!(properties.contains_key("location"));
3420 assert!(properties.contains_key("celsius"));
3421 }
3422
3423 #[test]
3424 fn tool_spec_for_carries_schema_name_and_description() {
3425 let spec = tool_spec_for::<WeatherInput>("get_weather", "Fetch current weather");
3426 assert_eq!(spec.name.0, "get_weather");
3427 assert_eq!(spec.description, "Fetch current weather");
3428 assert!(spec.input_schema.is_object());
3429 }
3430 }
3431
3432 #[test]
3433 fn transforms_compose_via_chained_methods() {
3434 let source = registry_with(&["read_file", "write_file", "delete_file"])
3435 .filtered(|name| name.0 != "delete_file")
3436 .prefixed("fs");
3437 let mut names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
3438 names.sort();
3439 assert_eq!(names, vec!["fs_read_file", "fs_write_file"]);
3440 }
3441}