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, Mutex, 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, json};
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
162pub const TOOL_OUTPUT_LIMIT_METADATA_KEY: &str = "agentkit.tool_output_limit";
167
168#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum ToolOutputOverflowAction {
173 Fail,
177 InlineClip,
179 StoreForReadback,
183}
184
185#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
187pub struct ToolOutputLimit {
188 pub max_bytes: usize,
190 pub action: ToolOutputOverflowAction,
192}
193
194impl ToolOutputLimit {
195 pub fn fail(max_bytes: usize) -> Self {
197 Self {
198 max_bytes,
199 action: ToolOutputOverflowAction::Fail,
200 }
201 }
202
203 pub fn inline_clip(max_bytes: usize) -> Self {
205 Self {
206 max_bytes,
207 action: ToolOutputOverflowAction::InlineClip,
208 }
209 }
210
211 pub fn store_for_readback(max_bytes: usize) -> Self {
214 Self {
215 max_bytes,
216 action: ToolOutputOverflowAction::StoreForReadback,
217 }
218 }
219
220 fn to_metadata_value(&self) -> Value {
221 serde_json::to_value(self).expect("ToolOutputLimit serializes")
222 }
223
224 fn from_metadata(metadata: &MetadataMap) -> Option<Self> {
225 metadata
226 .get(TOOL_OUTPUT_LIMIT_METADATA_KEY)
227 .and_then(|value| serde_json::from_value(value.clone()).ok())
228 }
229}
230
231#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
257pub struct ToolSpec {
258 pub name: ToolName,
260 pub description: String,
262 pub input_schema: Value,
264 pub annotations: ToolAnnotations,
266 pub metadata: MetadataMap,
268}
269
270#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
276pub struct ToolCatalogEvent {
277 pub source: String,
279 pub added: Vec<String>,
281 pub removed: Vec<String>,
283 pub changed: Vec<String>,
285}
286
287impl ToolCatalogEvent {
288 pub fn new(source: impl Into<String>) -> Self {
290 Self {
291 source: source.into(),
292 added: Vec::new(),
293 removed: Vec::new(),
294 changed: Vec::new(),
295 }
296 }
297
298 pub fn for_each_name_mut(&mut self, mut f: impl FnMut(&mut String)) {
300 for vec in [&mut self.added, &mut self.removed, &mut self.changed] {
301 for name in vec.iter_mut() {
302 f(name);
303 }
304 }
305 }
306
307 pub fn retain_names(&mut self, mut predicate: impl FnMut(&str) -> bool) {
310 self.added.retain(|n| predicate(n));
311 self.removed.retain(|n| predicate(n));
312 self.changed.retain(|n| predicate(n));
313 }
314}
315
316impl ToolSpec {
317 pub fn new(
319 name: impl Into<ToolName>,
320 description: impl Into<String>,
321 input_schema: Value,
322 ) -> Self {
323 Self {
324 name: name.into(),
325 description: description.into(),
326 input_schema,
327 annotations: ToolAnnotations::default(),
328 metadata: MetadataMap::new(),
329 }
330 }
331
332 pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
334 self.annotations = annotations;
335 self
336 }
337
338 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
340 self.metadata = metadata;
341 self
342 }
343
344 pub fn with_output_limit(mut self, limit: ToolOutputLimit) -> Self {
350 self.metadata.insert(
351 TOOL_OUTPUT_LIMIT_METADATA_KEY.to_string(),
352 limit.to_metadata_value(),
353 );
354 self
355 }
356}
357
358#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
364pub struct ToolRequest {
365 pub call_id: ToolCallId,
367 pub tool_name: ToolName,
369 pub input: Value,
371 pub session_id: SessionId,
373 pub turn_id: TurnId,
375 pub metadata: MetadataMap,
377}
378
379impl ToolRequest {
380 pub fn new(
382 call_id: impl Into<ToolCallId>,
383 tool_name: impl Into<ToolName>,
384 input: Value,
385 session_id: impl Into<SessionId>,
386 turn_id: impl Into<TurnId>,
387 ) -> Self {
388 Self {
389 call_id: call_id.into(),
390 tool_name: tool_name.into(),
391 input,
392 session_id: session_id.into(),
393 turn_id: turn_id.into(),
394 metadata: MetadataMap::new(),
395 }
396 }
397
398 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
400 self.metadata = metadata;
401 self
402 }
403}
404
405#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
410pub struct ToolResult {
411 pub result: ToolResultPart,
413 pub duration: Option<Duration>,
415 pub metadata: MetadataMap,
417}
418
419impl ToolResult {
420 pub fn new(result: ToolResultPart) -> Self {
422 Self {
423 result,
424 duration: None,
425 metadata: MetadataMap::new(),
426 }
427 }
428
429 pub fn with_duration(mut self, duration: Duration) -> Self {
431 self.duration = Some(duration);
432 self
433 }
434
435 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
437 self.metadata = metadata;
438 self
439 }
440}
441
442pub trait ToolResources: Send + Sync {
468 fn as_any(&self) -> &dyn Any;
471}
472
473impl ToolResources for () {
474 fn as_any(&self) -> &dyn Any {
475 self
476 }
477}
478
479pub struct ToolContext<'a> {
485 pub capability: CapabilityContext<'a>,
487 pub permissions: &'a dyn PermissionChecker,
489 pub resources: &'a dyn ToolResources,
491 pub cancellation: Option<TurnCancellation>,
493}
494
495#[derive(Clone)]
501pub struct OwnedToolContext {
502 pub session_id: SessionId,
504 pub turn_id: TurnId,
506 pub metadata: MetadataMap,
508 pub permissions: Arc<dyn PermissionChecker>,
510 pub resources: Arc<dyn ToolResources>,
512 pub cancellation: Option<TurnCancellation>,
514}
515
516impl OwnedToolContext {
517 pub fn borrowed(&self) -> ToolContext<'_> {
519 ToolContext {
520 capability: CapabilityContext {
521 session_id: Some(&self.session_id),
522 turn_id: Some(&self.turn_id),
523 metadata: &self.metadata,
524 },
525 permissions: self.permissions.as_ref(),
526 resources: self.resources.as_ref(),
527 cancellation: self.cancellation.clone(),
528 }
529 }
530}
531
532#[derive(Clone, Debug)]
535pub struct ToolOutputTruncationContext {
536 pub tool_name: ToolName,
537 pub call_id: ToolCallId,
538 pub session_id: SessionId,
539 pub turn_id: TurnId,
540 pub tool_spec: ToolSpec,
541}
542
543impl From<(&ToolRequest, ToolSpec)> for ToolOutputTruncationContext {
544 fn from((request, tool_spec): (&ToolRequest, ToolSpec)) -> Self {
545 Self {
546 tool_name: request.tool_name.clone(),
547 call_id: request.call_id.clone(),
548 session_id: request.session_id.clone(),
549 turn_id: request.turn_id.clone(),
550 tool_spec,
551 }
552 }
553}
554
555#[async_trait]
560pub trait ToolOutputTruncationStrategy: Send + Sync {
561 async fn apply(
562 &self,
563 ctx: ToolOutputTruncationContext,
564 output: ToolOutput,
565 ) -> Result<ToolOutput, ToolError>;
566}
567
568#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
570pub struct ToolOutputArtifactId(pub String);
571
572impl fmt::Display for ToolOutputArtifactId {
573 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
574 self.0.fmt(f)
575 }
576}
577
578#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
580pub struct ToolOutputArtifact {
581 pub id: ToolOutputArtifactId,
582 pub tool_name: ToolName,
583 pub call_id: ToolCallId,
584 pub session_id: SessionId,
585 pub turn_id: TurnId,
586 pub original_bytes: usize,
587 pub body: String,
588}
589
590#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
592pub struct ToolOutputArtifactSlice {
593 pub id: ToolOutputArtifactId,
594 pub offset: usize,
595 pub next_offset: usize,
596 pub original_bytes: usize,
597 pub eof: bool,
598 pub content: String,
599}
600
601#[async_trait]
602pub trait ToolOutputArtifactStore: Send + Sync {
603 async fn put(
604 &self,
605 ctx: &ToolOutputTruncationContext,
606 body: String,
607 original_bytes: usize,
608 ) -> Result<ToolOutputArtifact, ToolError>;
609
610 async fn read(
611 &self,
612 id: &ToolOutputArtifactId,
613 offset: usize,
614 max_bytes: usize,
615 ) -> Result<ToolOutputArtifactSlice, ToolError>;
616}
617
618#[derive(Debug, Default)]
620pub struct InMemoryToolOutputArtifactStore {
621 next_id: AtomicU64,
622 artifacts: Mutex<BTreeMap<ToolOutputArtifactId, ToolOutputArtifact>>,
623}
624
625impl InMemoryToolOutputArtifactStore {
626 pub fn new() -> Self {
627 Self::default()
628 }
629}
630
631#[async_trait]
632impl ToolOutputArtifactStore for InMemoryToolOutputArtifactStore {
633 async fn put(
634 &self,
635 ctx: &ToolOutputTruncationContext,
636 body: String,
637 original_bytes: usize,
638 ) -> Result<ToolOutputArtifact, ToolError> {
639 let n = self.next_id.fetch_add(1, Ordering::Relaxed);
640 let id = ToolOutputArtifactId(format!(
641 "{}:{}:{}",
642 sanitize_artifact_id_component(ctx.session_id.0.as_str()),
643 sanitize_artifact_id_component(ctx.call_id.0.as_str()),
644 n
645 ));
646 let artifact = ToolOutputArtifact {
647 id: id.clone(),
648 tool_name: ctx.tool_name.clone(),
649 call_id: ctx.call_id.clone(),
650 session_id: ctx.session_id.clone(),
651 turn_id: ctx.turn_id.clone(),
652 original_bytes,
653 body,
654 };
655 self.artifacts
656 .lock()
657 .unwrap_or_else(|e| e.into_inner())
658 .insert(id, artifact.clone());
659 Ok(artifact)
660 }
661
662 async fn read(
663 &self,
664 id: &ToolOutputArtifactId,
665 offset: usize,
666 max_bytes: usize,
667 ) -> Result<ToolOutputArtifactSlice, ToolError> {
668 let artifact = self
669 .artifacts
670 .lock()
671 .unwrap_or_else(|e| e.into_inner())
672 .get(id)
673 .cloned()
674 .ok_or_else(|| {
675 ToolError::InvalidInput(format!("unknown tool result artifact: {id}"))
676 })?;
677 let body = artifact.body;
678 if offset > body.len() || !body.is_char_boundary(offset) {
679 return Err(ToolError::InvalidInput(format!(
680 "offset {offset} is not a UTF-8 boundary in tool result artifact {id}"
681 )));
682 }
683 let requested_end = offset.saturating_add(max_bytes).min(body.len());
684 let end = body.floor_char_boundary(requested_end);
685 Ok(ToolOutputArtifactSlice {
686 id: id.clone(),
687 offset,
688 next_offset: end,
689 original_bytes: artifact.original_bytes,
690 eof: end == body.len(),
691 content: body[offset..end].to_string(),
692 })
693 }
694}
695
696fn sanitize_artifact_id_component(s: &str) -> String {
697 let cleaned: String = s
698 .chars()
699 .map(|c| {
700 if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
701 c
702 } else {
703 '_'
704 }
705 })
706 .take(64)
707 .collect();
708 if cleaned.is_empty() {
709 "_".to_string()
710 } else {
711 cleaned
712 }
713}
714
715pub struct ConfigurableToolOutputTruncationStrategy {
718 default_limit: Option<ToolOutputLimit>,
719 per_tool_limits: BTreeMap<ToolName, ToolOutputLimit>,
720 use_tool_metadata: bool,
721 store: Arc<dyn ToolOutputArtifactStore>,
722}
723
724impl ConfigurableToolOutputTruncationStrategy {
725 pub fn new(store: Arc<dyn ToolOutputArtifactStore>) -> Self {
726 Self {
727 default_limit: None,
728 per_tool_limits: BTreeMap::new(),
729 use_tool_metadata: true,
730 store,
731 }
732 }
733
734 pub fn with_default_limit(mut self, limit: ToolOutputLimit) -> Self {
735 self.default_limit = Some(limit);
736 self
737 }
738
739 pub fn with_tool_limit(
740 mut self,
741 tool_name: impl Into<ToolName>,
742 limit: ToolOutputLimit,
743 ) -> Self {
744 self.per_tool_limits.insert(tool_name.into(), limit);
745 self
746 }
747
748 pub fn use_tool_metadata(mut self, value: bool) -> Self {
749 self.use_tool_metadata = value;
750 self
751 }
752
753 fn limit_for(&self, ctx: &ToolOutputTruncationContext) -> Option<ToolOutputLimit> {
754 self.per_tool_limits
755 .get(&ctx.tool_name)
756 .cloned()
757 .or_else(|| {
758 self.use_tool_metadata
759 .then(|| ToolOutputLimit::from_metadata(&ctx.tool_spec.metadata))
760 .flatten()
761 })
762 .or_else(|| self.default_limit.clone())
763 }
764}
765
766#[async_trait]
767impl ToolOutputTruncationStrategy for ConfigurableToolOutputTruncationStrategy {
768 async fn apply(
769 &self,
770 ctx: ToolOutputTruncationContext,
771 output: ToolOutput,
772 ) -> Result<ToolOutput, ToolError> {
773 let Some(limit) = self.limit_for(&ctx) else {
774 return Ok(output);
775 };
776 let model_bytes = tool_output_model_bytes(&output);
777 if model_bytes <= limit.max_bytes {
778 return Ok(output);
779 }
780
781 match limit.action {
782 ToolOutputOverflowAction::Fail => Err(ToolError::ExecutionFailed(format!(
783 "tool {} produced {model_bytes} bytes, exceeding configured limit of {} bytes",
784 ctx.tool_name, limit.max_bytes
785 ))),
786 ToolOutputOverflowAction::InlineClip => Ok(clip_tool_output_inline(
787 output,
788 limit.max_bytes,
789 model_bytes,
790 )),
791 ToolOutputOverflowAction::StoreForReadback => {
792 let body = tool_output_readback_body(&output);
793 let artifact = self.store.put(&ctx, body, model_bytes).await?;
794 Ok(fit_structured_tool_output(
795 json!({
796 "truncated": true,
797 "tool_result_id": artifact.id.0,
798 "read_tool": TOOL_RESULT_READ_TOOL_NAME,
799 "read_args": {
800 "id": artifact.id.0,
801 "offset": 0,
802 "limit": limit.max_bytes
803 },
804 "original_bytes": artifact.original_bytes,
805 }),
806 limit.max_bytes,
807 ))
808 }
809 }
810 }
811}
812
813fn tool_output_model_bytes(output: &ToolOutput) -> usize {
814 match output {
815 ToolOutput::Text(s) => s.len(),
816 other => serde_json::to_string(other)
817 .map(|s| s.len())
818 .unwrap_or(usize::MAX),
819 }
820}
821
822fn tool_output_readback_body(output: &ToolOutput) -> String {
823 match output {
824 ToolOutput::Text(s) => s.clone(),
825 ToolOutput::Structured(value) => {
826 serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
827 }
828 ToolOutput::Parts(parts) => serde_json::to_string_pretty(parts).unwrap_or_default(),
829 ToolOutput::Files(files) => serde_json::to_string_pretty(files).unwrap_or_default(),
830 }
831}
832
833fn clip_tool_output_inline(
834 output: ToolOutput,
835 max_bytes: usize,
836 original_bytes: usize,
837) -> ToolOutput {
838 match output {
839 ToolOutput::Text(s) => {
840 ToolOutput::Text(clip_string_with_marker(&s, max_bytes, original_bytes))
841 }
842 other => {
843 let body = tool_output_readback_body(&other);
844 fit_structured_tool_output(
845 json!({
846 "truncated": true,
847 "original_bytes": original_bytes,
848 "content": body,
849 }),
850 max_bytes,
851 )
852 }
853 }
854}
855
856fn clip_string_with_marker(s: &str, max_bytes: usize, original_bytes: usize) -> String {
857 let marker = format!("\n[tool output truncated: original_bytes={original_bytes}]");
858 if marker.len() >= max_bytes {
859 let cut = marker.floor_char_boundary(max_bytes.min(marker.len()));
860 return marker[..cut].to_string();
861 }
862 let keep_bytes = max_bytes.saturating_sub(marker.len());
863 let cut = s.floor_char_boundary(keep_bytes.min(s.len()));
864 format!("{}{}", &s[..cut], marker)
865}
866
867fn fit_structured_tool_output(mut value: Value, max_bytes: usize) -> ToolOutput {
868 loop {
869 let output = ToolOutput::Structured(value.clone());
870 if tool_output_model_bytes(&output) <= max_bytes {
871 return output;
872 }
873
874 let Some(Value::String(content)) = value.get_mut("content") else {
875 return ToolOutput::Structured(json!({
876 "truncated": true,
877 "error": "tool output metadata exceeded configured max_bytes"
878 }));
879 };
880 if content.is_empty() {
881 return ToolOutput::Structured(json!({
882 "truncated": true,
883 "error": "tool output metadata exceeded configured max_bytes"
884 }));
885 }
886
887 let current_len = content.len();
888 let shrink_by = tool_output_model_bytes(&output)
889 .saturating_sub(max_bytes)
890 .saturating_add(32)
891 .min(current_len);
892 let new_len = content.floor_char_boundary(current_len - shrink_by);
893 content.truncate(new_len);
894 }
895}
896
897pub const TOOL_RESULT_READ_TOOL_NAME: &str = "tool_result_read";
898const TOOL_RESULT_READ_OUTPUT_ENVELOPE_BYTES: usize = 4096;
899const TOOL_RESULT_READ_JSON_ESCAPE_BYTES_PER_INPUT_BYTE: usize = 6;
900
901#[derive(Clone)]
904pub struct ToolResultReadTool {
905 spec: ToolSpec,
906 store: Arc<dyn ToolOutputArtifactStore>,
907 max_read_bytes: usize,
908}
909
910impl ToolResultReadTool {
911 pub fn new(store: Arc<dyn ToolOutputArtifactStore>, max_read_bytes: usize) -> Self {
912 Self {
913 spec: ToolSpec::new(
914 TOOL_RESULT_READ_TOOL_NAME,
915 "Read a bounded UTF-8 byte slice from a stored oversized tool result.",
916 json!({
917 "type": "object",
918 "properties": {
919 "id": { "type": "string" },
920 "offset": { "type": "integer", "minimum": 0 },
921 "limit": { "type": "integer", "minimum": 1 }
922 },
923 "required": ["id", "offset", "limit"],
924 "additionalProperties": false
925 }),
926 )
927 .with_annotations(ToolAnnotations {
928 read_only_hint: true,
929 idempotent_hint: true,
930 ..ToolAnnotations::default()
931 })
932 .with_output_limit(ToolOutputLimit::fail(
933 max_read_bytes
934 .saturating_mul(TOOL_RESULT_READ_JSON_ESCAPE_BYTES_PER_INPUT_BYTE)
935 .saturating_add(TOOL_RESULT_READ_OUTPUT_ENVELOPE_BYTES),
936 )),
937 store,
938 max_read_bytes,
939 }
940 }
941}
942
943#[derive(Deserialize)]
944struct ToolResultReadInput {
945 id: String,
946 offset: usize,
947 limit: usize,
948}
949
950#[async_trait]
951impl Tool for ToolResultReadTool {
952 fn spec(&self) -> &ToolSpec {
953 &self.spec
954 }
955
956 async fn invoke(
957 &self,
958 request: ToolRequest,
959 _ctx: &mut ToolContext<'_>,
960 ) -> Result<ToolResult, ToolError> {
961 let input: ToolResultReadInput = serde_json::from_value(request.input.clone())
962 .map_err(|error| ToolError::InvalidInput(format!("invalid tool input: {error}")))?;
963 if input.limit == 0 {
964 return Err(ToolError::InvalidInput(
965 "limit must be greater than 0".to_string(),
966 ));
967 }
968 if input.limit > self.max_read_bytes {
969 return Err(ToolError::InvalidInput(format!(
970 "limit {} exceeds maximum read size of {} bytes",
971 input.limit, self.max_read_bytes
972 )));
973 }
974 let slice = self
975 .store
976 .read(&ToolOutputArtifactId(input.id), input.offset, input.limit)
977 .await?;
978 Ok(ToolResult::new(ToolResultPart::success(
979 request.call_id,
980 ToolOutput::Structured(json!({
981 "id": slice.id.0,
982 "offset": slice.offset,
983 "next_offset": slice.next_offset,
984 "original_bytes": slice.original_bytes,
985 "eof": slice.eof,
986 "content": slice.content,
987 })),
988 )))
989 }
990}
991
992pub fn tool_result_readback_registry(
994 store: Arc<dyn ToolOutputArtifactStore>,
995 max_read_bytes: usize,
996) -> ToolRegistry {
997 ToolRegistry::new().with(ToolResultReadTool::new(store, max_read_bytes))
998}
999
1000pub trait PermissionRequest: Send + Sync {
1029 fn kind(&self) -> &'static str;
1031 fn summary(&self) -> String;
1033 fn metadata(&self) -> &MetadataMap;
1035 fn as_any(&self) -> &dyn Any;
1037}
1038
1039#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1044pub enum PermissionCode {
1045 PathNotAllowed,
1047 CommandNotAllowed,
1049 NetworkNotAllowed,
1051 ServerNotTrusted,
1053 AuthScopeNotAllowed,
1055 CustomPolicyDenied,
1057 UnknownRequest,
1059}
1060
1061#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1066pub struct PermissionDenial {
1067 pub code: PermissionCode,
1069 pub message: String,
1071 pub metadata: MetadataMap,
1073}
1074
1075#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1080pub enum ApprovalReason {
1081 PolicyRequiresConfirmation,
1083 EscalatedRisk,
1085 UnknownTarget,
1087 SensitivePath,
1089 SensitiveCommand,
1091 SensitiveServer,
1093 SensitiveAuthScope,
1095}
1096
1097#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1102pub struct ApprovalRequest {
1103 pub task_id: Option<TaskId>,
1105 pub call_id: Option<ToolCallId>,
1108 pub id: ApprovalId,
1110 pub request_kind: String,
1112 pub reason: ApprovalReason,
1114 pub summary: String,
1116 pub metadata: MetadataMap,
1118}
1119
1120impl ApprovalRequest {
1121 pub fn new(
1123 id: impl Into<ApprovalId>,
1124 request_kind: impl Into<String>,
1125 reason: ApprovalReason,
1126 summary: impl Into<String>,
1127 ) -> Self {
1128 Self {
1129 task_id: None,
1130 call_id: None,
1131 id: id.into(),
1132 request_kind: request_kind.into(),
1133 reason,
1134 summary: summary.into(),
1135 metadata: MetadataMap::new(),
1136 }
1137 }
1138
1139 pub fn with_task_id(mut self, task_id: impl Into<TaskId>) -> Self {
1141 self.task_id = Some(task_id.into());
1142 self
1143 }
1144
1145 pub fn with_call_id(mut self, call_id: impl Into<ToolCallId>) -> Self {
1147 self.call_id = Some(call_id.into());
1148 self
1149 }
1150
1151 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
1153 self.metadata = metadata;
1154 self
1155 }
1156}
1157
1158#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1160pub enum ApprovalDecision {
1161 Approve,
1163 Deny {
1165 reason: Option<String>,
1167 },
1168}
1169
1170#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1177pub enum ToolInterruption {
1178 ApprovalRequired(ApprovalRequest),
1180}
1181
1182#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1184pub enum PermissionDecision {
1185 Allow,
1187 Deny(PermissionDenial),
1189 RequireApproval(ApprovalRequest),
1191}
1192
1193pub trait PermissionChecker: Send + Sync {
1217 fn evaluate(&self, request: &dyn PermissionRequest) -> PermissionDecision;
1219}
1220
1221#[derive(Copy, Clone, Debug, Default)]
1226pub struct AllowAllPermissions;
1227
1228impl PermissionChecker for AllowAllPermissions {
1229 fn evaluate(&self, _request: &dyn PermissionRequest) -> PermissionDecision {
1230 PermissionDecision::Allow
1231 }
1232}
1233
1234#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1240pub enum PolicyMatch {
1241 NoOpinion,
1243 Allow,
1245 Deny(PermissionDenial),
1247 RequireApproval(ApprovalRequest),
1249}
1250
1251pub trait PermissionPolicy: Send + Sync {
1260 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch;
1262}
1263
1264pub struct CompositePermissionChecker {
1284 policies: Vec<Box<dyn PermissionPolicy>>,
1285 fallback: PermissionDecision,
1286}
1287
1288impl CompositePermissionChecker {
1289 pub fn new(fallback: PermissionDecision) -> Self {
1297 Self {
1298 policies: Vec::new(),
1299 fallback,
1300 }
1301 }
1302
1303 pub fn with_policy(mut self, policy: impl PermissionPolicy + 'static) -> Self {
1305 self.policies.push(Box::new(policy));
1306 self
1307 }
1308}
1309
1310impl PermissionChecker for CompositePermissionChecker {
1311 fn evaluate(&self, request: &dyn PermissionRequest) -> PermissionDecision {
1312 let mut saw_allow = false;
1313 let mut approval = None;
1314
1315 for policy in &self.policies {
1316 match policy.evaluate(request) {
1317 PolicyMatch::NoOpinion => {}
1318 PolicyMatch::Allow => saw_allow = true,
1319 PolicyMatch::Deny(denial) => return PermissionDecision::Deny(denial),
1320 PolicyMatch::RequireApproval(req) => approval = Some(req),
1321 }
1322 }
1323
1324 if let Some(req) = approval {
1325 PermissionDecision::RequireApproval(req)
1326 } else if saw_allow {
1327 PermissionDecision::Allow
1328 } else {
1329 self.fallback.clone()
1330 }
1331 }
1332}
1333
1334#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1339pub struct ShellPermissionRequest {
1340 pub executable: String,
1342 pub argv: Vec<String>,
1344 pub cwd: Option<PathBuf>,
1346 pub env_keys: Vec<String>,
1348 pub metadata: MetadataMap,
1350}
1351
1352impl PermissionRequest for ShellPermissionRequest {
1353 fn kind(&self) -> &'static str {
1354 "shell.command"
1355 }
1356
1357 fn summary(&self) -> String {
1358 if self.argv.is_empty() {
1359 self.executable.clone()
1360 } else {
1361 format!("{} {}", self.executable, self.argv.join(" "))
1362 }
1363 }
1364
1365 fn metadata(&self) -> &MetadataMap {
1366 &self.metadata
1367 }
1368
1369 fn as_any(&self) -> &dyn Any {
1370 self
1371 }
1372}
1373
1374#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1379pub enum FileSystemPermissionRequest {
1380 Read {
1382 path: PathBuf,
1383 metadata: MetadataMap,
1384 },
1385 Write {
1387 path: PathBuf,
1388 metadata: MetadataMap,
1389 },
1390 Edit {
1392 path: PathBuf,
1393 metadata: MetadataMap,
1394 },
1395 Delete {
1397 path: PathBuf,
1398 metadata: MetadataMap,
1399 },
1400 Move {
1402 from: PathBuf,
1403 to: PathBuf,
1404 metadata: MetadataMap,
1405 },
1406 List {
1408 path: PathBuf,
1409 metadata: MetadataMap,
1410 },
1411 CreateDir {
1413 path: PathBuf,
1414 metadata: MetadataMap,
1415 },
1416}
1417
1418impl FileSystemPermissionRequest {
1419 fn metadata_map(&self) -> &MetadataMap {
1420 match self {
1421 Self::Read { metadata, .. }
1422 | Self::Write { metadata, .. }
1423 | Self::Edit { metadata, .. }
1424 | Self::Delete { metadata, .. }
1425 | Self::Move { metadata, .. }
1426 | Self::List { metadata, .. }
1427 | Self::CreateDir { metadata, .. } => metadata,
1428 }
1429 }
1430}
1431
1432impl PermissionRequest for FileSystemPermissionRequest {
1433 fn kind(&self) -> &'static str {
1434 match self {
1435 Self::Read { .. } => "filesystem.read",
1436 Self::Write { .. } => "filesystem.write",
1437 Self::Edit { .. } => "filesystem.edit",
1438 Self::Delete { .. } => "filesystem.delete",
1439 Self::Move { .. } => "filesystem.move",
1440 Self::List { .. } => "filesystem.list",
1441 Self::CreateDir { .. } => "filesystem.mkdir",
1442 }
1443 }
1444
1445 fn summary(&self) -> String {
1446 match self {
1447 Self::Read { path, .. } => format!("Read {}", path.display()),
1448 Self::Write { path, .. } => format!("Write {}", path.display()),
1449 Self::Edit { path, .. } => format!("Edit {}", path.display()),
1450 Self::Delete { path, .. } => format!("Delete {}", path.display()),
1451 Self::Move { from, to, .. } => {
1452 format!("Move {} to {}", from.display(), to.display())
1453 }
1454 Self::List { path, .. } => format!("List {}", path.display()),
1455 Self::CreateDir { path, .. } => format!("Create directory {}", path.display()),
1456 }
1457 }
1458
1459 fn metadata(&self) -> &MetadataMap {
1460 self.metadata_map()
1461 }
1462
1463 fn as_any(&self) -> &dyn Any {
1464 self
1465 }
1466}
1467
1468#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1473pub enum McpPermissionRequest {
1474 Connect {
1476 server_id: String,
1477 metadata: MetadataMap,
1478 },
1479 InvokeTool {
1481 server_id: String,
1482 tool_name: String,
1483 metadata: MetadataMap,
1484 },
1485 ReadResource {
1487 server_id: String,
1488 resource_id: String,
1489 metadata: MetadataMap,
1490 },
1491 FetchPrompt {
1493 server_id: String,
1494 prompt_id: String,
1495 metadata: MetadataMap,
1496 },
1497 UseAuthScope {
1499 server_id: String,
1500 scope: String,
1501 metadata: MetadataMap,
1502 },
1503}
1504
1505impl McpPermissionRequest {
1506 fn metadata_map(&self) -> &MetadataMap {
1507 match self {
1508 Self::Connect { metadata, .. }
1509 | Self::InvokeTool { metadata, .. }
1510 | Self::ReadResource { metadata, .. }
1511 | Self::FetchPrompt { metadata, .. }
1512 | Self::UseAuthScope { metadata, .. } => metadata,
1513 }
1514 }
1515}
1516
1517impl PermissionRequest for McpPermissionRequest {
1518 fn kind(&self) -> &'static str {
1519 match self {
1520 Self::Connect { .. } => "mcp.connect",
1521 Self::InvokeTool { .. } => "mcp.invoke_tool",
1522 Self::ReadResource { .. } => "mcp.read_resource",
1523 Self::FetchPrompt { .. } => "mcp.fetch_prompt",
1524 Self::UseAuthScope { .. } => "mcp.use_auth_scope",
1525 }
1526 }
1527
1528 fn summary(&self) -> String {
1529 match self {
1530 Self::Connect { server_id, .. } => format!("Connect MCP server {server_id}"),
1531 Self::InvokeTool {
1532 server_id,
1533 tool_name,
1534 ..
1535 } => format!("Invoke MCP tool {server_id}.{tool_name}"),
1536 Self::ReadResource {
1537 server_id,
1538 resource_id,
1539 ..
1540 } => format!("Read MCP resource {server_id}:{resource_id}"),
1541 Self::FetchPrompt {
1542 server_id,
1543 prompt_id,
1544 ..
1545 } => format!("Fetch MCP prompt {server_id}:{prompt_id}"),
1546 Self::UseAuthScope {
1547 server_id, scope, ..
1548 } => format!("Use MCP auth scope {server_id}:{scope}"),
1549 }
1550 }
1551
1552 fn metadata(&self) -> &MetadataMap {
1553 self.metadata_map()
1554 }
1555
1556 fn as_any(&self) -> &dyn Any {
1557 self
1558 }
1559}
1560
1561pub struct CustomKindPolicy {
1577 allowed_kinds: BTreeSet<String>,
1578 denied_kinds: BTreeSet<String>,
1579 require_approval_by_default: bool,
1580}
1581
1582impl CustomKindPolicy {
1583 pub fn new(require_approval_by_default: bool) -> Self {
1590 Self {
1591 allowed_kinds: BTreeSet::new(),
1592 denied_kinds: BTreeSet::new(),
1593 require_approval_by_default,
1594 }
1595 }
1596
1597 pub fn allow_kind(mut self, kind: impl Into<String>) -> Self {
1599 self.allowed_kinds.insert(kind.into());
1600 self
1601 }
1602
1603 pub fn deny_kind(mut self, kind: impl Into<String>) -> Self {
1605 self.denied_kinds.insert(kind.into());
1606 self
1607 }
1608}
1609
1610impl PermissionPolicy for CustomKindPolicy {
1611 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1612 let kind = request.kind();
1613 if !kind.starts_with("custom.") {
1614 return PolicyMatch::NoOpinion;
1615 }
1616 if self.denied_kinds.contains(kind) {
1617 return PolicyMatch::Deny(PermissionDenial {
1618 code: PermissionCode::CustomPolicyDenied,
1619 message: format!("custom permission kind {kind} is denied"),
1620 metadata: request.metadata().clone(),
1621 });
1622 }
1623 if self.allowed_kinds.contains(kind) {
1624 return PolicyMatch::Allow;
1625 }
1626 if self.require_approval_by_default {
1627 PolicyMatch::RequireApproval(ApprovalRequest {
1628 task_id: None,
1629 call_id: None,
1630 id: ApprovalId::new(format!("approval:{kind}")),
1631 request_kind: kind.to_string(),
1632 reason: ApprovalReason::PolicyRequiresConfirmation,
1633 summary: request.summary(),
1634 metadata: request.metadata().clone(),
1635 })
1636 } else {
1637 PolicyMatch::NoOpinion
1638 }
1639 }
1640}
1641
1642pub struct PathPolicy {
1662 allowed_roots: Vec<CanonicalRoot>,
1663 read_only_roots: Vec<CanonicalRoot>,
1664 protected_roots: Vec<CanonicalRoot>,
1665 require_approval_outside_allowed: bool,
1666}
1667
1668impl PathPolicy {
1669 pub fn new() -> Self {
1672 Self {
1673 allowed_roots: Vec::new(),
1674 read_only_roots: Vec::new(),
1675 protected_roots: Vec::new(),
1676 require_approval_outside_allowed: true,
1677 }
1678 }
1679
1680 pub fn allow_root(mut self, root: impl Into<PathBuf>) -> Self {
1682 self.allowed_roots.push(CanonicalRoot::new(root.into()));
1683 self
1684 }
1685
1686 pub fn read_only_root(mut self, root: impl Into<PathBuf>) -> Self {
1688 self.read_only_roots.push(CanonicalRoot::new(root.into()));
1689 self
1690 }
1691
1692 pub fn protect_root(mut self, root: impl Into<PathBuf>) -> Self {
1694 self.protected_roots.push(CanonicalRoot::new(root.into()));
1695 self
1696 }
1697
1698 pub fn require_approval_outside_allowed(mut self, value: bool) -> Self {
1701 self.require_approval_outside_allowed = value;
1702 self
1703 }
1704}
1705
1706impl Default for PathPolicy {
1707 fn default() -> Self {
1708 Self::new()
1709 }
1710}
1711
1712fn resolve_canonical(path: &Path) -> PathBuf {
1716 let abs = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
1717 canonicalize_with_partial_fallback(&abs).unwrap_or(abs)
1718}
1719
1720fn canonicalize_with_partial_fallback(abs: &Path) -> Option<PathBuf> {
1721 if let Ok(canonical) = std::fs::canonicalize(abs) {
1722 return Some(canonical);
1723 }
1724 let mut tail: Vec<std::ffi::OsString> = Vec::new();
1725 let mut current = abs.to_path_buf();
1726 loop {
1727 let name = current.file_name().map(|n| n.to_os_string())?;
1728 tail.push(name);
1729 if !current.pop() {
1730 return None;
1731 }
1732 if let Ok(canonical) = std::fs::canonicalize(¤t) {
1733 let mut out = canonical;
1734 for seg in tail.iter().rev() {
1735 out.push(seg);
1736 }
1737 return Some(out);
1738 }
1739 }
1740}
1741
1742struct CanonicalRoot {
1748 lexical: PathBuf,
1749 canonical: OnceLock<PathBuf>,
1750}
1751
1752impl CanonicalRoot {
1753 fn new(lexical: PathBuf) -> Self {
1754 Self {
1755 lexical,
1756 canonical: OnceLock::new(),
1757 }
1758 }
1759
1760 fn resolve(&self) -> std::borrow::Cow<'_, Path> {
1761 if let Some(canonical) = self.canonical.get() {
1762 return std::borrow::Cow::Borrowed(canonical);
1763 }
1764 let abs = std::path::absolute(&self.lexical).unwrap_or_else(|_| self.lexical.clone());
1765 if let Ok(canonical) = std::fs::canonicalize(&abs) {
1766 let _ = self.canonical.set(canonical);
1767 return std::borrow::Cow::Borrowed(self.canonical.get().unwrap());
1768 }
1769 std::borrow::Cow::Owned(canonicalize_with_partial_fallback(&abs).unwrap_or(abs))
1770 }
1771}
1772
1773impl PermissionPolicy for PathPolicy {
1774 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1775 let Some(fs) = request
1776 .as_any()
1777 .downcast_ref::<FileSystemPermissionRequest>()
1778 else {
1779 return PolicyMatch::NoOpinion;
1780 };
1781
1782 let raw_paths: Vec<&Path> = match fs {
1783 FileSystemPermissionRequest::Move { from, to, .. } => {
1784 vec![from.as_path(), to.as_path()]
1785 }
1786 FileSystemPermissionRequest::Read { path, .. }
1787 | FileSystemPermissionRequest::Write { path, .. }
1788 | FileSystemPermissionRequest::Edit { path, .. }
1789 | FileSystemPermissionRequest::Delete { path, .. }
1790 | FileSystemPermissionRequest::List { path, .. }
1791 | FileSystemPermissionRequest::CreateDir { path, .. } => vec![path.as_path()],
1792 };
1793
1794 let candidate_paths: Vec<PathBuf> =
1795 raw_paths.iter().map(|p| resolve_canonical(p)).collect();
1796
1797 let mutates = matches!(
1798 fs,
1799 FileSystemPermissionRequest::Write { .. }
1800 | FileSystemPermissionRequest::Edit { .. }
1801 | FileSystemPermissionRequest::Delete { .. }
1802 | FileSystemPermissionRequest::Move { .. }
1803 | FileSystemPermissionRequest::CreateDir { .. }
1804 );
1805
1806 if candidate_paths.iter().any(|path| {
1807 self.protected_roots
1808 .iter()
1809 .any(|root| path.starts_with(root.resolve().as_ref()))
1810 }) {
1811 return PolicyMatch::Deny(PermissionDenial {
1812 code: PermissionCode::PathNotAllowed,
1813 message: format!("path access denied for {}", fs.summary()),
1814 metadata: fs.metadata().clone(),
1815 });
1816 }
1817
1818 if mutates
1819 && candidate_paths.iter().any(|path| {
1820 self.read_only_roots
1821 .iter()
1822 .any(|root| path.starts_with(root.resolve().as_ref()))
1823 })
1824 {
1825 return PolicyMatch::Deny(PermissionDenial {
1826 code: PermissionCode::PathNotAllowed,
1827 message: format!("path is read-only for {}", fs.summary()),
1828 metadata: fs.metadata().clone(),
1829 });
1830 }
1831
1832 if self.allowed_roots.is_empty() {
1833 return PolicyMatch::NoOpinion;
1834 }
1835
1836 let all_allowed = candidate_paths.iter().all(|path| {
1837 self.allowed_roots
1838 .iter()
1839 .any(|root| path.starts_with(root.resolve().as_ref()))
1840 });
1841
1842 if all_allowed {
1843 PolicyMatch::Allow
1844 } else if self.require_approval_outside_allowed {
1845 PolicyMatch::RequireApproval(ApprovalRequest {
1846 task_id: None,
1847 call_id: None,
1848 id: ApprovalId::new(format!("approval:{}", fs.kind())),
1849 request_kind: fs.kind().to_string(),
1850 reason: ApprovalReason::SensitivePath,
1851 summary: fs.summary(),
1852 metadata: fs.metadata().clone(),
1853 })
1854 } else {
1855 PolicyMatch::Deny(PermissionDenial {
1856 code: PermissionCode::PathNotAllowed,
1857 message: format!("path outside allowed roots for {}", fs.summary()),
1858 metadata: fs.metadata().clone(),
1859 })
1860 }
1861 }
1862}
1863
1864pub struct CommandPolicy {
1885 allowed_executables: BTreeSet<String>,
1886 denied_executables: BTreeSet<String>,
1887 allowed_cwds: Vec<PathBuf>,
1888 denied_env_keys: BTreeSet<String>,
1889 require_approval_for_unknown: bool,
1890}
1891
1892impl CommandPolicy {
1893 pub fn new() -> Self {
1896 Self {
1897 allowed_executables: BTreeSet::new(),
1898 denied_executables: BTreeSet::new(),
1899 allowed_cwds: Vec::new(),
1900 denied_env_keys: BTreeSet::new(),
1901 require_approval_for_unknown: true,
1902 }
1903 }
1904
1905 pub fn allow_executable(mut self, executable: impl Into<String>) -> Self {
1907 self.allowed_executables.insert(executable.into());
1908 self
1909 }
1910
1911 pub fn deny_executable(mut self, executable: impl Into<String>) -> Self {
1913 self.denied_executables.insert(executable.into());
1914 self
1915 }
1916
1917 pub fn allow_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
1919 self.allowed_cwds.push(cwd.into());
1920 self
1921 }
1922
1923 pub fn deny_env_key(mut self, key: impl Into<String>) -> Self {
1925 self.denied_env_keys.insert(key.into());
1926 self
1927 }
1928
1929 pub fn require_approval_for_unknown(mut self, value: bool) -> Self {
1932 self.require_approval_for_unknown = value;
1933 self
1934 }
1935}
1936
1937impl Default for CommandPolicy {
1938 fn default() -> Self {
1939 Self::new()
1940 }
1941}
1942
1943impl PermissionPolicy for CommandPolicy {
1944 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
1945 let Some(shell) = request.as_any().downcast_ref::<ShellPermissionRequest>() else {
1946 return PolicyMatch::NoOpinion;
1947 };
1948
1949 if self.denied_executables.contains(&shell.executable)
1950 || shell
1951 .env_keys
1952 .iter()
1953 .any(|key| self.denied_env_keys.contains(key))
1954 {
1955 return PolicyMatch::Deny(PermissionDenial {
1956 code: PermissionCode::CommandNotAllowed,
1957 message: format!("command denied for {}", shell.summary()),
1958 metadata: shell.metadata().clone(),
1959 });
1960 }
1961
1962 if let Some(cwd) = &shell.cwd
1963 && !self.allowed_cwds.is_empty()
1964 && !self.allowed_cwds.iter().any(|root| cwd.starts_with(root))
1965 {
1966 return PolicyMatch::RequireApproval(ApprovalRequest {
1967 task_id: None,
1968 call_id: None,
1969 id: ApprovalId::new("approval:shell.cwd"),
1970 request_kind: shell.kind().to_string(),
1971 reason: ApprovalReason::SensitiveCommand,
1972 summary: shell.summary(),
1973 metadata: shell.metadata().clone(),
1974 });
1975 }
1976
1977 if self.allowed_executables.is_empty()
1978 || self.allowed_executables.contains(&shell.executable)
1979 {
1980 PolicyMatch::Allow
1981 } else if self.require_approval_for_unknown {
1982 PolicyMatch::RequireApproval(ApprovalRequest {
1983 task_id: None,
1984 call_id: None,
1985 id: ApprovalId::new("approval:shell.command"),
1986 request_kind: shell.kind().to_string(),
1987 reason: ApprovalReason::SensitiveCommand,
1988 summary: shell.summary(),
1989 metadata: shell.metadata().clone(),
1990 })
1991 } else {
1992 PolicyMatch::Deny(PermissionDenial {
1993 code: PermissionCode::CommandNotAllowed,
1994 message: format!("executable {} is not allowed", shell.executable),
1995 metadata: shell.metadata().clone(),
1996 })
1997 }
1998 }
1999}
2000
2001pub struct McpServerPolicy {
2015 trusted_servers: BTreeSet<String>,
2016 allowed_auth_scopes: BTreeSet<String>,
2017 require_approval_for_untrusted: bool,
2018}
2019
2020impl McpServerPolicy {
2021 pub fn new() -> Self {
2024 Self {
2025 trusted_servers: BTreeSet::new(),
2026 allowed_auth_scopes: BTreeSet::new(),
2027 require_approval_for_untrusted: true,
2028 }
2029 }
2030
2031 pub fn trust_server(mut self, server_id: impl Into<String>) -> Self {
2033 self.trusted_servers.insert(server_id.into());
2034 self
2035 }
2036
2037 pub fn allow_auth_scope(mut self, scope: impl Into<String>) -> Self {
2039 self.allowed_auth_scopes.insert(scope.into());
2040 self
2041 }
2042}
2043
2044impl Default for McpServerPolicy {
2045 fn default() -> Self {
2046 Self::new()
2047 }
2048}
2049
2050impl PermissionPolicy for McpServerPolicy {
2051 fn evaluate(&self, request: &dyn PermissionRequest) -> PolicyMatch {
2052 let Some(mcp) = request.as_any().downcast_ref::<McpPermissionRequest>() else {
2053 return PolicyMatch::NoOpinion;
2054 };
2055
2056 let server_id = match mcp {
2057 McpPermissionRequest::Connect { server_id, .. }
2058 | McpPermissionRequest::InvokeTool { server_id, .. }
2059 | McpPermissionRequest::ReadResource { server_id, .. }
2060 | McpPermissionRequest::FetchPrompt { server_id, .. }
2061 | McpPermissionRequest::UseAuthScope { server_id, .. } => server_id,
2062 };
2063
2064 if !self.trusted_servers.is_empty() && !self.trusted_servers.contains(server_id) {
2065 return if self.require_approval_for_untrusted {
2066 PolicyMatch::RequireApproval(ApprovalRequest {
2067 task_id: None,
2068 call_id: None,
2069 id: ApprovalId::new(format!("approval:mcp:{server_id}")),
2070 request_kind: mcp.kind().to_string(),
2071 reason: ApprovalReason::SensitiveServer,
2072 summary: mcp.summary(),
2073 metadata: mcp.metadata().clone(),
2074 })
2075 } else {
2076 PolicyMatch::Deny(PermissionDenial {
2077 code: PermissionCode::ServerNotTrusted,
2078 message: format!("MCP server {server_id} is not trusted"),
2079 metadata: mcp.metadata().clone(),
2080 })
2081 };
2082 }
2083
2084 if let McpPermissionRequest::UseAuthScope { scope, .. } = mcp
2085 && !self.allowed_auth_scopes.is_empty()
2086 && !self.allowed_auth_scopes.contains(scope)
2087 {
2088 return PolicyMatch::Deny(PermissionDenial {
2089 code: PermissionCode::AuthScopeNotAllowed,
2090 message: format!("MCP auth scope {scope} is not allowed"),
2091 metadata: mcp.metadata().clone(),
2092 });
2093 }
2094
2095 PolicyMatch::Allow
2096 }
2097}
2098
2099#[async_trait]
2151pub trait Tool: Send + Sync {
2152 fn spec(&self) -> &ToolSpec;
2154
2155 fn current_spec(&self) -> Option<ToolSpec> {
2163 Some(self.spec().clone())
2164 }
2165
2166 fn proposed_requests(
2178 &self,
2179 _request: &ToolRequest,
2180 ) -> Result<Vec<Box<dyn PermissionRequest>>, ToolError> {
2181 Ok(Vec::new())
2182 }
2183
2184 async fn invoke(
2193 &self,
2194 request: ToolRequest,
2195 ctx: &mut ToolContext<'_>,
2196 ) -> Result<ToolResult, ToolError>;
2197}
2198
2199#[derive(Clone, Default)]
2230pub struct ToolRegistry {
2231 tools: BTreeMap<ToolName, Arc<dyn Tool>>,
2232}
2233
2234impl ToolRegistry {
2235 pub fn new() -> Self {
2237 Self::default()
2238 }
2239
2240 pub fn register<T>(&mut self, tool: T) -> &mut Self
2242 where
2243 T: Tool + 'static,
2244 {
2245 self.tools.insert(tool.spec().name.clone(), Arc::new(tool));
2246 self
2247 }
2248
2249 pub fn with<T>(mut self, tool: T) -> Self
2251 where
2252 T: Tool + 'static,
2253 {
2254 self.register(tool);
2255 self
2256 }
2257
2258 pub fn register_arc(&mut self, tool: Arc<dyn Tool>) -> &mut Self {
2260 self.tools.insert(tool.spec().name.clone(), tool);
2261 self
2262 }
2263
2264 pub fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2266 self.tools.get(name).cloned()
2267 }
2268
2269 pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
2271 self.tools.values().cloned().collect()
2272 }
2273
2274 pub fn merge(mut self, other: Self) -> Self {
2283 self.tools.extend(other.tools);
2284 self
2285 }
2286
2287 pub fn specs(&self) -> Vec<ToolSpec> {
2289 self.tools
2290 .values()
2291 .filter_map(|tool| tool.current_spec())
2292 .collect()
2293 }
2294}
2295
2296pub trait ToolSource: Send + Sync {
2304 fn specs(&self) -> Vec<ToolSpec>;
2306
2307 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>>;
2309
2310 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2314 Vec::new()
2315 }
2316
2317 fn prefixed(self, prefix: impl Into<String>) -> Prefixed<Self>
2327 where
2328 Self: Sized,
2329 {
2330 Prefixed::new(self, prefix)
2331 }
2332
2333 fn filtered<F>(self, predicate: F) -> Filtered<Self, F>
2339 where
2340 Self: Sized,
2341 F: Fn(&ToolName) -> bool + Send + Sync + 'static,
2342 {
2343 Filtered::new(self, predicate)
2344 }
2345
2346 fn renamed<I>(self, mapping: I) -> Renamed<Self>
2353 where
2354 Self: Sized,
2355 I: IntoIterator<Item = (ToolName, ToolName)>,
2356 {
2357 Renamed::new(self, mapping)
2358 }
2359}
2360
2361impl ToolSource for ToolRegistry {
2362 fn specs(&self) -> Vec<ToolSpec> {
2363 ToolRegistry::specs(self)
2364 }
2365
2366 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2367 ToolRegistry::get(self, name)
2368 }
2369}
2370
2371impl<S> ToolSource for Arc<S>
2372where
2373 S: ToolSource + ?Sized,
2374{
2375 fn specs(&self) -> Vec<ToolSpec> {
2376 (**self).specs()
2377 }
2378
2379 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2380 (**self).get(name)
2381 }
2382
2383 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2384 (**self).drain_catalog_events()
2385 }
2386}
2387
2388pub struct Prefixed<S> {
2391 inner: S,
2392 prefix: String,
2393}
2394
2395impl<S> Prefixed<S> {
2396 pub fn new(inner: S, prefix: impl Into<String>) -> Self {
2398 Self {
2399 inner,
2400 prefix: prefix.into(),
2401 }
2402 }
2403
2404 fn rewrite(&self, name: &str) -> String {
2405 format!("{}_{}", self.prefix, name)
2406 }
2407
2408 fn strip<'a>(&self, name: &'a str) -> Option<&'a str> {
2409 name.strip_prefix(self.prefix.as_str())
2410 .and_then(|rest| rest.strip_prefix('_'))
2411 }
2412}
2413
2414impl<S> ToolSource for Prefixed<S>
2415where
2416 S: ToolSource,
2417{
2418 fn specs(&self) -> Vec<ToolSpec> {
2419 self.inner
2420 .specs()
2421 .into_iter()
2422 .map(|mut spec| {
2423 spec.name = ToolName::new(self.rewrite(spec.name.0.as_str()));
2424 spec
2425 })
2426 .collect()
2427 }
2428
2429 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2430 let original = self.strip(name.0.as_str())?;
2431 let inner_name = ToolName::new(original);
2432 let inner_tool = self.inner.get(&inner_name)?;
2433 let mut public_spec = inner_tool.spec().clone();
2434 public_spec.name = name.clone();
2435 Some(Arc::new(RewrittenTool {
2436 inner: inner_tool,
2437 inner_name,
2438 public_spec,
2439 }))
2440 }
2441
2442 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2443 self.inner
2444 .drain_catalog_events()
2445 .into_iter()
2446 .map(|mut event| {
2447 event.for_each_name_mut(|name| *name = self.rewrite(name.as_str()));
2448 event
2449 })
2450 .collect()
2451 }
2452}
2453
2454pub struct Filtered<S, F> {
2457 inner: S,
2458 predicate: F,
2459}
2460
2461impl<S, F> Filtered<S, F> {
2462 pub fn new(inner: S, predicate: F) -> Self {
2464 Self { inner, predicate }
2465 }
2466}
2467
2468impl<S, F> ToolSource for Filtered<S, F>
2469where
2470 S: ToolSource,
2471 F: Fn(&ToolName) -> bool + Send + Sync + 'static,
2472{
2473 fn specs(&self) -> Vec<ToolSpec> {
2474 self.inner
2475 .specs()
2476 .into_iter()
2477 .filter(|spec| (self.predicate)(&spec.name))
2478 .collect()
2479 }
2480
2481 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2482 if !(self.predicate)(name) {
2483 return None;
2484 }
2485 self.inner.get(name)
2486 }
2487
2488 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2489 self.inner
2490 .drain_catalog_events()
2491 .into_iter()
2492 .map(|mut event| {
2493 event.retain_names(|n| (self.predicate)(&ToolName::new(n)));
2494 event
2495 })
2496 .collect()
2497 }
2498}
2499
2500pub struct Renamed<S> {
2507 inner: S,
2508 forward: BTreeMap<ToolName, ToolName>,
2509 backward: BTreeMap<ToolName, ToolName>,
2510}
2511
2512impl<S> Renamed<S> {
2513 pub fn new<I>(inner: S, mapping: I) -> Self
2515 where
2516 I: IntoIterator<Item = (ToolName, ToolName)>,
2517 {
2518 let forward: BTreeMap<ToolName, ToolName> = mapping.into_iter().collect();
2519 let backward = forward
2520 .iter()
2521 .map(|(k, v)| (v.clone(), k.clone()))
2522 .collect();
2523 Self {
2524 inner,
2525 forward,
2526 backward,
2527 }
2528 }
2529}
2530
2531impl<S> ToolSource for Renamed<S>
2532where
2533 S: ToolSource,
2534{
2535 fn specs(&self) -> Vec<ToolSpec> {
2536 self.inner
2537 .specs()
2538 .into_iter()
2539 .map(|mut spec| {
2540 if let Some(new_name) = self.forward.get(&spec.name) {
2541 spec.name = new_name.clone();
2542 }
2543 spec
2544 })
2545 .collect()
2546 }
2547
2548 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2549 if let Some(original) = self.backward.get(name) {
2550 let inner_tool = self.inner.get(original)?;
2551 let mut public_spec = inner_tool.spec().clone();
2552 public_spec.name = name.clone();
2553 Some(Arc::new(RewrittenTool {
2554 inner: inner_tool,
2555 inner_name: original.clone(),
2556 public_spec,
2557 }))
2558 } else if self.forward.contains_key(name) {
2559 None
2561 } else {
2562 self.inner.get(name)
2563 }
2564 }
2565
2566 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2567 self.inner
2568 .drain_catalog_events()
2569 .into_iter()
2570 .map(|mut event| {
2571 event.for_each_name_mut(|name| {
2572 if let Some(new) = self.forward.get(&ToolName::new(name.as_str())) {
2573 *name = new.0.clone();
2574 }
2575 });
2576 event
2577 })
2578 .collect()
2579 }
2580}
2581
2582#[cfg(feature = "schemars")]
2608pub fn schema_for<T: schemars::JsonSchema>() -> Value {
2609 let schema = schemars::schema_for!(T);
2610 serde_json::to_value(schema)
2611 .expect("schemars produces valid JSON; this conversion is infallible")
2612}
2613
2614#[cfg(feature = "schemars")]
2632pub fn tool_spec_for<T: schemars::JsonSchema>(
2633 name: impl Into<ToolName>,
2634 description: impl Into<String>,
2635) -> ToolSpec {
2636 ToolSpec::new(name, description, schema_for::<T>())
2637}
2638
2639struct RewrittenTool {
2645 inner: Arc<dyn Tool>,
2646 inner_name: ToolName,
2647 public_spec: ToolSpec,
2648}
2649
2650#[async_trait]
2651impl Tool for RewrittenTool {
2652 fn spec(&self) -> &ToolSpec {
2653 &self.public_spec
2654 }
2655
2656 fn current_spec(&self) -> Option<ToolSpec> {
2657 let inner_current = self.inner.current_spec()?;
2658 Some(ToolSpec {
2659 name: self.public_spec.name.clone(),
2660 description: inner_current.description,
2661 input_schema: inner_current.input_schema,
2662 annotations: inner_current.annotations,
2663 metadata: inner_current.metadata,
2664 })
2665 }
2666
2667 fn proposed_requests(
2668 &self,
2669 request: &ToolRequest,
2670 ) -> Result<Vec<Box<dyn PermissionRequest>>, ToolError> {
2671 let mut inner_request = request.clone();
2672 inner_request.tool_name = self.inner_name.clone();
2673 self.inner.proposed_requests(&inner_request)
2674 }
2675
2676 async fn invoke(
2677 &self,
2678 mut request: ToolRequest,
2679 ctx: &mut ToolContext<'_>,
2680 ) -> Result<ToolResult, ToolError> {
2681 request.tool_name = self.inner_name.clone();
2682 self.inner.invoke(request, ctx).await
2683 }
2684}
2685
2686struct ToolMap {
2704 inner: std::sync::RwLock<BTreeMap<ToolName, Arc<dyn Tool>>>,
2705}
2706
2707impl ToolMap {
2708 fn new() -> Self {
2709 Self {
2710 inner: std::sync::RwLock::new(BTreeMap::new()),
2711 }
2712 }
2713
2714 fn read(&self) -> std::sync::RwLockReadGuard<'_, BTreeMap<ToolName, Arc<dyn Tool>>> {
2715 self.inner.read().unwrap_or_else(|e| e.into_inner())
2716 }
2717
2718 fn write(&self) -> std::sync::RwLockWriteGuard<'_, BTreeMap<ToolName, Arc<dyn Tool>>> {
2719 self.inner.write().unwrap_or_else(|e| e.into_inner())
2720 }
2721}
2722
2723struct DynamicCatalogInner {
2726 source_id: String,
2727 tools: ToolMap,
2728 events_tx: tokio::sync::broadcast::Sender<ToolCatalogEvent>,
2729}
2730
2731pub fn dynamic_catalog(source_id: impl Into<String>) -> (CatalogWriter, CatalogReader) {
2749 let (events_tx, events_rx) = tokio::sync::broadcast::channel(128);
2750 let inner = Arc::new(DynamicCatalogInner {
2751 source_id: source_id.into(),
2752 tools: ToolMap::new(),
2753 events_tx,
2754 });
2755 (
2756 CatalogWriter {
2757 inner: Arc::clone(&inner),
2758 },
2759 CatalogReader {
2760 inner,
2761 events_rx: std::sync::Mutex::new(events_rx),
2762 },
2763 )
2764}
2765
2766pub struct CatalogWriter {
2773 inner: Arc<DynamicCatalogInner>,
2774}
2775
2776impl CatalogWriter {
2777 pub fn source_id(&self) -> &str {
2779 &self.inner.source_id
2780 }
2781
2782 pub fn reader(&self) -> CatalogReader {
2786 CatalogReader {
2787 inner: Arc::clone(&self.inner),
2788 events_rx: std::sync::Mutex::new(self.inner.events_tx.subscribe()),
2789 }
2790 }
2791
2792 pub fn upsert(&self, tool: Arc<dyn Tool>) {
2795 let name = tool.spec().name.clone();
2796 let mut guard = self.inner.tools.write();
2797 let existed = guard.insert(name.clone(), tool).is_some();
2798 drop(guard);
2799 let mut event = ToolCatalogEvent::new(self.inner.source_id.clone());
2800 if existed {
2801 event.changed.push(name.0);
2802 } else {
2803 event.added.push(name.0);
2804 }
2805 let _ = self.inner.events_tx.send(event);
2806 }
2807
2808 pub fn remove(&self, name: &ToolName) -> bool {
2811 let mut guard = self.inner.tools.write();
2812 let removed = guard.remove(name).is_some();
2813 drop(guard);
2814 if removed {
2815 let mut event = ToolCatalogEvent::new(self.inner.source_id.clone());
2816 event.removed.push(name.0.clone());
2817 let _ = self.inner.events_tx.send(event);
2818 }
2819 removed
2820 }
2821
2822 pub fn replace_all(&self, tools: impl IntoIterator<Item = Arc<dyn Tool>>) {
2825 let new_map: BTreeMap<ToolName, Arc<dyn Tool>> = tools
2826 .into_iter()
2827 .map(|tool| (tool.spec().name.clone(), tool))
2828 .collect();
2829
2830 let mut guard = self.inner.tools.write();
2831 let mut event = ToolCatalogEvent::new(self.inner.source_id.clone());
2832
2833 for (name, new_tool) in new_map.iter() {
2834 match guard.get(name) {
2835 None => event.added.push(name.0.clone()),
2836 Some(existing)
2837 if !Arc::ptr_eq(existing, new_tool)
2838 && existing.current_spec() != new_tool.current_spec() =>
2839 {
2840 event.changed.push(name.0.clone());
2841 }
2842 Some(_) => {}
2843 }
2844 }
2845 for name in guard.keys() {
2846 if !new_map.contains_key(name) {
2847 event.removed.push(name.0.clone());
2848 }
2849 }
2850
2851 *guard = new_map;
2852 drop(guard);
2853
2854 if !event.added.is_empty() || !event.removed.is_empty() || !event.changed.is_empty() {
2855 let _ = self.inner.events_tx.send(event);
2856 }
2857 }
2858
2859 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ToolCatalogEvent> {
2863 self.inner.events_tx.subscribe()
2864 }
2865}
2866
2867pub struct CatalogReader {
2872 inner: Arc<DynamicCatalogInner>,
2873 events_rx: std::sync::Mutex<tokio::sync::broadcast::Receiver<ToolCatalogEvent>>,
2874}
2875
2876impl CatalogReader {
2877 pub fn source_id(&self) -> &str {
2879 &self.inner.source_id
2880 }
2881
2882 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ToolCatalogEvent> {
2885 self.inner.events_tx.subscribe()
2886 }
2887}
2888
2889impl Clone for CatalogReader {
2890 fn clone(&self) -> Self {
2891 Self {
2892 inner: Arc::clone(&self.inner),
2893 events_rx: std::sync::Mutex::new(self.inner.events_tx.subscribe()),
2894 }
2895 }
2896}
2897
2898impl ToolSource for CatalogReader {
2899 fn specs(&self) -> Vec<ToolSpec> {
2900 self.inner
2901 .tools
2902 .read()
2903 .values()
2904 .filter_map(|tool| tool.current_spec())
2905 .collect()
2906 }
2907
2908 fn get(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
2909 self.inner.tools.read().get(name).cloned()
2910 }
2911
2912 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
2913 let mut rx = self.events_rx.lock().unwrap_or_else(|e| e.into_inner());
2918 let mut out = Vec::new();
2919 loop {
2920 match rx.try_recv() {
2921 Ok(event) => out.push(event),
2922 Err(tokio::sync::broadcast::error::TryRecvError::Empty) => break,
2923 Err(tokio::sync::broadcast::error::TryRecvError::Closed) => break,
2924 Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => continue,
2925 }
2926 }
2927 out
2928 }
2929}
2930
2931impl ToolSpec {
2932 pub fn as_invocable_spec(&self) -> InvocableSpec {
2935 InvocableSpec::new(
2936 CapabilityName::new(self.name.0.clone()),
2937 self.description.clone(),
2938 self.input_schema.clone(),
2939 )
2940 .with_metadata(self.metadata.clone())
2941 }
2942}
2943
2944pub struct ToolInvocableAdapter {
2950 spec: InvocableSpec,
2951 tool: Arc<dyn Tool>,
2952 permissions: Arc<dyn PermissionChecker>,
2953 resources: Arc<dyn ToolResources>,
2954 next_call_id: AtomicU64,
2955}
2956
2957impl ToolInvocableAdapter {
2958 pub fn new(
2961 tool: Arc<dyn Tool>,
2962 permissions: Arc<dyn PermissionChecker>,
2963 resources: Arc<dyn ToolResources>,
2964 ) -> Option<Self> {
2965 let spec = tool.current_spec()?.as_invocable_spec();
2966 Some(Self {
2967 spec,
2968 tool,
2969 permissions,
2970 resources,
2971 next_call_id: AtomicU64::new(1),
2972 })
2973 }
2974}
2975
2976#[async_trait]
2977impl Invocable for ToolInvocableAdapter {
2978 fn spec(&self) -> &InvocableSpec {
2979 &self.spec
2980 }
2981
2982 async fn invoke(
2983 &self,
2984 request: InvocableRequest,
2985 ctx: &mut CapabilityContext<'_>,
2986 ) -> Result<InvocableResult, CapabilityError> {
2987 let tool_request = ToolRequest {
2988 call_id: ToolCallId::new(format!(
2989 "tool-call-{}",
2990 self.next_call_id.fetch_add(1, Ordering::Relaxed)
2991 )),
2992 tool_name: self.tool.spec().name.clone(),
2993 input: request.input,
2994 session_id: ctx
2995 .session_id
2996 .cloned()
2997 .unwrap_or_else(|| SessionId::new("capability-session")),
2998 turn_id: ctx
2999 .turn_id
3000 .cloned()
3001 .unwrap_or_else(|| TurnId::new("capability-turn")),
3002 metadata: request.metadata,
3003 };
3004
3005 for permission_request in self
3006 .tool
3007 .proposed_requests(&tool_request)
3008 .map_err(|error| CapabilityError::InvalidInput(error.to_string()))?
3009 {
3010 match self.permissions.evaluate(permission_request.as_ref()) {
3011 PermissionDecision::Allow => {}
3012 PermissionDecision::Deny(denial) => {
3013 return Err(CapabilityError::ExecutionFailed(format!(
3014 "tool permission denied: {denial:?}"
3015 )));
3016 }
3017 PermissionDecision::RequireApproval(req) => {
3018 return Err(CapabilityError::Unavailable(format!(
3019 "tool invocation requires approval: {}",
3020 req.summary
3021 )));
3022 }
3023 }
3024 }
3025
3026 let mut tool_ctx = ToolContext {
3027 capability: CapabilityContext {
3028 session_id: ctx.session_id,
3029 turn_id: ctx.turn_id,
3030 metadata: ctx.metadata,
3031 },
3032 permissions: self.permissions.as_ref(),
3033 resources: self.resources.as_ref(),
3034 cancellation: None,
3035 };
3036
3037 let result = self
3038 .tool
3039 .invoke(tool_request, &mut tool_ctx)
3040 .await
3041 .map_err(|error| CapabilityError::ExecutionFailed(error.to_string()))?;
3042
3043 Ok(InvocableResult {
3044 output: match result.result.output {
3045 ToolOutput::Text(text) => InvocableOutput::Text(text),
3046 ToolOutput::Structured(value) => InvocableOutput::Structured(value),
3047 ToolOutput::Parts(parts) => InvocableOutput::Items(vec![Item {
3048 id: None,
3049 kind: ItemKind::Tool,
3050 parts,
3051 metadata: MetadataMap::new(),
3052 usage: None,
3053 finish_reason: None,
3054 created_at: None,
3055 }]),
3056 ToolOutput::Files(files) => {
3057 let parts = files.into_iter().map(Part::File).collect();
3058 InvocableOutput::Items(vec![Item {
3059 id: None,
3060 kind: ItemKind::Tool,
3061 parts,
3062 metadata: MetadataMap::new(),
3063 usage: None,
3064 finish_reason: None,
3065 created_at: None,
3066 }])
3067 }
3068 },
3069 metadata: result.metadata,
3070 })
3071 }
3072}
3073
3074pub struct ToolCapabilityProvider {
3080 invocables: Vec<Arc<dyn Invocable>>,
3081}
3082
3083impl ToolCapabilityProvider {
3084 pub fn from_registry(
3087 registry: &ToolRegistry,
3088 permissions: Arc<dyn PermissionChecker>,
3089 resources: Arc<dyn ToolResources>,
3090 ) -> Self {
3091 let invocables = registry
3092 .tools()
3093 .into_iter()
3094 .filter_map(|tool| {
3095 ToolInvocableAdapter::new(tool, permissions.clone(), resources.clone())
3096 .map(|adapter| Arc::new(adapter) as Arc<dyn Invocable>)
3097 })
3098 .collect();
3099
3100 Self { invocables }
3101 }
3102}
3103
3104impl CapabilityProvider for ToolCapabilityProvider {
3105 fn invocables(&self) -> Vec<Arc<dyn Invocable>> {
3106 self.invocables.clone()
3107 }
3108
3109 fn resources(&self) -> Vec<Arc<dyn ResourceProvider>> {
3110 Vec::new()
3111 }
3112
3113 fn prompts(&self) -> Vec<Arc<dyn PromptProvider>> {
3114 Vec::new()
3115 }
3116}
3117
3118#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3124pub enum ToolExecutionOutcome {
3125 Completed(ToolResult),
3127 Interrupted(ToolInterruption),
3129 Failed(ToolError),
3131}
3132
3133#[async_trait]
3141pub trait ToolExecutor: Send + Sync {
3142 fn specs(&self) -> Vec<ToolSpec>;
3144
3145 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
3150 Vec::new()
3151 }
3152
3153 async fn execute(
3155 &self,
3156 request: ToolRequest,
3157 ctx: &mut ToolContext<'_>,
3158 ) -> ToolExecutionOutcome;
3159
3160 async fn execute_owned(
3163 &self,
3164 request: ToolRequest,
3165 ctx: OwnedToolContext,
3166 ) -> ToolExecutionOutcome {
3167 let mut borrowed = ctx.borrowed();
3168 self.execute(request, &mut borrowed).await
3169 }
3170
3171 async fn execute_approved(
3177 &self,
3178 request: ToolRequest,
3179 approved_request: &ApprovalRequest,
3180 ctx: &mut ToolContext<'_>,
3181 ) -> ToolExecutionOutcome {
3182 let _ = approved_request;
3183 self.execute(request, ctx).await
3184 }
3185
3186 async fn execute_approved_owned(
3189 &self,
3190 request: ToolRequest,
3191 approved_request: &ApprovalRequest,
3192 ctx: OwnedToolContext,
3193 ) -> ToolExecutionOutcome {
3194 let mut borrowed = ctx.borrowed();
3195 self.execute_approved(request, approved_request, &mut borrowed)
3196 .await
3197 }
3198}
3199
3200#[async_trait]
3201impl<T> ToolExecutor for Arc<T>
3202where
3203 T: ToolExecutor + ?Sized,
3204{
3205 fn specs(&self) -> Vec<ToolSpec> {
3206 (**self).specs()
3207 }
3208
3209 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
3210 (**self).drain_catalog_events()
3211 }
3212
3213 async fn execute(
3214 &self,
3215 request: ToolRequest,
3216 ctx: &mut ToolContext<'_>,
3217 ) -> ToolExecutionOutcome {
3218 (**self).execute(request, ctx).await
3219 }
3220
3221 async fn execute_approved(
3222 &self,
3223 request: ToolRequest,
3224 approved_request: &ApprovalRequest,
3225 ctx: &mut ToolContext<'_>,
3226 ) -> ToolExecutionOutcome {
3227 (**self)
3228 .execute_approved(request, approved_request, ctx)
3229 .await
3230 }
3231}
3232
3233#[derive(Clone, Debug, Default, PartialEq, Eq)]
3236pub enum CollisionPolicy {
3237 #[default]
3240 FirstWins,
3241 LastWins,
3243}
3244
3245pub struct BasicToolExecutor {
3264 sources: Vec<Arc<dyn ToolSource>>,
3265 collision: CollisionPolicy,
3266 output_truncation: Option<Arc<dyn ToolOutputTruncationStrategy>>,
3267}
3268
3269impl BasicToolExecutor {
3270 pub fn new(sources: impl IntoIterator<Item = Arc<dyn ToolSource>>) -> Self {
3272 Self {
3273 sources: sources.into_iter().collect(),
3274 collision: CollisionPolicy::default(),
3275 output_truncation: None,
3276 }
3277 }
3278
3279 pub fn from_registry(registry: ToolRegistry) -> Self {
3281 Self::new([Arc::new(registry) as Arc<dyn ToolSource>])
3282 }
3283
3284 pub fn with_collision_policy(mut self, policy: CollisionPolicy) -> Self {
3287 self.collision = policy;
3288 self
3289 }
3290
3291 pub fn with_output_truncation_strategy(
3295 mut self,
3296 strategy: impl ToolOutputTruncationStrategy + 'static,
3297 ) -> Self {
3298 self.output_truncation = Some(Arc::new(strategy));
3299 self
3300 }
3301
3302 pub fn with_output_truncation_strategy_arc(
3304 mut self,
3305 strategy: Arc<dyn ToolOutputTruncationStrategy>,
3306 ) -> Self {
3307 self.output_truncation = Some(strategy);
3308 self
3309 }
3310
3311 pub fn specs(&self) -> Vec<ToolSpec> {
3314 let mut seen = BTreeSet::new();
3315 let mut out = Vec::new();
3316 let iter: Box<dyn Iterator<Item = &Arc<dyn ToolSource>>> = match self.collision {
3317 CollisionPolicy::FirstWins => Box::new(self.sources.iter()),
3318 CollisionPolicy::LastWins => Box::new(self.sources.iter().rev()),
3319 };
3320 for source in iter {
3321 for spec in source.specs() {
3322 if seen.insert(spec.name.clone()) {
3323 out.push(spec);
3324 }
3325 }
3326 }
3327 out
3328 }
3329
3330 fn lookup(&self, name: &ToolName) -> Option<Arc<dyn Tool>> {
3331 match self.collision {
3332 CollisionPolicy::FirstWins => self.sources.iter().find_map(|s| s.get(name)),
3333 CollisionPolicy::LastWins => self.sources.iter().rev().find_map(|s| s.get(name)),
3334 }
3335 }
3336
3337 async fn execute_inner(
3338 &self,
3339 request: ToolRequest,
3340 approved_request_id: Option<&ApprovalId>,
3341 ctx: &mut ToolContext<'_>,
3342 ) -> ToolExecutionOutcome {
3343 let Some(tool) = self.lookup(&request.tool_name) else {
3344 return ToolExecutionOutcome::Failed(ToolError::NotFound(request.tool_name));
3345 };
3346
3347 match tool.proposed_requests(&request) {
3348 Ok(requests) => {
3349 for permission_request in requests {
3350 match ctx.permissions.evaluate(permission_request.as_ref()) {
3351 PermissionDecision::Allow => {}
3352 PermissionDecision::Deny(denial) => {
3353 return ToolExecutionOutcome::Failed(ToolError::PermissionDenied(
3354 denial,
3355 ));
3356 }
3357 PermissionDecision::RequireApproval(mut req) => {
3358 req.call_id = Some(request.call_id.clone());
3359 if approved_request_id != Some(&req.id) {
3360 return ToolExecutionOutcome::Interrupted(
3361 ToolInterruption::ApprovalRequired(req),
3362 );
3363 }
3364 }
3365 }
3366 }
3367 }
3368 Err(error) => return ToolExecutionOutcome::Failed(error),
3369 }
3370
3371 let truncation_ctx = ToolOutputTruncationContext::from((&request, tool.spec().clone()));
3372 match tool.invoke(request, ctx).await {
3373 Ok(mut result) => {
3374 if let Some(strategy) = &self.output_truncation {
3375 match strategy.apply(truncation_ctx, result.result.output).await {
3376 Ok(output) => {
3377 result.result.output = output;
3378 }
3379 Err(error) => return ToolExecutionOutcome::Failed(error),
3380 }
3381 }
3382 ToolExecutionOutcome::Completed(result)
3383 }
3384 Err(error) => ToolExecutionOutcome::Failed(error),
3385 }
3386 }
3387}
3388
3389#[async_trait]
3390impl ToolExecutor for BasicToolExecutor {
3391 fn specs(&self) -> Vec<ToolSpec> {
3392 BasicToolExecutor::specs(self)
3393 }
3394
3395 fn drain_catalog_events(&self) -> Vec<ToolCatalogEvent> {
3396 self.sources
3397 .iter()
3398 .flat_map(|s| s.drain_catalog_events())
3399 .collect()
3400 }
3401
3402 async fn execute(
3403 &self,
3404 request: ToolRequest,
3405 ctx: &mut ToolContext<'_>,
3406 ) -> ToolExecutionOutcome {
3407 self.execute_inner(request, None, ctx).await
3408 }
3409
3410 async fn execute_approved(
3411 &self,
3412 request: ToolRequest,
3413 approved_request: &ApprovalRequest,
3414 ctx: &mut ToolContext<'_>,
3415 ) -> ToolExecutionOutcome {
3416 self.execute_inner(request, Some(&approved_request.id), ctx)
3417 .await
3418 }
3419}
3420
3421#[derive(Debug, Error, Clone, PartialEq, Serialize, Deserialize)]
3426pub enum ToolError {
3427 #[error("tool not found: {0}")]
3429 NotFound(ToolName),
3430 #[error("invalid tool input: {0}")]
3432 InvalidInput(String),
3433 #[error("tool permission denied: {0:?}")]
3435 PermissionDenied(PermissionDenial),
3436 #[error("tool execution failed: {0}")]
3438 ExecutionFailed(String),
3439 #[error("tool unavailable: {0}")]
3441 Unavailable(String),
3442 #[error("tool execution cancelled")]
3444 Cancelled,
3445 #[error("internal tool error: {0}")]
3447 Internal(String),
3448}
3449
3450impl ToolError {
3451 pub fn permission_denied(denial: PermissionDenial) -> Self {
3453 Self::PermissionDenied(denial)
3454 }
3455}
3456
3457impl From<PermissionDenial> for ToolError {
3458 fn from(value: PermissionDenial) -> Self {
3459 Self::permission_denied(value)
3460 }
3461}
3462
3463#[cfg(test)]
3464mod tests {
3465 use super::*;
3466 use async_trait::async_trait;
3467 use serde_json::json;
3468
3469 #[test]
3470 fn command_policy_can_deny_unknown_executables_without_approval() {
3471 let policy = CommandPolicy::new()
3472 .allow_executable("pwd")
3473 .require_approval_for_unknown(false);
3474 let request = ShellPermissionRequest {
3475 executable: "rm".into(),
3476 argv: vec!["-rf".into(), "/tmp/demo".into()],
3477 cwd: None,
3478 env_keys: Vec::new(),
3479 metadata: MetadataMap::new(),
3480 };
3481
3482 match policy.evaluate(&request) {
3483 PolicyMatch::Deny(denial) => {
3484 assert_eq!(denial.code, PermissionCode::CommandNotAllowed);
3485 }
3486 other => panic!("unexpected policy match: {other:?}"),
3487 }
3488 }
3489
3490 #[test]
3491 fn path_policy_allows_reads_under_read_only_roots() {
3492 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
3493 let request = FileSystemPermissionRequest::Read {
3494 path: PathBuf::from("/workspace/vendor/lib.rs"),
3495 metadata: MetadataMap::new(),
3496 };
3497
3498 match policy.evaluate(&request) {
3499 PolicyMatch::NoOpinion | PolicyMatch::Allow => {}
3500 other => panic!("unexpected policy match: {other:?}"),
3501 }
3502 }
3503
3504 #[test]
3505 fn path_policy_denies_mutations_under_read_only_roots() {
3506 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
3507 let request = FileSystemPermissionRequest::Edit {
3508 path: PathBuf::from("/workspace/vendor/lib.rs"),
3509 metadata: MetadataMap::new(),
3510 };
3511
3512 match policy.evaluate(&request) {
3513 PolicyMatch::Deny(denial) => {
3514 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3515 assert!(denial.message.contains("read-only"));
3516 }
3517 other => panic!("unexpected policy match: {other:?}"),
3518 }
3519 }
3520
3521 #[test]
3522 fn path_policy_denies_moves_into_read_only_roots() {
3523 let policy = PathPolicy::new().read_only_root("/workspace/vendor");
3524 let request = FileSystemPermissionRequest::Move {
3525 from: PathBuf::from("/workspace/src/lib.rs"),
3526 to: PathBuf::from("/workspace/vendor/lib.rs"),
3527 metadata: MetadataMap::new(),
3528 };
3529
3530 match policy.evaluate(&request) {
3531 PolicyMatch::Deny(denial) => {
3532 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3533 assert!(denial.message.contains("read-only"));
3534 }
3535 other => panic!("unexpected policy match: {other:?}"),
3536 }
3537 }
3538
3539 #[cfg(unix)]
3540 struct SymlinkTmpDir(PathBuf);
3541
3542 #[cfg(unix)]
3543 impl SymlinkTmpDir {
3544 fn new(label: &str) -> Self {
3545 use std::time::{SystemTime, UNIX_EPOCH};
3546 let nanos = SystemTime::now()
3547 .duration_since(UNIX_EPOCH)
3548 .unwrap()
3549 .as_nanos();
3550 let dir = std::env::temp_dir().join(format!(
3551 "agentkit-pathpolicy-{}-{}-{}",
3552 label,
3553 std::process::id(),
3554 nanos
3555 ));
3556 std::fs::create_dir_all(&dir).unwrap();
3557 Self(std::fs::canonicalize(&dir).unwrap())
3560 }
3561
3562 fn path(&self) -> &Path {
3563 &self.0
3564 }
3565 }
3566
3567 #[cfg(unix)]
3568 impl Drop for SymlinkTmpDir {
3569 fn drop(&mut self) {
3570 let _ = std::fs::remove_dir_all(&self.0);
3571 }
3572 }
3573
3574 #[cfg(unix)]
3575 fn assert_path_denied(
3576 policy: &PathPolicy,
3577 request: FileSystemPermissionRequest,
3578 ) -> PermissionDenial {
3579 match policy.evaluate(&request) {
3580 PolicyMatch::Deny(denial) => denial,
3581 other => panic!("expected deny, got: {other:?}"),
3582 }
3583 }
3584
3585 #[cfg(unix)]
3586 #[test]
3587 fn path_policy_blocks_symlink_escape_from_allowed_root() {
3588 let tmp = SymlinkTmpDir::new("allow-escape");
3589 let allowed = tmp.path().join("workspace");
3590 let outside = tmp.path().join("outside");
3591 std::fs::create_dir_all(&allowed).unwrap();
3592 std::fs::create_dir_all(&outside).unwrap();
3593 let secret = outside.join("secret.txt");
3594 std::fs::write(&secret, b"top-secret").unwrap();
3595 let escape = allowed.join("leak");
3596 std::os::unix::fs::symlink(&secret, &escape).unwrap();
3597
3598 let policy = PathPolicy::new()
3599 .allow_root(&allowed)
3600 .require_approval_outside_allowed(false);
3601 let denial = assert_path_denied(
3602 &policy,
3603 FileSystemPermissionRequest::Read {
3604 path: escape,
3605 metadata: MetadataMap::new(),
3606 },
3607 );
3608 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3609 }
3610
3611 #[cfg(unix)]
3612 #[test]
3613 fn path_policy_blocks_symlink_into_protected_root() {
3614 let tmp = SymlinkTmpDir::new("protect-bypass");
3615 let workspace = tmp.path().join("workspace");
3616 std::fs::create_dir_all(&workspace).unwrap();
3617 let secret = workspace.join(".env");
3618 std::fs::write(&secret, b"API_KEY=xxx").unwrap();
3619 let alias = workspace.join("config");
3620 std::os::unix::fs::symlink(&secret, &alias).unwrap();
3621
3622 let policy = PathPolicy::new()
3623 .allow_root(&workspace)
3624 .protect_root(&secret);
3625 let denial = assert_path_denied(
3626 &policy,
3627 FileSystemPermissionRequest::Read {
3628 path: alias,
3629 metadata: MetadataMap::new(),
3630 },
3631 );
3632 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3633 assert!(denial.message.contains("denied"));
3634 }
3635
3636 #[cfg(unix)]
3637 #[test]
3638 fn path_policy_blocks_symlink_write_into_read_only_root() {
3639 let tmp = SymlinkTmpDir::new("readonly-bypass");
3640 let workspace = tmp.path().join("workspace");
3641 let vendor = workspace.join("vendor");
3642 std::fs::create_dir_all(&vendor).unwrap();
3643 let target = vendor.join("lib.rs");
3644 std::fs::write(&target, b"// vendored").unwrap();
3645 let writable_alias = workspace.join("writable");
3646 std::os::unix::fs::symlink(&target, &writable_alias).unwrap();
3647
3648 let policy = PathPolicy::new()
3649 .allow_root(&workspace)
3650 .read_only_root(&vendor);
3651 let denial = assert_path_denied(
3652 &policy,
3653 FileSystemPermissionRequest::Edit {
3654 path: writable_alias,
3655 metadata: MetadataMap::new(),
3656 },
3657 );
3658 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3659 assert!(denial.message.contains("read-only"));
3660 }
3661
3662 #[cfg(unix)]
3663 #[test]
3664 fn path_policy_resolves_symlink_parent_for_nonexistent_leaf() {
3665 let tmp = SymlinkTmpDir::new("create-escape");
3666 let allowed = tmp.path().join("workspace");
3667 let outside = tmp.path().join("outside");
3668 std::fs::create_dir_all(&allowed).unwrap();
3669 std::fs::create_dir_all(&outside).unwrap();
3670 let escape_dir = allowed.join("escape");
3671 std::os::unix::fs::symlink(&outside, &escape_dir).unwrap();
3672 let new_file = escape_dir.join("new.txt");
3673
3674 let policy = PathPolicy::new()
3675 .allow_root(&allowed)
3676 .require_approval_outside_allowed(false);
3677 let denial = assert_path_denied(
3678 &policy,
3679 FileSystemPermissionRequest::Write {
3680 path: new_file,
3681 metadata: MetadataMap::new(),
3682 },
3683 );
3684 assert_eq!(denial.code, PermissionCode::PathNotAllowed);
3685 }
3686
3687 #[derive(Clone)]
3688 struct HiddenTool {
3689 spec: ToolSpec,
3690 }
3691
3692 impl HiddenTool {
3693 fn new() -> Self {
3694 Self {
3695 spec: ToolSpec {
3696 name: ToolName::new("hidden"),
3697 description: "hidden".into(),
3698 input_schema: json!({"type": "object"}),
3699 annotations: ToolAnnotations::default(),
3700 metadata: MetadataMap::new(),
3701 },
3702 }
3703 }
3704 }
3705
3706 #[async_trait]
3707 impl Tool for HiddenTool {
3708 fn spec(&self) -> &ToolSpec {
3709 &self.spec
3710 }
3711
3712 fn current_spec(&self) -> Option<ToolSpec> {
3713 None
3714 }
3715
3716 async fn invoke(
3717 &self,
3718 request: ToolRequest,
3719 _ctx: &mut ToolContext<'_>,
3720 ) -> Result<ToolResult, ToolError> {
3721 Ok(ToolResult {
3722 result: ToolResultPart {
3723 call_id: request.call_id,
3724 output: ToolOutput::Text("hidden".into()),
3725 is_error: false,
3726 metadata: MetadataMap::new(),
3727 },
3728 duration: None,
3729 metadata: MetadataMap::new(),
3730 })
3731 }
3732 }
3733
3734 #[test]
3735 fn hidden_tools_are_omitted_from_specs_and_capabilities() {
3736 let registry = ToolRegistry::new().with(HiddenTool::new());
3737
3738 assert!(registry.specs().is_empty());
3739
3740 let provider = ToolCapabilityProvider::from_registry(
3741 ®istry,
3742 Arc::new(AllowAllPermissionChecker),
3743 Arc::new(()),
3744 );
3745 assert!(provider.invocables().is_empty());
3746 }
3747
3748 struct AllowAllPermissionChecker;
3749
3750 impl PermissionChecker for AllowAllPermissionChecker {
3751 fn evaluate(&self, _request: &dyn PermissionRequest) -> PermissionDecision {
3752 PermissionDecision::Allow
3753 }
3754 }
3755
3756 #[derive(Clone)]
3759 struct PanickingSpecTool {
3760 spec: ToolSpec,
3761 }
3762
3763 impl PanickingSpecTool {
3764 fn new(name: &str) -> Self {
3765 Self {
3766 spec: ToolSpec {
3767 name: ToolName::new(name),
3768 description: "panics on current_spec".into(),
3769 input_schema: json!({"type": "object"}),
3770 annotations: ToolAnnotations::default(),
3771 metadata: MetadataMap::new(),
3772 },
3773 }
3774 }
3775 }
3776
3777 #[async_trait]
3778 impl Tool for PanickingSpecTool {
3779 fn spec(&self) -> &ToolSpec {
3780 &self.spec
3781 }
3782
3783 fn current_spec(&self) -> Option<ToolSpec> {
3784 panic!("PanickingSpecTool::current_spec");
3785 }
3786
3787 async fn invoke(
3788 &self,
3789 request: ToolRequest,
3790 _ctx: &mut ToolContext<'_>,
3791 ) -> Result<ToolResult, ToolError> {
3792 Ok(ToolResult {
3793 result: ToolResultPart {
3794 call_id: request.call_id,
3795 output: ToolOutput::Text("never".into()),
3796 is_error: false,
3797 metadata: MetadataMap::new(),
3798 },
3799 duration: None,
3800 metadata: MetadataMap::new(),
3801 })
3802 }
3803 }
3804
3805 #[test]
3816 fn catalog_recovers_from_panicked_writer() {
3817 let (writer, reader) = dynamic_catalog("test");
3818
3819 writer.upsert(Arc::new(PanickingSpecTool::new("boom")));
3823 let _ = reader.drain_catalog_events();
3824
3825 let panic_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3830 writer.replace_all(vec![
3831 Arc::new(PanickingSpecTool::new("boom")) as Arc<dyn Tool>
3832 ]);
3833 }));
3834 assert!(
3835 panic_result.is_err(),
3836 "PanickingSpecTool::current_spec must propagate"
3837 );
3838
3839 assert!(
3843 reader.get(&ToolName::new("boom")).is_some(),
3844 "catalog still readable after poisoning panic"
3845 );
3846
3847 assert!(writer.remove(&ToolName::new("boom")));
3850
3851 writer.upsert(Arc::new(HiddenTool::new()));
3855 assert!(
3856 reader.get(&ToolName::new("hidden")).is_some(),
3857 "catalog usable for further writes + reads"
3858 );
3859 }
3860
3861 #[derive(Clone)]
3862 struct EchoTool {
3863 spec: ToolSpec,
3864 }
3865
3866 impl EchoTool {
3867 fn new(name: &str) -> Self {
3868 Self {
3869 spec: ToolSpec {
3870 name: ToolName::new(name),
3871 description: format!("echo {name}"),
3872 input_schema: json!({"type": "object"}),
3873 annotations: ToolAnnotations::default(),
3874 metadata: MetadataMap::new(),
3875 },
3876 }
3877 }
3878 }
3879
3880 #[async_trait]
3881 impl Tool for EchoTool {
3882 fn spec(&self) -> &ToolSpec {
3883 &self.spec
3884 }
3885
3886 async fn invoke(
3887 &self,
3888 request: ToolRequest,
3889 _ctx: &mut ToolContext<'_>,
3890 ) -> Result<ToolResult, ToolError> {
3891 Ok(ToolResult::new(ToolResultPart::success(
3892 request.call_id,
3893 ToolOutput::text(request.tool_name.0.clone()),
3894 )))
3895 }
3896 }
3897
3898 fn registry_with(names: &[&str]) -> ToolRegistry {
3899 names.iter().fold(ToolRegistry::new(), |reg, name| {
3900 reg.with(EchoTool::new(name))
3901 })
3902 }
3903
3904 #[test]
3905 fn prefixed_rewrites_specs_and_resolves_lookups() {
3906 let source = registry_with(&["get_temp", "get_humidity"]).prefixed("weather");
3907 let names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
3908 assert_eq!(names, vec!["weather_get_humidity", "weather_get_temp"]);
3909
3910 assert!(source.get(&ToolName::new("weather_get_temp")).is_some());
3911 assert!(
3912 source.get(&ToolName::new("get_temp")).is_none(),
3913 "original name must not resolve when prefixed"
3914 );
3915 assert!(source.get(&ToolName::new("unknown")).is_none());
3916 }
3917
3918 #[tokio::test]
3919 async fn prefixed_invoke_sees_inner_name_on_request() {
3920 let source = registry_with(&["get_temp"]).prefixed("weather");
3921 let tool = source.get(&ToolName::new("weather_get_temp")).unwrap();
3922
3923 assert_eq!(tool.spec().name.0, "weather_get_temp");
3925
3926 let owned = OwnedToolContext {
3928 session_id: SessionId::new("s"),
3929 turn_id: TurnId::new("t"),
3930 metadata: MetadataMap::new(),
3931 permissions: Arc::new(AllowAllPermissions),
3932 resources: Arc::new(()),
3933 cancellation: None,
3934 };
3935 let mut ctx = owned.borrowed();
3936 let request = ToolRequest {
3937 call_id: ToolCallId::new("c"),
3938 tool_name: ToolName::new("weather_get_temp"),
3939 input: json!({}),
3940 session_id: SessionId::new("s"),
3941 turn_id: TurnId::new("t"),
3942 metadata: MetadataMap::new(),
3943 };
3944 let result = tool.invoke(request, &mut ctx).await.unwrap();
3945 match result.result.output {
3946 ToolOutput::Text(text) => assert_eq!(text, "get_temp"),
3947 other => panic!("unexpected output: {other:?}"),
3948 }
3949 }
3950
3951 #[derive(Clone)]
3952 struct StaticOutputTool {
3953 spec: ToolSpec,
3954 output: ToolOutput,
3955 }
3956
3957 impl StaticOutputTool {
3958 fn new(name: &str, output: ToolOutput) -> Self {
3959 Self {
3960 spec: ToolSpec::new(name, format!("static {name}"), json!({"type": "object"})),
3961 output,
3962 }
3963 }
3964
3965 fn with_output_limit(mut self, limit: ToolOutputLimit) -> Self {
3966 self.spec = self.spec.with_output_limit(limit);
3967 self
3968 }
3969 }
3970
3971 #[async_trait]
3972 impl Tool for StaticOutputTool {
3973 fn spec(&self) -> &ToolSpec {
3974 &self.spec
3975 }
3976
3977 async fn invoke(
3978 &self,
3979 request: ToolRequest,
3980 _ctx: &mut ToolContext<'_>,
3981 ) -> Result<ToolResult, ToolError> {
3982 Ok(ToolResult::new(ToolResultPart::success(
3983 request.call_id,
3984 self.output.clone(),
3985 )))
3986 }
3987 }
3988
3989 fn test_context() -> OwnedToolContext {
3990 OwnedToolContext {
3991 session_id: SessionId::new("s"),
3992 turn_id: TurnId::new("t"),
3993 metadata: MetadataMap::new(),
3994 permissions: Arc::new(AllowAllPermissions),
3995 resources: Arc::new(()),
3996 cancellation: None,
3997 }
3998 }
3999
4000 #[tokio::test]
4001 async fn executor_stores_oversized_output_using_tool_metadata_limit() {
4002 let store = Arc::new(InMemoryToolOutputArtifactStore::new());
4003 let strategy = ConfigurableToolOutputTruncationStrategy::new(store.clone());
4004 let tool = StaticOutputTool::new("big", ToolOutput::text("x".repeat(500)))
4005 .with_output_limit(ToolOutputLimit::store_for_readback(300));
4006 let executor = BasicToolExecutor::from_registry(ToolRegistry::new().with(tool))
4007 .with_output_truncation_strategy(strategy);
4008
4009 let outcome = executor
4010 .execute_owned(
4011 ToolRequest::new(
4012 "call",
4013 "big",
4014 json!({}),
4015 SessionId::new("s"),
4016 TurnId::new("t"),
4017 ),
4018 test_context(),
4019 )
4020 .await;
4021
4022 let ToolExecutionOutcome::Completed(result) = outcome else {
4023 panic!("expected completed outcome, got {outcome:?}");
4024 };
4025 let ToolOutput::Structured(envelope) = result.result.output else {
4026 panic!("expected truncation envelope");
4027 };
4028 assert_eq!(envelope["truncated"], true);
4029 assert_eq!(envelope["read_tool"], TOOL_RESULT_READ_TOOL_NAME);
4030 let id = envelope["tool_result_id"].as_str().expect("tool_result_id");
4031
4032 let slice = store
4033 .read(&ToolOutputArtifactId(id.to_string()), 0, 50)
4034 .await
4035 .expect("read artifact");
4036 assert_eq!(slice.content, "x".repeat(50));
4037 assert_eq!(slice.next_offset, 50);
4038 assert!(!slice.eof);
4039 }
4040
4041 #[tokio::test]
4042 async fn tool_result_read_enforces_explicit_max_read_size() {
4043 let store = Arc::new(InMemoryToolOutputArtifactStore::new());
4044 let spec = ToolSpec::new("big", "big output", json!({"type": "object"}));
4045 let request = ToolRequest::new(
4046 "call",
4047 "big",
4048 json!({}),
4049 SessionId::new("s"),
4050 TurnId::new("t"),
4051 );
4052 let ctx = ToolOutputTruncationContext::from((&request, spec));
4053 let artifact = store
4054 .put(&ctx, "abcdef".to_string(), 6)
4055 .await
4056 .expect("store artifact");
4057 let tool = ToolResultReadTool::new(store, 4);
4058 let owned_ctx = test_context();
4059 let mut tool_ctx = owned_ctx.borrowed();
4060
4061 let err = tool
4062 .invoke(
4063 ToolRequest::new(
4064 "read-call",
4065 TOOL_RESULT_READ_TOOL_NAME,
4066 json!({"id": artifact.id.0, "offset": 0, "limit": 5}),
4067 SessionId::new("s"),
4068 TurnId::new("t"),
4069 ),
4070 &mut tool_ctx,
4071 )
4072 .await
4073 .expect_err("read past max must fail");
4074 match err {
4075 ToolError::InvalidInput(message) => assert!(message.contains("exceeds maximum")),
4076 other => panic!("expected InvalidInput, got {other:?}"),
4077 }
4078 }
4079
4080 #[tokio::test]
4081 async fn tool_result_read_rejects_zero_limit() {
4082 let store = Arc::new(InMemoryToolOutputArtifactStore::new());
4083 let spec = ToolSpec::new("big", "big output", json!({"type": "object"}));
4084 let request = ToolRequest::new(
4085 "call",
4086 "big",
4087 json!({}),
4088 SessionId::new("s"),
4089 TurnId::new("t"),
4090 );
4091 let ctx = ToolOutputTruncationContext::from((&request, spec));
4092 let artifact = store
4093 .put(&ctx, "abcdef".to_string(), 6)
4094 .await
4095 .expect("store artifact");
4096 let tool = ToolResultReadTool::new(store, 4);
4097 let owned_ctx = test_context();
4098 let mut tool_ctx = owned_ctx.borrowed();
4099
4100 let err = tool
4101 .invoke(
4102 ToolRequest::new(
4103 "read-call",
4104 TOOL_RESULT_READ_TOOL_NAME,
4105 json!({"id": artifact.id.0, "offset": 0, "limit": 0}),
4106 SessionId::new("s"),
4107 TurnId::new("t"),
4108 ),
4109 &mut tool_ctx,
4110 )
4111 .await
4112 .expect_err("zero limit must fail");
4113 match err {
4114 ToolError::InvalidInput(message) => assert!(message.contains("greater than 0")),
4115 other => panic!("expected InvalidInput, got {other:?}"),
4116 }
4117 }
4118
4119 #[tokio::test]
4120 async fn tool_result_read_executor_allows_full_content_limit_with_envelope() {
4121 let store = Arc::new(InMemoryToolOutputArtifactStore::new());
4122 let spec = ToolSpec::new("big", "big output", json!({"type": "object"}));
4123 let request = ToolRequest::new(
4124 "call",
4125 "big",
4126 json!({}),
4127 SessionId::new("s"),
4128 TurnId::new("t"),
4129 );
4130 let ctx = ToolOutputTruncationContext::from((&request, spec));
4131 let artifact = store
4132 .put(&ctx, "abcd".to_string(), 4)
4133 .await
4134 .expect("store artifact");
4135 let executor = BasicToolExecutor::from_registry(
4136 ToolRegistry::new().with(ToolResultReadTool::new(store.clone(), 4)),
4137 )
4138 .with_output_truncation_strategy(ConfigurableToolOutputTruncationStrategy::new(store));
4139
4140 let outcome = executor
4141 .execute_owned(
4142 ToolRequest::new(
4143 "read-call",
4144 TOOL_RESULT_READ_TOOL_NAME,
4145 json!({"id": artifact.id.0, "offset": 0, "limit": 4}),
4146 SessionId::new("s"),
4147 TurnId::new("t"),
4148 ),
4149 test_context(),
4150 )
4151 .await;
4152
4153 let ToolExecutionOutcome::Completed(result) = outcome else {
4154 panic!("expected completed outcome, got {outcome:?}");
4155 };
4156 let ToolOutput::Structured(output) = result.result.output else {
4157 panic!("expected structured readback output");
4158 };
4159 assert_eq!(output["content"], "abcd");
4160 assert_eq!(output["eof"], true);
4161 }
4162
4163 #[tokio::test]
4164 async fn tool_result_read_executor_allows_json_escaped_full_content_limit() {
4165 let store = Arc::new(InMemoryToolOutputArtifactStore::new());
4166 let spec = ToolSpec::new("big", "big output", json!({"type": "object"}));
4167 let request = ToolRequest::new(
4168 "call",
4169 "big",
4170 json!({}),
4171 SessionId::new("s"),
4172 TurnId::new("t"),
4173 );
4174 let ctx = ToolOutputTruncationContext::from((&request, spec));
4175 let content = "\0".repeat(4);
4176 let artifact = store
4177 .put(&ctx, content.clone(), content.len())
4178 .await
4179 .expect("store artifact");
4180 let executor = BasicToolExecutor::from_registry(
4181 ToolRegistry::new().with(ToolResultReadTool::new(store.clone(), 4)),
4182 )
4183 .with_output_truncation_strategy(ConfigurableToolOutputTruncationStrategy::new(store));
4184
4185 let outcome = executor
4186 .execute_owned(
4187 ToolRequest::new(
4188 "read-call",
4189 TOOL_RESULT_READ_TOOL_NAME,
4190 json!({"id": artifact.id.0, "offset": 0, "limit": 4}),
4191 SessionId::new("s"),
4192 TurnId::new("t"),
4193 ),
4194 test_context(),
4195 )
4196 .await;
4197
4198 let ToolExecutionOutcome::Completed(result) = outcome else {
4199 panic!("expected completed outcome, got {outcome:?}");
4200 };
4201 let ToolOutput::Structured(output) = result.result.output else {
4202 panic!("expected structured readback output");
4203 };
4204 assert_eq!(output["content"], content);
4205 assert_eq!(output["eof"], true);
4206 }
4207
4208 #[test]
4209 fn inline_clip_respects_limit_when_marker_exceeds_budget() {
4210 let clipped = clip_string_with_marker("abcdef", 8, 1000);
4211
4212 assert!(clipped.len() <= 8);
4213 assert!(clipped.is_char_boundary(clipped.len()));
4214 }
4215
4216 #[test]
4217 fn filtered_hides_tools_rejected_by_predicate() {
4218 let source = registry_with(&["safe", "danger_drop", "danger_delete"])
4219 .filtered(|name| !name.0.starts_with("danger_"));
4220 let names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
4221 assert_eq!(names, vec!["safe"]);
4222
4223 assert!(source.get(&ToolName::new("safe")).is_some());
4224 assert!(source.get(&ToolName::new("danger_drop")).is_none());
4225 }
4226
4227 #[test]
4228 fn renamed_remaps_specs_and_lookups() {
4229 let source = registry_with(&["legacy_name", "passthrough"])
4230 .renamed([(ToolName::new("legacy_name"), ToolName::new("modern_name"))]);
4231 let mut names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
4232 names.sort();
4233 assert_eq!(names, vec!["modern_name", "passthrough"]);
4234
4235 assert!(source.get(&ToolName::new("modern_name")).is_some());
4236 assert!(
4237 source.get(&ToolName::new("legacy_name")).is_none(),
4238 "original name is hidden after renaming"
4239 );
4240 assert!(source.get(&ToolName::new("passthrough")).is_some());
4241 }
4242
4243 #[cfg(feature = "schemars")]
4244 mod schemars_helpers {
4245 use super::*;
4246 use schemars::JsonSchema;
4247 use serde::Deserialize;
4248
4249 #[derive(JsonSchema, Deserialize)]
4250 #[allow(dead_code)]
4251 struct WeatherInput {
4252 location: String,
4254 #[serde(default)]
4256 celsius: bool,
4257 }
4258
4259 #[test]
4260 fn schema_for_emits_object_schema_with_typed_fields() {
4261 let schema = schema_for::<WeatherInput>();
4262 let obj = schema.as_object().expect("schema is a JSON object");
4263 assert_eq!(
4264 obj.get("type").and_then(|v| v.as_str()),
4265 Some("object"),
4266 "root type should be object"
4267 );
4268 let properties = obj
4269 .get("properties")
4270 .and_then(|v| v.as_object())
4271 .expect("properties block");
4272 assert!(properties.contains_key("location"));
4273 assert!(properties.contains_key("celsius"));
4274 }
4275
4276 #[test]
4277 fn tool_spec_for_carries_schema_name_and_description() {
4278 let spec = tool_spec_for::<WeatherInput>("get_weather", "Fetch current weather");
4279 assert_eq!(spec.name.0, "get_weather");
4280 assert_eq!(spec.description, "Fetch current weather");
4281 assert!(spec.input_schema.is_object());
4282 }
4283 }
4284
4285 #[test]
4286 fn transforms_compose_via_chained_methods() {
4287 let source = registry_with(&["read_file", "write_file", "delete_file"])
4288 .filtered(|name| name.0 != "delete_file")
4289 .prefixed("fs");
4290 let mut names: Vec<_> = source.specs().into_iter().map(|s| s.name.0).collect();
4291 names.sort();
4292 assert_eq!(names, vec!["fs_read_file", "fs_write_file"]);
4293 }
4294}