1use std::any::Any;
22use std::collections::{BTreeMap, BTreeSet};
23use std::fmt;
24use std::path::{Path, PathBuf};
25use std::sync::atomic::{AtomicU64, Ordering};
26use std::sync::{Arc, OnceLock};
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 = std::path::absolute(&self.lexical).unwrap_or_else(|_| self.lexical.clone());
1215 if let Ok(canonical) = std::fs::canonicalize(&abs) {
1216 let _ = self.canonical.set(canonical);
1217 return std::borrow::Cow::Borrowed(self.canonical.get().unwrap());
1218 }
1219 std::borrow::Cow::Owned(canonicalize_with_partial_fallback(&abs).unwrap_or(abs))
1220 }
1221}
1222
1223impl PermissionPolicy for PathPolicy {
1224 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1225 let Some(fs) = request
1226 .as_any()
1227 .downcast_ref::<FileSystemPermissionRequest>()
1228 else {
1229 return PolicyMatch::NoOpinion;
1230 };
1231
1232 let raw_paths: Vec<&Path> = match fs {
1233 FileSystemPermissionRequest::Move { from, to, .. } => {
1234 vec![from.as_path(), to.as_path()]
1235 }
1236 FileSystemPermissionRequest::Read { path, .. }
1237 | FileSystemPermissionRequest::Write { path, .. }
1238 | FileSystemPermissionRequest::Edit { path, .. }
1239 | FileSystemPermissionRequest::Delete { path, .. }
1240 | FileSystemPermissionRequest::List { path, .. }
1241 | FileSystemPermissionRequest::CreateDir { path, .. } => vec![path.as_path()],
1242 };
1243
1244 let candidate_paths: Vec<PathBuf> =
1245 raw_paths.iter().map(|p| resolve_canonical(p)).collect();
1246
1247 let mutates = matches!(
1248 fs,
1249 FileSystemPermissionRequest::Write { .. }
1250 | FileSystemPermissionRequest::Edit { .. }
1251 | FileSystemPermissionRequest::Delete { .. }
1252 | FileSystemPermissionRequest::Move { .. }
1253 | FileSystemPermissionRequest::CreateDir { .. }
1254 );
1255
1256 if candidate_paths.iter().any(|path| {
1257 self.protected_roots
1258 .iter()
1259 .any(|root| path.starts_with(root.resolve().as_ref()))
1260 }) {
1261 return PolicyMatch::Deny(PermissionDenial {
1262 code: PermissionCode::PathNotAllowed,
1263 message: format!("path access denied for {}", fs.summary()),
1264 metadata: fs.metadata().clone(),
1265 });
1266 }
1267
1268 if mutates
1269 && candidate_paths.iter().any(|path| {
1270 self.read_only_roots
1271 .iter()
1272 .any(|root| path.starts_with(root.resolve().as_ref()))
1273 })
1274 {
1275 return PolicyMatch::Deny(PermissionDenial {
1276 code: PermissionCode::PathNotAllowed,
1277 message: format!("path is read-only for {}", fs.summary()),
1278 metadata: fs.metadata().clone(),
1279 });
1280 }
1281
1282 if self.allowed_roots.is_empty() {
1283 return PolicyMatch::NoOpinion;
1284 }
1285
1286 let all_allowed = candidate_paths.iter().all(|path| {
1287 self.allowed_roots
1288 .iter()
1289 .any(|root| path.starts_with(root.resolve().as_ref()))
1290 });
1291
1292 if all_allowed {
1293 PolicyMatch::Allow
1294 } else if self.require_approval_outside_allowed {
1295 PolicyMatch::RequireApproval(ApprovalRequest {
1296 task_id: None,
1297 call_id: None,
1298 id: ApprovalId::new(format!("approval:{}", fs.kind())),
1299 request_kind: fs.kind().to_string(),
1300 reason: ApprovalReason::SensitivePath,
1301 summary: fs.summary(),
1302 metadata: fs.metadata().clone(),
1303 })
1304 } else {
1305 PolicyMatch::Deny(PermissionDenial {
1306 code: PermissionCode::PathNotAllowed,
1307 message: format!("path outside allowed roots for {}", fs.summary()),
1308 metadata: fs.metadata().clone(),
1309 })
1310 }
1311 }
1312}
1313
1314pub struct CommandPolicy {
1335 allowed_executables: BTreeSet<String>,
1336 denied_executables: BTreeSet<String>,
1337 allowed_cwds: Vec<PathBuf>,
1338 denied_env_keys: BTreeSet<String>,
1339 require_approval_for_unknown: bool,
1340}
1341
1342impl CommandPolicy {
1343 pub fn new() -> Self {
1346 Self {
1347 allowed_executables: BTreeSet::new(),
1348 denied_executables: BTreeSet::new(),
1349 allowed_cwds: Vec::new(),
1350 denied_env_keys: BTreeSet::new(),
1351 require_approval_for_unknown: true,
1352 }
1353 }
1354
1355 pub fn allow_executable(mut self, executable: impl Into<String>) -> Self {
1357 self.allowed_executables.insert(executable.into());
1358 self
1359 }
1360
1361 pub fn deny_executable(mut self, executable: impl Into<String>) -> Self {
1363 self.denied_executables.insert(executable.into());
1364 self
1365 }
1366
1367 pub fn allow_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
1369 self.allowed_cwds.push(cwd.into());
1370 self
1371 }
1372
1373 pub fn deny_env_key(mut self, key: impl Into<String>) -> Self {
1375 self.denied_env_keys.insert(key.into());
1376 self
1377 }
1378
1379 pub fn require_approval_for_unknown(mut self, value: bool) -> Self {
1382 self.require_approval_for_unknown = value;
1383 self
1384 }
1385}
1386
1387impl Default for CommandPolicy {
1388 fn default() -> Self {
1389 Self::new()
1390 }
1391}
1392
1393impl PermissionPolicy for CommandPolicy {
1394 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1395 let Some(shell) = request.as_any().downcast_ref::<ShellPermissionRequest>() else {
1396 return PolicyMatch::NoOpinion;
1397 };
1398
1399 if self.denied_executables.contains(&shell.executable)
1400 || shell
1401 .env_keys
1402 .iter()
1403 .any(|key| self.denied_env_keys.contains(key))
1404 {
1405 return PolicyMatch::Deny(PermissionDenial {
1406 code: PermissionCode::CommandNotAllowed,
1407 message: format!("command denied for {}", shell.summary()),
1408 metadata: shell.metadata().clone(),
1409 });
1410 }
1411
1412 if let Some(cwd) = &shell.cwd
1413 && !self.allowed_cwds.is_empty()
1414 && !self.allowed_cwds.iter().any(|root| cwd.starts_with(root))
1415 {
1416 return PolicyMatch::RequireApproval(ApprovalRequest {
1417 task_id: None,
1418 call_id: None,
1419 id: ApprovalId::new("approval:shell.cwd"),
1420 request_kind: shell.kind().to_string(),
1421 reason: ApprovalReason::SensitiveCommand,
1422 summary: shell.summary(),
1423 metadata: shell.metadata().clone(),
1424 });
1425 }
1426
1427 if self.allowed_executables.is_empty()
1428 || self.allowed_executables.contains(&shell.executable)
1429 {
1430 PolicyMatch::Allow
1431 } else if self.require_approval_for_unknown {
1432 PolicyMatch::RequireApproval(ApprovalRequest {
1433 task_id: None,
1434 call_id: None,
1435 id: ApprovalId::new("approval:shell.command"),
1436 request_kind: shell.kind().to_string(),
1437 reason: ApprovalReason::SensitiveCommand,
1438 summary: shell.summary(),
1439 metadata: shell.metadata().clone(),
1440 })
1441 } else {
1442 PolicyMatch::Deny(PermissionDenial {
1443 code: PermissionCode::CommandNotAllowed,
1444 message: format!("executable {} is not allowed", shell.executable),
1445 metadata: shell.metadata().clone(),
1446 })
1447 }
1448 }
1449}
1450
1451pub struct McpServerPolicy {
1465 trusted_servers: BTreeSet<String>,
1466 allowed_auth_scopes: BTreeSet<String>,
1467 require_approval_for_untrusted: bool,
1468}
1469
1470impl McpServerPolicy {
1471 pub fn new() -> Self {
1474 Self {
1475 trusted_servers: BTreeSet::new(),
1476 allowed_auth_scopes: BTreeSet::new(),
1477 require_approval_for_untrusted: true,
1478 }
1479 }
1480
1481 pub fn trust_server(mut self, server_id: impl Into<String>) -> Self {
1483 self.trusted_servers.insert(server_id.into());
1484 self
1485 }
1486
1487 pub fn allow_auth_scope(mut self, scope: impl Into<String>) -> Self {
1489 self.allowed_auth_scopes.insert(scope.into());
1490 self
1491 }
1492}
1493
1494impl Default for McpServerPolicy {
1495 fn default() -> Self {
1496 Self::new()
1497 }
1498}
1499
1500impl PermissionPolicy for McpServerPolicy {
1501 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1502 let Some(mcp) = request.as_any().downcast_ref::<McpPermissionRequest>() else {
1503 return PolicyMatch::NoOpinion;
1504 };
1505
1506 let server_id = match mcp {
1507 McpPermissionRequest::Connect { server_id, .. }
1508 | McpPermissionRequest::InvokeTool { server_id, .. }
1509 | McpPermissionRequest::ReadResource { server_id, .. }
1510 | McpPermissionRequest::FetchPrompt { server_id, .. }
1511 | McpPermissionRequest::UseAuthScope { server_id, .. } => server_id,
1512 };
1513
1514 if !self.trusted_servers.is_empty() && !self.trusted_servers.contains(server_id) {
1515 return if self.require_approval_for_untrusted {
1516 PolicyMatch::RequireApproval(ApprovalRequest {
1517 task_id: None,
1518 call_id: None,
1519 id: ApprovalId::new(format!("approval:mcp:{server_id}")),
1520 request_kind: mcp.kind().to_string(),
1521 reason: ApprovalReason::SensitiveServer,
1522 summary: mcp.summary(),
1523 metadata: mcp.metadata().clone(),
1524 })
1525 } else {
1526 PolicyMatch::Deny(PermissionDenial {
1527 code: PermissionCode::ServerNotTrusted,
1528 message: format!("MCP server {server_id} is not trusted"),
1529 metadata: mcp.metadata().clone(),
1530 })
1531 };
1532 }
1533
1534 if let McpPermissionRequest::UseAuthScope { scope, .. } = mcp
1535 && !self.allowed_auth_scopes.is_empty()
1536 && !self.allowed_auth_scopes.contains(scope)
1537 {
1538 return PolicyMatch::Deny(PermissionDenial {
1539 code: PermissionCode::AuthScopeNotAllowed,
1540 message: format!("MCP auth scope {scope} is not allowed"),
1541 metadata: mcp.metadata().clone(),
1542 });
1543 }
1544
1545 PolicyMatch::Allow
1546 }
1547}
1548
1549#[async_trait]
1601pub trait Tool: Send + Sync {
1602 fn spec(&self) -> &ToolSpec;
1604
1605 fn current_spec(&self) -> Option<ToolSpec> {
1613 Some(self.spec().clone())
1614 }
1615
1616 fn proposed_requests(
1628 &self,
1629 _request: &ToolRequest,
1630 ) -> Result<Vec<Box<dyn PermissionRequest>>, ToolError> {
1631 Ok(Vec::new())
1632 }
1633
1634 async fn invoke(
1643 &self,
1644 request: ToolRequest,
1645 ctx: &mut ToolContext<'_>,
1646 ) -> Result<ToolResult, ToolError>;
1647}
1648
1649#[derive(Clone, Default)]
1680pub struct ToolRegistry {
1681 tools: BTreeMap<ToolName, Arc<dyn Tool>>,
1682}
1683
1684impl ToolRegistry {
1685 pub fn new() -> Self {
1687 Self::default()
1688 }
1689
1690 pub fn register<T>(&mut self, tool: T) -> &mut Self
1692 where
1693 T: Tool + 'static,
1694 {
1695 self.tools.insert(tool.spec().name.clone(), Arc::new(tool));
1696 self
1697 }
1698
1699 pub fn with<T>(mut self, tool: T) -> Self
1701 where
1702 T: Tool + 'static,
1703 {
1704 self.register(tool);
1705 self
1706 }
1707
1708 pub fn register_arc(&mut self, tool: Arc<dyn Tool>) -> &mut Self {
1710 self.tools.insert(tool.spec().name.clone(), tool);
1711 self
1712 }
1713
1714 pub fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1716 self.tools.get(name).cloned()
1717 }
1718
1719 pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
1721 self.tools.values().cloned().collect()
1722 }
1723
1724 pub fn merge(mut self, other: Self) -> Self {
1733 self.tools.extend(other.tools);
1734 self
1735 }
1736
1737 pub fn specs(&self) -> Vec<ToolSpec> {
1739 self.tools
1740 .values()
1741 .filter_map(|tool| tool.current_spec())
1742 .collect()
1743 }
1744}
1745
1746pub trait ToolSource: Send + Sync {
1754 fn specs(&self) -> Vec<ToolSpec>;
1756
1757 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>>;
1759
1760 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
1764 Vec::new()
1765 }
1766
1767 fn prefixed(self, prefix: impl Into<String>) -> Prefixed<Self>
1777 where
1778 Self: Sized,
1779 {
1780 Prefixed::new(self, prefix)
1781 }
1782
1783 fn filtered<F>(self, predicate: F) -> Filtered<Self, F>
1789 where
1790 Self: Sized,
1791 F: Fn(&ToolName) -> bool + Send + Sync + 'static,
1792 {
1793 Filtered::new(self, predicate)
1794 }
1795
1796 fn renamed<I>(self, mapping: I) -> Renamed<Self>
1803 where
1804 Self: Sized,
1805 I: IntoIterator<Item = (ToolName, ToolName)>,
1806 {
1807 Renamed::new(self, mapping)
1808 }
1809}
1810
1811impl ToolSource for ToolRegistry {
1812 fn specs(&self) -> Vec<ToolSpec> {
1813 ToolRegistry::specs(self)
1814 }
1815
1816 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1817 ToolRegistry::get(self, name)
1818 }
1819}
1820
1821impl<S> ToolSource for Arc<S>
1822where
1823 S: ToolSource + ?Sized,
1824{
1825 fn specs(&self) -> Vec<ToolSpec> {
1826 (**self).specs()
1827 }
1828
1829 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1830 (**self).get(name)
1831 }
1832
1833 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
1834 (**self).drain_catalog_events()
1835 }
1836}
1837
1838pub struct Prefixed<S> {
1841 inner: S,
1842 prefix: String,
1843}
1844
1845impl<S> Prefixed<S> {
1846 pub fn new(inner: S, prefix: impl Into<String>) -> Self {
1848 Self {
1849 inner,
1850 prefix: prefix.into(),
1851 }
1852 }
1853
1854 fn rewrite(&self, name: &str) -> String {
1855 format!("{}_{}", self.prefix, name)
1856 }
1857
1858 fn strip<'a>(&self, name: &'a str) -> Option<&'a str> {
1859 name.strip_prefix(self.prefix.as_str())
1860 .and_then(|rest| rest.strip_prefix('_'))
1861 }
1862}
1863
1864impl<S> ToolSource for Prefixed<S>
1865where
1866 S: ToolSource,
1867{
1868 fn specs(&self) -> Vec<ToolSpec> {
1869 self.inner
1870 .specs()
1871 .into_iter()
1872 .map(|mut spec| {
1873 spec.name = ToolName::new(self.rewrite(spec.name.0.as_str()));
1874 spec
1875 })
1876 .collect()
1877 }
1878
1879 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1880 let original = self.strip(name.0.as_str())?;
1881 let inner_name = ToolName::new(original);
1882 let inner_tool = self.inner.get(&inner_name)?;
1883 let mut public_spec = inner_tool.spec().clone();
1884 public_spec.name = name.clone();
1885 Some(Arc::new(RewrittenTool {
1886 inner: inner_tool,
1887 inner_name,
1888 public_spec,
1889 }))
1890 }
1891
1892 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
1893 self.inner
1894 .drain_catalog_events()
1895 .into_iter()
1896 .map(|mut event| {
1897 event.for_each_name_mut(|name| *name = self.rewrite(name.as_str()));
1898 event
1899 })
1900 .collect()
1901 }
1902}
1903
1904pub struct Filtered<S, F> {
1907 inner: S,
1908 predicate: F,
1909}
1910
1911impl<S, F> Filtered<S, F> {
1912 pub fn new(inner: S, predicate: F) -> Self {
1914 Self { inner, predicate }
1915 }
1916}
1917
1918impl<S, F> ToolSource for Filtered<S, F>
1919where
1920 S: ToolSource,
1921 F: Fn(&ToolName) -> bool + Send + Sync + 'static,
1922{
1923 fn specs(&self) -> Vec<ToolSpec> {
1924 self.inner
1925 .specs()
1926 .into_iter()
1927 .filter(|spec| (self.predicate)(&spec.name))
1928 .collect()
1929 }
1930
1931 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1932 if !(self.predicate)(name) {
1933 return None;
1934 }
1935 self.inner.get(name)
1936 }
1937
1938 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
1939 self.inner
1940 .drain_catalog_events()
1941 .into_iter()
1942 .map(|mut event| {
1943 event.retain_names(|n| (self.predicate)(&ToolName::new(n)));
1944 event
1945 })
1946 .collect()
1947 }
1948}
1949
1950pub struct Renamed<S> {
1957 inner: S,
1958 forward: BTreeMap<ToolName, ToolName>,
1959 backward: BTreeMap<ToolName, ToolName>,
1960}
1961
1962impl<S> Renamed<S> {
1963 pub fn new<I>(inner: S, mapping: I) -> Self
1965 where
1966 I: IntoIterator<Item = (ToolName, ToolName)>,
1967 {
1968 let forward: BTreeMap<ToolName, ToolName> = mapping.into_iter().collect();
1969 let backward = forward
1970 .iter()
1971 .map(|(k, v)| (v.clone(), k.clone()))
1972 .collect();
1973 Self {
1974 inner,
1975 forward,
1976 backward,
1977 }
1978 }
1979}
1980
1981impl<S> ToolSource for Renamed<S>
1982where
1983 S: ToolSource,
1984{
1985 fn specs(&self) -> Vec<ToolSpec> {
1986 self.inner
1987 .specs()
1988 .into_iter()
1989 .map(|mut spec| {
1990 if let Some(new_name) = self.forward.get(&spec.name) {
1991 spec.name = new_name.clone();
1992 }
1993 spec
1994 })
1995 .collect()
1996 }
1997
1998 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
1999 if let Some(original) = self.backward.get(name) {
2000 let inner_tool = self.inner.get(original)?;
2001 let mut public_spec = inner_tool.spec().clone();
2002 public_spec.name = name.clone();
2003 Some(Arc::new(RewrittenTool {
2004 inner: inner_tool,
2005 inner_name: original.clone(),
2006 public_spec,
2007 }))
2008 } else if self.forward.contains_key(name) {
2009 None
2011 } else {
2012 self.inner.get(name)
2013 }
2014 }
2015
2016 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2017 self.inner
2018 .drain_catalog_events()
2019 .into_iter()
2020 .map(|mut event| {
2021 event.for_each_name_mut(|name| {
2022 if let Some(new) = self.forward.get(&ToolName::new(name.as_str())) {
2023 *name = new.0.clone();
2024 }
2025 });
2026 event
2027 })
2028 .collect()
2029 }
2030}
2031
2032#[cfg(feature = "schemars")]
2058pub fn schema_for<T: schemars::JsonSchema>() -> Value {
2059 let schema = schemars::schema_for!(T);
2060 serde_json::to_value(schema)
2061 .expect("schemars produces valid JSON; this conversion is infallible")
2062}
2063
2064#[cfg(feature = "schemars")]
2082pub fn tool_spec_for<T: schemars::JsonSchema>(
2083 name: impl Into<ToolName>,
2084 description: impl Into<String>,
2085) -> ToolSpec {
2086 ToolSpec::new(name, description, schema_for::<T>())
2087}
2088
2089struct RewrittenTool {
2095 inner: Arc<dyn Tool>,
2096 inner_name: ToolName,
2097 public_spec: ToolSpec,
2098}
2099
2100#[async_trait]
2101impl Tool for RewrittenTool {
2102 fn spec(&self) -> &ToolSpec {
2103 &self.public_spec
2104 }
2105
2106 fn current_spec(&self) -> Option<ToolSpec> {
2107 let inner_current = self.inner.current_spec()?;
2108 Some(ToolSpec {
2109 name: self.public_spec.name.clone(),
2110 description: inner_current.description,
2111 input_schema: inner_current.input_schema,
2112 annotations: inner_current.annotations,
2113 metadata: inner_current.metadata,
2114 })
2115 }
2116
2117 fn proposed_requests(
2118 &self,
2119 request: &ToolRequest,
2120 ) -> Result<Vec<Box<dyn PermissionRequest>>, ToolError> {
2121 let mut inner_request = request.clone();
2122 inner_request.tool_name = self.inner_name.clone();
2123 self.inner.proposed_requests(&inner_request)
2124 }
2125
2126 async fn invoke(
2127 &self,
2128 mut request: ToolRequest,
2129 ctx: &mut ToolContext<'_>,
2130 ) -> Result<ToolResult, ToolError> {
2131 request.tool_name = self.inner_name.clone();
2132 self.inner.invoke(request, ctx).await
2133 }
2134}
2135
2136struct ToolMap {
2154 inner: std::sync::RwLock<BTreeMap<ToolName, Arc<dyn Tool>>>,
2155}
2156
2157impl ToolMap {
2158 fn new() -> Self {
2159 Self {
2160 inner: std::sync::RwLock::new(BTreeMap::new()),
2161 }
2162 }
2163
2164 fn read(&self) -> std::sync::RwLockReadGuard<'_, BTreeMap<ToolName, Arc<dyn Tool>>> {
2165 self.inner.read().unwrap_or_else(|e| e.into_inner())
2166 }
2167
2168 fn write(&self) -> std::sync::RwLockWriteGuard<'_, BTreeMap<ToolName, Arc<dyn Tool>>> {
2169 self.inner.write().unwrap_or_else(|e| e.into_inner())
2170 }
2171}
2172
2173struct DynamicCatalogInner {
2176 source_id: String,
2177 tools: ToolMap,
2178 events_tx: tokio::sync::broadcast::Sender<ToolCatalogEvent>,
2179}
2180
2181pub fn dynamic_catalog(source_id: impl Into<String>) -> (CatalogWriter, CatalogReader) {
2199 let (events_tx, events_rx) = tokio::sync::broadcast::channel(128);
2200 let inner = Arc::new(DynamicCatalogInner {
2201 source_id: source_id.into(),
2202 tools: ToolMap::new(),
2203 events_tx,
2204 });
2205 (
2206 CatalogWriter {
2207 inner: Arc::clone(&inner),
2208 },
2209 CatalogReader {
2210 inner,
2211 events_rx: std::sync::Mutex::new(events_rx),
2212 },
2213 )
2214}
2215
2216pub struct CatalogWriter {
2223 inner: Arc<DynamicCatalogInner>,
2224}
2225
2226impl CatalogWriter {
2227 pub fn source_id(&self) -> &str {
2229 &self.inner.source_id
2230 }
2231
2232 pub fn reader(&self) -> CatalogReader {
2236 CatalogReader {
2237 inner: Arc::clone(&self.inner),
2238 events_rx: std::sync::Mutex::new(self.inner.events_tx.subscribe()),
2239 }
2240 }
2241
2242 pub fn upsert(&self, tool: Arc<dyn Tool>) {
2245 let name = tool.spec().name.clone();
2246 let mut guard = self.inner.tools.write();
2247 let existed = guard.insert(name.clone(), tool).is_some();
2248 drop(guard);
2249 let mut event = ToolCatalogEvent::new(self.inner.source_id.clone());
2250 if existed {
2251 event.changed.push(name.0);
2252 } else {
2253 event.added.push(name.0);
2254 }
2255 let _ = self.inner.events_tx.send(event);
2256 }
2257
2258 pub fn remove(&self, name: &ToolName) -> bool {
2261 let mut guard = self.inner.tools.write();
2262 let removed = guard.remove(name).is_some();
2263 drop(guard);
2264 if removed {
2265 let mut event = ToolCatalogEvent::new(self.inner.source_id.clone());
2266 event.removed.push(name.0.clone());
2267 let _ = self.inner.events_tx.send(event);
2268 }
2269 removed
2270 }
2271
2272 pub fn replace_all(&self, tools: impl IntoIterator<Item = Arc<dyn Tool>>) {
2275 let new_map: BTreeMap<ToolName, Arc<dyn Tool>> = tools
2276 .into_iter()
2277 .map(|tool| (tool.spec().name.clone(), tool))
2278 .collect();
2279
2280 let mut guard = self.inner.tools.write();
2281 let mut event = ToolCatalogEvent::new(self.inner.source_id.clone());
2282
2283 for (name, new_tool) in new_map.iter() {
2284 match guard.get(name) {
2285 None => event.added.push(name.0.clone()),
2286 Some(existing)
2287 if !Arc::ptr_eq(existing, new_tool)
2288 && existing.current_spec() != new_tool.current_spec() =>
2289 {
2290 event.changed.push(name.0.clone());
2291 }
2292 Some(_) => {}
2293 }
2294 }
2295 for name in guard.keys() {
2296 if !new_map.contains_key(name) {
2297 event.removed.push(name.0.clone());
2298 }
2299 }
2300
2301 *guard = new_map;
2302 drop(guard);
2303
2304 if !event.added.is_empty() || !event.removed.is_empty() || !event.changed.is_empty() {
2305 let _ = self.inner.events_tx.send(event);
2306 }
2307 }
2308
2309 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ToolCatalogEvent> {
2313 self.inner.events_tx.subscribe()
2314 }
2315}
2316
2317pub struct CatalogReader {
2322 inner: Arc<DynamicCatalogInner>,
2323 events_rx: std::sync::Mutex<tokio::sync::broadcast::Receiver<ToolCatalogEvent>>,
2324}
2325
2326impl CatalogReader {
2327 pub fn source_id(&self) -> &str {
2329 &self.inner.source_id
2330 }
2331
2332 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ToolCatalogEvent> {
2335 self.inner.events_tx.subscribe()
2336 }
2337}
2338
2339impl Clone for CatalogReader {
2340 fn clone(&self) -> Self {
2341 Self {
2342 inner: Arc::clone(&self.inner),
2343 events_rx: std::sync::Mutex::new(self.inner.events_tx.subscribe()),
2344 }
2345 }
2346}
2347
2348impl ToolSource for CatalogReader {
2349 fn specs(&self) -> Vec<ToolSpec> {
2350 self.inner
2351 .tools
2352 .read()
2353 .values()
2354 .filter_map(|tool| tool.current_spec())
2355 .collect()
2356 }
2357
2358 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2359 self.inner.tools.read().get(name).cloned()
2360 }
2361
2362 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2363 let mut rx = self.events_rx.lock().unwrap_or_else(|e| e.into_inner());
2368 let mut out = Vec::new();
2369 loop {
2370 match rx.try_recv() {
2371 Ok(event) => out.push(event),
2372 Err(tokio::sync::broadcast::error::TryRecvError::Empty) => break,
2373 Err(tokio::sync::broadcast::error::TryRecvError::Closed) => break,
2374 Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => continue,
2375 }
2376 }
2377 out
2378 }
2379}
2380
2381impl ToolSpec {
2382 pub fn as_invocable_spec(&self) -> InvocableSpec {
2385 InvocableSpec::new(
2386 CapabilityName::new(self.name.0.clone()),
2387 self.description.clone(),
2388 self.input_schema.clone(),
2389 )
2390 .with_metadata(self.metadata.clone())
2391 }
2392}
2393
2394pub struct ToolInvocableAdapter {
2400 spec: InvocableSpec,
2401 tool: Arc<dyn Tool>,
2402 permissions: Arc<dyn PermissionChecker>,
2403 resources: Arc<dyn ToolResources>,
2404 next_call_id: AtomicU64,
2405}
2406
2407impl ToolInvocableAdapter {
2408 pub fn new(
2411 tool: Arc<dyn Tool>,
2412 permissions: Arc<dyn PermissionChecker>,
2413 resources: Arc<dyn ToolResources>,
2414 ) -> Option<Self> {
2415 let spec = tool.current_spec()?.as_invocable_spec();
2416 Some(Self {
2417 spec,
2418 tool,
2419 permissions,
2420 resources,
2421 next_call_id: AtomicU64::new(1),
2422 })
2423 }
2424}
2425
2426#[async_trait]
2427impl Invocable for ToolInvocableAdapter {
2428 fn spec(&self) -> &InvocableSpec {
2429 &self.spec
2430 }
2431
2432 async fn invoke(
2433 &self,
2434 request: InvocableRequest,
2435 ctx: &mut CapabilityContext<'_>,
2436 ) -> Result<InvocableResult, CapabilityError> {
2437 let tool_request = ToolRequest {
2438 call_id: ToolCallId::new(format!(
2439 "tool-call-{}",
2440 self.next_call_id.fetch_add(1, Ordering::Relaxed)
2441 )),
2442 tool_name: self.tool.spec().name.clone(),
2443 input: request.input,
2444 session_id: ctx
2445 .session_id
2446 .cloned()
2447 .unwrap_or_else(|| SessionId::new("capability-session")),
2448 turn_id: ctx
2449 .turn_id
2450 .cloned()
2451 .unwrap_or_else(|| TurnId::new("capability-turn")),
2452 metadata: request.metadata,
2453 };
2454
2455 for permission_request in self
2456 .tool
2457 .proposed_requests(&tool_request)
2458 .map_err(|error| CapabilityError::InvalidInput(error.to_string()))?
2459 {
2460 match self.permissions.evaluate(permission_request.as_ref()) {
2461 PermissionDecision::Allow => {}
2462 PermissionDecision::Deny(denial) => {
2463 return Err(CapabilityError::ExecutionFailed(format!(
2464 "tool permission denied: {denial:?}"
2465 )));
2466 }
2467 PermissionDecision::RequireApproval(req) => {
2468 return Err(CapabilityError::Unavailable(format!(
2469 "tool invocation requires approval: {}",
2470 req.summary
2471 )));
2472 }
2473 }
2474 }
2475
2476 let mut tool_ctx = ToolContext {
2477 capability: CapabilityContext {
2478 session_id: ctx.session_id,
2479 turn_id: ctx.turn_id,
2480 metadata: ctx.metadata,
2481 },
2482 permissions: self.permissions.as_ref(),
2483 resources: self.resources.as_ref(),
2484 cancellation: None,
2485 };
2486
2487 let result = self
2488 .tool
2489 .invoke(tool_request, &mut tool_ctx)
2490 .await
2491 .map_err(|error| CapabilityError::ExecutionFailed(error.to_string()))?;
2492
2493 Ok(InvocableResult {
2494 output: match result.result.output {
2495 ToolOutput::Text(text) => InvocableOutput::Text(text),
2496 ToolOutput::Structured(value) => InvocableOutput::Structured(value),
2497 ToolOutput::Parts(parts) => InvocableOutput::Items(vec![Item {
2498 id: None,
2499 kind: ItemKind::Tool,
2500 parts,
2501 metadata: MetadataMap::new(),
2502 }]),
2503 ToolOutput::Files(files) => {
2504 let parts = files.into_iter().map(Part::File).collect();
2505 InvocableOutput::Items(vec![Item {
2506 id: None,
2507 kind: ItemKind::Tool,
2508 parts,
2509 metadata: MetadataMap::new(),
2510 }])
2511 }
2512 },
2513 metadata: result.metadata,
2514 })
2515 }
2516}
2517
2518pub struct ToolCapabilityProvider {
2524 invocables: Vec<Arc<dyn Invocable>>,
2525}
2526
2527impl ToolCapabilityProvider {
2528 pub fn from_registry(
2531 registry: &ToolRegistry,
2532 permissions: Arc<dyn PermissionChecker>,
2533 resources: Arc<dyn ToolResources>,
2534 ) -> Self {
2535 let invocables = registry
2536 .tools()
2537 .into_iter()
2538 .filter_map(|tool| {
2539 ToolInvocableAdapter::new(tool, permissions.clone(), resources.clone())
2540 .map(|adapter| Arc::new(adapter) as Arc<dyn Invocable>)
2541 })
2542 .collect();
2543
2544 Self { invocables }
2545 }
2546}
2547
2548impl CapabilityProvider for ToolCapabilityProvider {
2549 fn invocables(&self) -> Vec<Arc<dyn Invocable>> {
2550 self.invocables.clone()
2551 }
2552
2553 fn resources(&self) -> Vec<Arc<dyn ResourceProvider>> {
2554 Vec::new()
2555 }
2556
2557 fn prompts(&self) -> Vec<Arc<dyn PromptProvider>> {
2558 Vec::new()
2559 }
2560}
2561
2562#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2568pub enum ToolExecutionOutcome {
2569 Completed(ToolResult),
2571 Interrupted(ToolInterruption),
2573 Failed(ToolError),
2575}
2576
2577#[async_trait]
2585pub trait ToolExecutor: Send + Sync {
2586 fn specs(&self) -> Vec<ToolSpec>;
2588
2589 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2594 Vec::new()
2595 }
2596
2597 async fn execute(
2599 &self,
2600 request: ToolRequest,
2601 ctx: &mut ToolContext<'_>,
2602 ) -> ToolExecutionOutcome;
2603
2604 async fn execute_owned(
2607 &self,
2608 request: ToolRequest,
2609 ctx: OwnedToolContext,
2610 ) -> ToolExecutionOutcome {
2611 let mut borrowed = ctx.borrowed();
2612 self.execute(request, &mut borrowed).await
2613 }
2614
2615 async fn execute_approved(
2621 &self,
2622 request: ToolRequest,
2623 approved_request: &ApprovalRequest,
2624 ctx: &mut ToolContext<'_>,
2625 ) -> ToolExecutionOutcome {
2626 let _ = approved_request;
2627 self.execute(request, ctx).await
2628 }
2629
2630 async fn execute_approved_owned(
2633 &self,
2634 request: ToolRequest,
2635 approved_request: &ApprovalRequest,
2636 ctx: OwnedToolContext,
2637 ) -> ToolExecutionOutcome {
2638 let mut borrowed = ctx.borrowed();
2639 self.execute_approved(request, approved_request, &mut borrowed)
2640 .await
2641 }
2642}
2643
2644#[async_trait]
2645impl<T> ToolExecutor for Arc<T>
2646where
2647 T: ToolExecutor + ?Sized,
2648{
2649 fn specs(&self) -> Vec<ToolSpec> {
2650 (**self).specs()
2651 }
2652
2653 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2654 (**self).drain_catalog_events()
2655 }
2656
2657 async fn execute(
2658 &self,
2659 request: ToolRequest,
2660 ctx: &mut ToolContext<'_>,
2661 ) -> ToolExecutionOutcome {
2662 (**self).execute(request, ctx).await
2663 }
2664
2665 async fn execute_approved(
2666 &self,
2667 request: ToolRequest,
2668 approved_request: &ApprovalRequest,
2669 ctx: &mut ToolContext<'_>,
2670 ) -> ToolExecutionOutcome {
2671 (**self)
2672 .execute_approved(request, approved_request, ctx)
2673 .await
2674 }
2675}
2676
2677#[derive(Clone, Debug, Default, PartialEq, Eq)]
2680pub enum CollisionPolicy {
2681 #[default]
2684 FirstWins,
2685 LastWins,
2687}
2688
2689pub struct BasicToolExecutor {
2708 sources: Vec<Arc<dyn ToolSource>>,
2709 collision: CollisionPolicy,
2710}
2711
2712impl BasicToolExecutor {
2713 pub fn new(sources: impl IntoIterator<Item = Arc<dyn ToolSource>>) -> Self {
2715 Self {
2716 sources: sources.into_iter().collect(),
2717 collision: CollisionPolicy::default(),
2718 }
2719 }
2720
2721 pub fn from_registry(registry: ToolRegistry) -> Self {
2723 Self::new([Arc::new(registry) as Arc<dyn ToolSource>])
2724 }
2725
2726 pub fn with_collision_policy(mut self, policy: CollisionPolicy) -> Self {
2729 self.collision = policy;
2730 self
2731 }
2732
2733 pub fn specs(&self) -> Vec<ToolSpec> {
2736 let mut seen = BTreeSet::new();
2737 let mut out = Vec::new();
2738 let iter: Box<dyn Iterator<Item = &Arc<dyn ToolSource>>> = match self.collision {
2739 CollisionPolicy::FirstWins => Box::new(self.sources.iter()),
2740 CollisionPolicy::LastWins => Box::new(self.sources.iter().rev()),
2741 };
2742 for source in iter {
2743 for spec in source.specs() {
2744 if seen.insert(spec.name.clone()) {
2745 out.push(spec);
2746 }
2747 }
2748 }
2749 out
2750 }
2751
2752 fn lookup(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2753 match self.collision {
2754 CollisionPolicy::FirstWins => self.sources.iter().find_map(|s| s.get(name)),
2755 CollisionPolicy::LastWins => self.sources.iter().rev().find_map(|s| s.get(name)),
2756 }
2757 }
2758
2759 async fn execute_inner(
2760 &self,
2761 request: ToolRequest,
2762 approved_request_id: Option<&ApprovalId>,
2763 ctx: &mut ToolContext<'_>,
2764 ) -> ToolExecutionOutcome {
2765 let Some(tool) = self.lookup(&request.tool_name) else {
2766 return ToolExecutionOutcome::Failed(ToolError::NotFound(request.tool_name));
2767 };
2768
2769 match tool.proposed_requests(&request) {
2770 Ok(requests) => {
2771 for permission_request in requests {
2772 match ctx.permissions.evaluate(permission_request.as_ref()) {
2773 PermissionDecision::Allow => {}
2774 PermissionDecision::Deny(denial) => {
2775 return ToolExecutionOutcome::Failed(ToolError::PermissionDenied(
2776 denial,
2777 ));
2778 }
2779 PermissionDecision::RequireApproval(mut req) => {
2780 req.call_id = Some(request.call_id.clone());
2781 if approved_request_id != Some(&req.id) {
2782 return ToolExecutionOutcome::Interrupted(
2783 ToolInterruption::ApprovalRequired(req),
2784 );
2785 }
2786 }
2787 }
2788 }
2789 }
2790 Err(error) => return ToolExecutionOutcome::Failed(error),
2791 }
2792
2793 match tool.invoke(request, ctx).await {
2794 Ok(result) => ToolExecutionOutcome::Completed(result),
2795 Err(error) => ToolExecutionOutcome::Failed(error),
2796 }
2797 }
2798}
2799
2800#[async_trait]
2801impl ToolExecutor for BasicToolExecutor {
2802 fn specs(&self) -> Vec<ToolSpec> {
2803 BasicToolExecutor::specs(self)
2804 }
2805
2806 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2807 self.sources
2808 .iter()
2809 .flat_map(|s| s.drain_catalog_events())
2810 .collect()
2811 }
2812
2813 async fn execute(
2814 &self,
2815 request: ToolRequest,
2816 ctx: &mut ToolContext<'_>,
2817 ) -> ToolExecutionOutcome {
2818 self.execute_inner(request, None, ctx).await
2819 }
2820
2821 async fn execute_approved(
2822 &self,
2823 request: ToolRequest,
2824 approved_request: &ApprovalRequest,
2825 ctx: &mut ToolContext<'_>,
2826 ) -> ToolExecutionOutcome {
2827 self.execute_inner(request, Some(&approved_request.id), ctx)
2828 .await
2829 }
2830}
2831
2832#[derive(Debug, Error, Clone, PartialEq, Serialize, Deserialize)]
2837pub enum ToolError {
2838 #[error("tool not found: {0}")]
2840 NotFound(ToolName),
2841 #[error("invalid tool input: {0}")]
2843 InvalidInput(String),
2844 #[error("tool permission denied: {0:?}")]
2846 PermissionDenied(PermissionDenial),
2847 #[error("tool execution failed: {0}")]
2849 ExecutionFailed(String),
2850 #[error("tool unavailable: {0}")]
2852 Unavailable(String),
2853 #[error("tool execution cancelled")]
2855 Cancelled,
2856 #[error("internal tool error: {0}")]
2858 Internal(String),
2859}
2860
2861impl ToolError {
2862 pub fn permission_denied(denial: PermissionDenial) -> Self {
2864 Self::PermissionDenied(denial)
2865 }
2866}
2867
2868impl From<PermissionDenial> for ToolError {
2869 fn from(value: PermissionDenial) -> Self {
2870 Self::permission_denied(value)
2871 }
2872}
2873
2874#[cfg(test)]
2875mod tests {
2876 use super::*;
2877 use async_trait::async_trait;
2878 use serde_json::json;
2879
2880 #[test]
2881 fn command_policy_can_deny_unknown_executables_without_approval() {
2882 let policy = CommandPolicy::new()
2883 .allow_executable("pwd")
2884 .require_approval_for_unknown(false);
2885 let request = ShellPermissionRequest {
2886 executable: "rm".into(),
2887 argv: vec!["-rf".into(), "/tmp/demo".into()],
2888 cwd: None,
2889 env_keys: Vec::new(),
2890 metadata: MetadataMap::new(),
2891 };
2892
2893 match policy.evaluate(&request) {
2894 PolicyMatch::Deny(denial) => {
2895 assert_eq!(denial.code, PermissionCode::CommandNotAllowed);
2896 }
2897 other => panic!("unexpected policy match: {other:?}"),
2898 }
2899 }
2900
2901 #[test]
2902 fn path_policy_allows_reads_under_read_only_roots() {
2903 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2904 let request = FileSystemPermissionRequest::Read {
2905 path: PathBuf::from("/workspace/vendor/lib.rs"),
2906 metadata: MetadataMap::new(),
2907 };
2908
2909 match policy.evaluate(&request) {
2910 PolicyMatch::NoOpinion | PolicyMatch::Allow => {}
2911 other => panic!("unexpected policy match: {other:?}"),
2912 }
2913 }
2914
2915 #[test]
2916 fn path_policy_denies_mutations_under_read_only_roots() {
2917 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2918 let request = FileSystemPermissionRequest::Edit {
2919 path: PathBuf::from("/workspace/vendor/lib.rs"),
2920 metadata: MetadataMap::new(),
2921 };
2922
2923 match policy.evaluate(&request) {
2924 PolicyMatch::Deny(denial) => {
2925 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
2926 assert!(denial.message.contains("read-only"));
2927 }
2928 other => panic!("unexpected policy match: {other:?}"),
2929 }
2930 }
2931
2932 #[test]
2933 fn path_policy_denies_moves_into_read_only_roots() {
2934 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
2935 let request = FileSystemPermissionRequest::Move {
2936 from: PathBuf::from("/workspace/src/lib.rs"),
2937 to: PathBuf::from("/workspace/vendor/lib.rs"),
2938 metadata: MetadataMap::new(),
2939 };
2940
2941 match policy.evaluate(&request) {
2942 PolicyMatch::Deny(denial) => {
2943 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
2944 assert!(denial.message.contains("read-only"));
2945 }
2946 other => panic!("unexpected policy match: {other:?}"),
2947 }
2948 }
2949
2950 #[cfg(unix)]
2951 struct SymlinkTmpDir(PathBuf);
2952
2953 #[cfg(unix)]
2954 impl SymlinkTmpDir {
2955 fn new(label: &str) -> Self {
2956 use std::time::{SystemTime, UNIX_EPOCH};
2957 let nanos = SystemTime::now()
2958 .duration_since(UNIX_EPOCH)
2959 .unwrap()
2960 .as_nanos();
2961 let dir = std::env::temp_dir().join(format!(
2962 "agentkit-pathpolicy-{}-{}-{}",
2963 label,
2964 std::process::id(),
2965 nanos
2966 ));
2967 std::fs::create_dir_all(&dir).unwrap();
2968 Self(std::fs::canonicalize(&dir).unwrap())
2971 }
2972
2973 fn path(&self) -> &Path {
2974 &self.0
2975 }
2976 }
2977
2978 #[cfg(unix)]
2979 impl Drop for SymlinkTmpDir {
2980 fn drop(&mut self) {
2981 let _ = std::fs::remove_dir_all(&self.0);
2982 }
2983 }
2984
2985 #[cfg(unix)]
2986 fn assert_path_denied(
2987 policy: &PathPolicy,
2988 request: FileSystemPermissionRequest,
2989 ) -> PermissionDenial {
2990 match policy.evaluate(&request) {
2991 PolicyMatch::Deny(denial) => denial,
2992 other => panic!("expected deny, got: {other:?}"),
2993 }
2994 }
2995
2996 #[cfg(unix)]
2997 #[test]
2998 fn path_policy_blocks_symlink_escape_from_allowed_root() {
2999 let tmp = SymlinkTmpDir::new("allow-escape");
3000 let allowed = tmp.path().join("workspace");
3001 let outside = tmp.path().join("outside");
3002 std::fs::create_dir_all(&allowed).unwrap();
3003 std::fs::create_dir_all(&outside).unwrap();
3004 let secret = outside.join("secret.txt");
3005 std::fs::write(&secret, b"top-secret").unwrap();
3006 let escape = allowed.join("leak");
3007 std::os::unix::fs::symlink(&secret, &escape).unwrap();
3008
3009 let policy = PathPolicy::new()
3010 .allow_root(&allowed)
3011 .require_approval_outside_allowed(false);
3012 let denial = assert_path_denied(
3013 &policy,
3014 FileSystemPermissionRequest::Read {
3015 path: escape,
3016 metadata: MetadataMap::new(),
3017 },
3018 );
3019 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3020 }
3021
3022 #[cfg(unix)]
3023 #[test]
3024 fn path_policy_blocks_symlink_into_protected_root() {
3025 let tmp = SymlinkTmpDir::new("protect-bypass");
3026 let workspace = tmp.path().join("workspace");
3027 std::fs::create_dir_all(&workspace).unwrap();
3028 let secret = workspace.join(".env");
3029 std::fs::write(&secret, b"API_KEY=xxx").unwrap();
3030 let alias = workspace.join("config");
3031 std::os::unix::fs::symlink(&secret, &alias).unwrap();
3032
3033 let policy = PathPolicy::new()
3034 .allow_root(&workspace)
3035 .protect_root(&secret);
3036 let denial = assert_path_denied(
3037 &policy,
3038 FileSystemPermissionRequest::Read {
3039 path: alias,
3040 metadata: MetadataMap::new(),
3041 },
3042 );
3043 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3044 assert!(denial.message.contains("denied"));
3045 }
3046
3047 #[cfg(unix)]
3048 #[test]
3049 fn path_policy_blocks_symlink_write_into_read_only_root() {
3050 let tmp = SymlinkTmpDir::new("readonly-bypass");
3051 let workspace = tmp.path().join("workspace");
3052 let vendor = workspace.join("vendor");
3053 std::fs::create_dir_all(&vendor).unwrap();
3054 let target = vendor.join("lib.rs");
3055 std::fs::write(&target, b"// vendored").unwrap();
3056 let writable_alias = workspace.join("writable");
3057 std::os::unix::fs::symlink(&target, &writable_alias).unwrap();
3058
3059 let policy = PathPolicy::new()
3060 .allow_root(&workspace)
3061 .read_only_root(&vendor);
3062 let denial = assert_path_denied(
3063 &policy,
3064 FileSystemPermissionRequest::Edit {
3065 path: writable_alias,
3066 metadata: MetadataMap::new(),
3067 },
3068 );
3069 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3070 assert!(denial.message.contains("read-only"));
3071 }
3072
3073 #[cfg(unix)]
3074 #[test]
3075 fn path_policy_resolves_symlink_parent_for_nonexistent_leaf() {
3076 let tmp = SymlinkTmpDir::new("create-escape");
3077 let allowed = tmp.path().join("workspace");
3078 let outside = tmp.path().join("outside");
3079 std::fs::create_dir_all(&allowed).unwrap();
3080 std::fs::create_dir_all(&outside).unwrap();
3081 let escape_dir = allowed.join("escape");
3082 std::os::unix::fs::symlink(&outside, &escape_dir).unwrap();
3083 let new_file = escape_dir.join("new.txt");
3084
3085 let policy = PathPolicy::new()
3086 .allow_root(&allowed)
3087 .require_approval_outside_allowed(false);
3088 let denial = assert_path_denied(
3089 &policy,
3090 FileSystemPermissionRequest::Write {
3091 path: new_file,
3092 metadata: MetadataMap::new(),
3093 },
3094 );
3095 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3096 }
3097
3098 #[derive(Clone)]
3099 struct HiddenTool {
3100 spec: ToolSpec,
3101 }
3102
3103 impl HiddenTool {
3104 fn new() -> Self {
3105 Self {
3106 spec: ToolSpec {
3107 name: ToolName::new("hidden"),
3108 description: "hidden".into(),
3109 input_schema: json!({"type": "object"}),
3110 annotations: ToolAnnotations::default(),
3111 metadata: MetadataMap::new(),
3112 },
3113 }
3114 }
3115 }
3116
3117 #[async_trait]
3118 impl Tool for HiddenTool {
3119 fn spec(&self) -> &ToolSpec {
3120 &self.spec
3121 }
3122
3123 fn current_spec(&self) -> Option<ToolSpec> {
3124 None
3125 }
3126
3127 async fn invoke(
3128 &self,
3129 request: ToolRequest,
3130 _ctx: &mut ToolContext<'_>,
3131 ) -> Result<ToolResult, ToolError> {
3132 Ok(ToolResult {
3133 result: ToolResultPart {
3134 call_id: request.call_id,
3135 output: ToolOutput::Text("hidden".into()),
3136 is_error: false,
3137 metadata: MetadataMap::new(),
3138 },
3139 duration: None,
3140 metadata: MetadataMap::new(),
3141 })
3142 }
3143 }
3144
3145 #[test]
3146 fn hidden_tools_are_omitted_from_specs_and_capabilities() {
3147 let registry = ToolRegistry::new().with(HiddenTool::new());
3148
3149 assert!(registry.specs().is_empty());
3150
3151 let provider = ToolCapabilityProvider::from_registry(
3152 ®istry,
3153 Arc::new(AllowAllPermissionChecker),
3154 Arc::new(()),
3155 );
3156 assert!(provider.invocables().is_empty());
3157 }
3158
3159 struct AllowAllPermissionChecker;
3160
3161 impl PermissionChecker for AllowAllPermissionChecker {
3162 fn evaluate(&self, _request: &dyn PermissionRequest) -> PermissionDecision {
3163 PermissionDecision::Allow
3164 }
3165 }
3166
3167 #[derive(Clone)]
3170 struct PanickingSpecTool {
3171 spec: ToolSpec,
3172 }
3173
3174 impl PanickingSpecTool {
3175 fn new(name: &str) -> Self {
3176 Self {
3177 spec: ToolSpec {
3178 name: ToolName::new(name),
3179 description: "panics on current_spec".into(),
3180 input_schema: json!({"type": "object"}),
3181 annotations: ToolAnnotations::default(),
3182 metadata: MetadataMap::new(),
3183 },
3184 }
3185 }
3186 }
3187
3188 #[async_trait]
3189 impl Tool for PanickingSpecTool {
3190 fn spec(&self) -> &ToolSpec {
3191 &self.spec
3192 }
3193
3194 fn current_spec(&self) -> Option<ToolSpec> {
3195 panic!("PanickingSpecTool::current_spec");
3196 }
3197
3198 async fn invoke(
3199 &self,
3200 request: ToolRequest,
3201 _ctx: &mut ToolContext<'_>,
3202 ) -> Result<ToolResult, ToolError> {
3203 Ok(ToolResult {
3204 result: ToolResultPart {
3205 call_id: request.call_id,
3206 output: ToolOutput::Text("never".into()),
3207 is_error: false,
3208 metadata: MetadataMap::new(),
3209 },
3210 duration: None,
3211 metadata: MetadataMap::new(),
3212 })
3213 }
3214 }
3215
3216 #[test]
3227 fn catalog_recovers_from_panicked_writer() {
3228 let (writer, reader) = dynamic_catalog("test");
3229
3230 writer.upsert(Arc::new(PanickingSpecTool::new("boom")));
3234 let _ = reader.drain_catalog_events();
3235
3236 let panic_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3241 writer.replace_all(vec![
3242 Arc::new(PanickingSpecTool::new("boom")) as Arc<dyn Tool>
3243 ]);
3244 }));
3245 assert!(
3246 panic_result.is_err(),
3247 "PanickingSpecTool::current_spec must propagate"
3248 );
3249
3250 assert!(
3254 reader.get(&ToolName::new("boom")).is_some(),
3255 "catalog still readable after poisoning panic"
3256 );
3257
3258 assert!(writer.remove(&ToolName::new("boom")));
3261
3262 writer.upsert(Arc::new(HiddenTool::new()));
3266 assert!(
3267 reader.get(&ToolName::new("hidden")).is_some(),
3268 "catalog usable for further writes + reads"
3269 );
3270 }
3271
3272 #[derive(Clone)]
3273 struct EchoTool {
3274 spec: ToolSpec,
3275 }
3276
3277 impl EchoTool {
3278 fn new(name: &str) -> Self {
3279 Self {
3280 spec: ToolSpec {
3281 name: ToolName::new(name),
3282 description: format!("echo {name}"),
3283 input_schema: json!({"type": "object"}),
3284 annotations: ToolAnnotations::default(),
3285 metadata: MetadataMap::new(),
3286 },
3287 }
3288 }
3289 }
3290
3291 #[async_trait]
3292 impl Tool for EchoTool {
3293 fn spec(&self) -> &ToolSpec {
3294 &self.spec
3295 }
3296
3297 async fn invoke(
3298 &self,
3299 request: ToolRequest,
3300 _ctx: &mut ToolContext<'_>,
3301 ) -> Result<ToolResult, ToolError> {
3302 Ok(ToolResult::new(ToolResultPart::success(
3303 request.call_id,
3304 ToolOutput::text(request.tool_name.0.clone()),
3305 )))
3306 }
3307 }
3308
3309 fn registry_with(names: &[&str]) -> ToolRegistry {
3310 names.iter().fold(ToolRegistry::new(), |reg, name| {
3311 reg.with(EchoTool::new(name))
3312 })
3313 }
3314
3315 #[test]
3316 fn prefixed_rewrites_specs_and_resolves_lookups() {
3317 let source = registry_with(&["get_temp", "get_humidity"]).prefixed("weather");
3318 let names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
3319 assert_eq!(names, vec!["weather_get_humidity", "weather_get_temp"]);
3320
3321 assert!(source.get(&ToolName::new("weather_get_temp")).is_some());
3322 assert!(
3323 source.get(&ToolName::new("get_temp")).is_none(),
3324 "original name must not resolve when prefixed"
3325 );
3326 assert!(source.get(&ToolName::new("unknown")).is_none());
3327 }
3328
3329 #[tokio::test]
3330 async fn prefixed_invoke_sees_inner_name_on_request() {
3331 let source = registry_with(&["get_temp"]).prefixed("weather");
3332 let tool = source.get(&ToolName::new("weather_get_temp")).unwrap();
3333
3334 assert_eq!(tool.spec().name.0, "weather_get_temp");
3336
3337 let owned = OwnedToolContext {
3339 session_id: SessionId::new("s"),
3340 turn_id: TurnId::new("t"),
3341 metadata: MetadataMap::new(),
3342 permissions: Arc::new(AllowAllPermissions),
3343 resources: Arc::new(()),
3344 cancellation: None,
3345 };
3346 let mut ctx = owned.borrowed();
3347 let request = ToolRequest {
3348 call_id: ToolCallId::new("c"),
3349 tool_name: ToolName::new("weather_get_temp"),
3350 input: json!({}),
3351 session_id: SessionId::new("s"),
3352 turn_id: TurnId::new("t"),
3353 metadata: MetadataMap::new(),
3354 };
3355 let result = tool.invoke(request, &mut ctx).await.unwrap();
3356 match result.result.output {
3357 ToolOutput::Text(text) => assert_eq!(text, "get_temp"),
3358 other => panic!("unexpected output: {other:?}"),
3359 }
3360 }
3361
3362 #[test]
3363 fn filtered_hides_tools_rejected_by_predicate() {
3364 let source = registry_with(&["safe", "danger_drop", "danger_delete"])
3365 .filtered(|name| !name.0.starts_with("danger_"));
3366 let names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
3367 assert_eq!(names, vec!["safe"]);
3368
3369 assert!(source.get(&ToolName::new("safe")).is_some());
3370 assert!(source.get(&ToolName::new("danger_drop")).is_none());
3371 }
3372
3373 #[test]
3374 fn renamed_remaps_specs_and_lookups() {
3375 let source = registry_with(&["legacy_name", "passthrough"])
3376 .renamed([(ToolName::new("legacy_name"), ToolName::new("modern_name"))]);
3377 let mut names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
3378 names.sort();
3379 assert_eq!(names, vec!["modern_name", "passthrough"]);
3380
3381 assert!(source.get(&ToolName::new("modern_name")).is_some());
3382 assert!(
3383 source.get(&ToolName::new("legacy_name")).is_none(),
3384 "original name is hidden after renaming"
3385 );
3386 assert!(source.get(&ToolName::new("passthrough")).is_some());
3387 }
3388
3389 #[cfg(feature = "schemars")]
3390 mod schemars_helpers {
3391 use super::*;
3392 use schemars::JsonSchema;
3393 use serde::Deserialize;
3394
3395 #[derive(JsonSchema, Deserialize)]
3396 #[allow(dead_code)]
3397 struct WeatherInput {
3398 location: String,
3400 #[serde(default)]
3402 celsius: bool,
3403 }
3404
3405 #[test]
3406 fn schema_for_emits_object_schema_with_typed_fields() {
3407 let schema = schema_for::<WeatherInput>();
3408 let obj = schema.as_object().expect("schema is a JSON object");
3409 assert_eq!(
3410 obj.get("type").and_then(|v| v.as_str()),
3411 Some("object"),
3412 "root type should be object"
3413 );
3414 let properties = obj
3415 .get("properties")
3416 .and_then(|v| v.as_object())
3417 .expect("properties block");
3418 assert!(properties.contains_key("location"));
3419 assert!(properties.contains_key("celsius"));
3420 }
3421
3422 #[test]
3423 fn tool_spec_for_carries_schema_name_and_description() {
3424 let spec = tool_spec_for::<WeatherInput>("get_weather", "Fetch current weather");
3425 assert_eq!(spec.name.0, "get_weather");
3426 assert_eq!(spec.description, "Fetch current weather");
3427 assert!(spec.input_schema.is_object());
3428 }
3429 }
3430
3431 #[test]
3432 fn transforms_compose_via_chained_methods() {
3433 let source = registry_with(&["read_file", "write_file", "delete_file"])
3434 .filtered(|name| name.0 != "delete_file")
3435 .prefixed("fs");
3436 let mut names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
3437 names.sort();
3438 assert_eq!(names, vec!["fs_read_file", "fs_write_file"]);
3439 }
3440}