1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::Path;
5use thiserror::Error;
6
7pub const TOOL_SEP: char = ':';
10pub const TOOL_SEP_STR: &str = ":";
11
12#[derive(Error, Debug)]
13pub enum ManifestError {
14 #[error("Failed to read manifest file {0}: {1}")]
15 Io(String, std::io::Error),
16 #[error("Failed to parse manifest {0}: {1}")]
17 Parse(String, toml::de::Error),
18 #[error("No manifests directory found at {0}")]
19 NoDirectory(String),
20 #[error("Manifest {0} is invalid: {1}")]
21 Invalid(String, String),
22}
23
24#[derive(Debug, Clone, Deserialize)]
25#[serde(rename_all = "snake_case")]
26#[derive(Default)]
27pub enum AuthType {
28 Bearer,
29 Header,
30 Query,
31 Basic,
32 #[default]
33 None,
34 Oauth2,
35 Url,
40}
41
42#[derive(Debug, Clone, Deserialize)]
43pub struct Provider {
44 pub name: String,
45 pub description: String,
46 #[serde(default)]
48 pub base_url: String,
49 #[serde(default)]
50 pub auth_type: AuthType,
51 #[serde(default)]
52 pub auth_key_name: Option<String>,
53 #[serde(default)]
56 pub auth_header_name: Option<String>,
57 #[serde(default)]
59 pub auth_query_name: Option<String>,
60 #[serde(default)]
63 pub auth_value_prefix: Option<String>,
64 #[serde(default)]
67 pub extra_headers: HashMap<String, String>,
68 #[serde(default)]
70 pub oauth2_token_url: Option<String>,
71 #[serde(default)]
73 pub auth_secret_name: Option<String>,
74 #[serde(default)]
77 pub oauth2_basic_auth: bool,
78 #[serde(default)]
79 pub internal: bool,
80 #[serde(default = "default_handler")]
81 pub handler: String,
82
83 #[serde(default)]
86 pub mcp_transport: Option<String>,
87 #[serde(default)]
89 pub mcp_command: Option<String>,
90 #[serde(default)]
92 pub mcp_args: Vec<String>,
93 #[serde(default)]
95 pub mcp_url: Option<String>,
96 #[serde(default)]
98 pub mcp_env: HashMap<String, String>,
99
100 #[serde(default)]
103 pub cli_command: Option<String>,
104 #[serde(default)]
106 pub cli_default_args: Vec<String>,
107 #[serde(default)]
109 pub cli_env: HashMap<String, String>,
110 #[serde(default)]
112 pub cli_timeout_secs: Option<u64>,
113 #[serde(default)]
119 pub cli_output_args: Vec<String>,
120 #[serde(default)]
125 pub cli_output_positional: HashMap<String, usize>,
126
127 #[serde(default)]
133 pub upload_destinations: HashMap<String, crate::core::file_manager::UploadDestination>,
134 #[serde(default)]
137 pub upload_default_destination: Option<String>,
138
139 #[serde(default)]
142 pub openapi_spec: Option<String>,
143 #[serde(default)]
145 pub openapi_include_tags: Vec<String>,
146 #[serde(default)]
148 pub openapi_exclude_tags: Vec<String>,
149 #[serde(default)]
151 pub openapi_include_operations: Vec<String>,
152 #[serde(default)]
154 pub openapi_exclude_operations: Vec<String>,
155 #[serde(default)]
157 pub openapi_max_operations: Option<usize>,
158 #[serde(default)]
160 pub openapi_overrides: HashMap<String, OpenApiToolOverride>,
161
162 #[serde(default)]
166 pub auth_generator: Option<AuthGenerator>,
167
168 #[serde(default)]
171 pub category: Option<String>,
172
173 #[serde(default)]
176 pub skills: Vec<String>,
177}
178
179fn default_handler() -> String {
180 "http".to_string()
181}
182
183#[derive(Debug, Clone, Deserialize, Default)]
185pub struct OpenApiToolOverride {
186 pub hint: Option<String>,
187 #[serde(default)]
188 pub tags: Vec<String>,
189 #[serde(default)]
190 pub examples: Vec<String>,
191 pub description: Option<String>,
192 pub scope: Option<String>,
193 pub response_extract: Option<String>,
194 pub response_format: Option<String>,
195}
196
197#[derive(Debug, Clone, Deserialize)]
210pub struct AuthGenerator {
211 #[serde(rename = "type")]
212 pub gen_type: AuthGenType,
213 pub command: Option<String>,
215 #[serde(default)]
217 pub args: Vec<String>,
218 pub interpreter: Option<String>,
220 pub script: Option<String>,
222 #[serde(default)]
224 pub cache_ttl_secs: u64,
225 #[serde(default)]
227 pub output_format: AuthOutputFormat,
228 #[serde(default)]
230 pub env: HashMap<String, String>,
231 #[serde(default)]
233 pub inject: HashMap<String, InjectTarget>,
234 #[serde(default = "default_gen_timeout")]
236 pub timeout_secs: u64,
237}
238
239fn default_gen_timeout() -> u64 {
240 30
241}
242
243#[derive(Debug, Clone, Deserialize)]
244#[serde(rename_all = "snake_case")]
245pub enum AuthGenType {
246 Command,
247 Script,
248}
249
250#[derive(Debug, Clone, Deserialize, Default)]
251#[serde(rename_all = "snake_case")]
252pub enum AuthOutputFormat {
253 #[default]
254 Text,
255 Json,
256}
257
258#[derive(Debug, Clone, Deserialize)]
260pub struct InjectTarget {
261 #[serde(rename = "type")]
263 pub inject_type: String,
264 pub name: String,
266}
267
268#[derive(Debug, Clone, Deserialize)]
269#[serde(rename_all = "UPPERCASE")]
270#[derive(Default)]
271pub enum HttpMethod {
272 #[serde(alias = "get", alias = "Get")]
273 #[default]
274 Get,
275 #[serde(alias = "post", alias = "Post")]
276 Post,
277 #[serde(alias = "put", alias = "Put")]
278 Put,
279 #[serde(alias = "delete", alias = "Delete")]
280 Delete,
281}
282
283impl std::fmt::Display for HttpMethod {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 match self {
286 HttpMethod::Get => write!(f, "GET"),
287 HttpMethod::Post => write!(f, "POST"),
288 HttpMethod::Put => write!(f, "PUT"),
289 HttpMethod::Delete => write!(f, "DELETE"),
290 }
291 }
292}
293
294#[derive(Debug, Clone, Deserialize, Default)]
295#[serde(rename_all = "snake_case")]
296pub enum ResponseFormat {
297 MarkdownTable,
298 Json,
299 #[default]
300 Text,
301 Raw,
302}
303
304#[derive(Debug, Clone, Deserialize, Default)]
305pub struct ResponseConfig {
306 #[serde(default)]
308 pub extract: Option<String>,
309 #[serde(default)]
311 pub format: ResponseFormat,
312}
313
314#[derive(Debug, Clone, Deserialize)]
315pub struct Tool {
316 pub name: String,
317 pub description: String,
318 #[serde(default)]
319 pub endpoint: String,
320 #[serde(default)]
321 pub method: HttpMethod,
322 #[serde(default)]
324 pub scope: Option<String>,
325 #[serde(default)]
327 pub input_schema: Option<serde_json::Value>,
328 #[serde(default)]
330 pub response: Option<ResponseConfig>,
331
332 #[serde(default)]
335 pub tags: Vec<String>,
336 #[serde(default)]
338 pub hint: Option<String>,
339 #[serde(default)]
341 pub examples: Vec<String>,
342}
343
344#[derive(Debug, Clone, Deserialize)]
347pub struct Manifest {
348 pub provider: Provider,
349 #[serde(default, rename = "tools")]
350 pub tools: Vec<Tool>,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct CachedProvider {
358 pub name: String,
359 pub provider_type: String,
361 #[serde(default)]
362 pub base_url: String,
363 #[serde(default)]
364 pub auth_type: String,
365 #[serde(default)]
366 pub auth_key_name: Option<String>,
367 #[serde(default)]
368 pub auth_header_name: Option<String>,
369 #[serde(default)]
370 pub auth_query_name: Option<String>,
371 #[serde(default)]
373 pub spec_content: Option<String>,
374 #[serde(default)]
376 pub mcp_transport: Option<String>,
377 #[serde(default)]
378 pub mcp_url: Option<String>,
379 #[serde(default)]
380 pub mcp_command: Option<String>,
381 #[serde(default)]
382 pub mcp_args: Vec<String>,
383 #[serde(default)]
384 pub mcp_env: HashMap<String, String>,
385 #[serde(default)]
387 pub cli_command: Option<String>,
388 #[serde(default)]
389 pub cli_default_args: Vec<String>,
390 #[serde(default)]
391 pub cli_env: HashMap<String, String>,
392 #[serde(default)]
393 pub cli_timeout_secs: Option<u64>,
394 #[serde(default)]
396 pub auth: Option<String>,
397 #[serde(default)]
399 pub skills: Vec<String>,
400 pub created_at: String,
402 pub ttl_seconds: u64,
403}
404
405impl CachedProvider {
406 pub fn is_expired(&self) -> bool {
408 let created = match DateTime::parse_from_rfc3339(&self.created_at) {
409 Ok(dt) => dt.with_timezone(&Utc),
410 Err(_) => return true, };
412 let now = Utc::now();
413 let elapsed = now.signed_duration_since(created);
414 elapsed.num_seconds() as u64 > self.ttl_seconds
415 }
416
417 pub fn expires_at(&self) -> Option<String> {
419 let created = DateTime::parse_from_rfc3339(&self.created_at).ok()?;
420 let expires = created + chrono::Duration::seconds(self.ttl_seconds as i64);
421 Some(expires.to_rfc3339())
422 }
423
424 pub fn remaining_seconds(&self) -> u64 {
426 let created = match DateTime::parse_from_rfc3339(&self.created_at) {
427 Ok(dt) => dt.with_timezone(&Utc),
428 Err(_) => return 0,
429 };
430 let now = Utc::now();
431 let elapsed = now.signed_duration_since(created).num_seconds() as u64;
432 self.ttl_seconds.saturating_sub(elapsed)
433 }
434
435 pub fn to_provider(&self) -> Provider {
437 let auth_type = match self.auth_type.as_str() {
438 "bearer" => AuthType::Bearer,
439 "header" => AuthType::Header,
440 "query" => AuthType::Query,
441 "basic" => AuthType::Basic,
442 "oauth2" => AuthType::Oauth2,
443 _ => AuthType::None,
444 };
445
446 let handler = match self.provider_type.as_str() {
447 "mcp" => "mcp".to_string(),
448 "openapi" => "openapi".to_string(),
449 _ => "http".to_string(),
450 };
451
452 Provider {
453 name: self.name.clone(),
454 description: format!("{} (cached)", self.name),
455 base_url: self.base_url.clone(),
456 auth_type,
457 auth_key_name: self.auth_key_name.clone(),
458 auth_header_name: self.auth_header_name.clone(),
459 auth_query_name: self.auth_query_name.clone(),
460 auth_value_prefix: None,
461 extra_headers: HashMap::new(),
462 oauth2_token_url: None,
463 auth_secret_name: None,
464 oauth2_basic_auth: false,
465 internal: false,
466 handler,
467 mcp_transport: self.mcp_transport.clone(),
468 mcp_command: self.mcp_command.clone(),
469 mcp_args: self.mcp_args.clone(),
470 mcp_url: self.mcp_url.clone(),
471 mcp_env: self.mcp_env.clone(),
472 openapi_spec: None,
473 openapi_include_tags: Vec::new(),
474 openapi_exclude_tags: Vec::new(),
475 openapi_include_operations: Vec::new(),
476 openapi_exclude_operations: Vec::new(),
477 openapi_max_operations: None,
478 openapi_overrides: HashMap::new(),
479 cli_command: self.cli_command.clone(),
480 cli_default_args: self.cli_default_args.clone(),
481 cli_env: self.cli_env.clone(),
482 cli_timeout_secs: self.cli_timeout_secs,
483 cli_output_args: Vec::new(),
484 cli_output_positional: HashMap::new(),
485 upload_destinations: HashMap::new(),
486 upload_default_destination: None,
487 auth_generator: None,
488 category: None,
489 skills: self.skills.clone(),
490 }
491 }
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct McpToolDef {
498 pub name: String,
499 #[serde(default)]
500 pub description: Option<String>,
501 #[serde(default, rename = "inputSchema")]
502 pub input_schema: Option<serde_json::Value>,
503}
504
505pub struct ManifestRegistry {
507 manifests: Vec<Manifest>,
508 tool_index: HashMap<String, (usize, usize)>,
510}
511
512impl ManifestRegistry {
513 pub fn load(dir: &Path) -> Result<Self, ManifestError> {
516 if !dir.is_dir() {
517 return Err(ManifestError::NoDirectory(dir.display().to_string()));
518 }
519
520 let mut manifests = Vec::new();
521 let mut tool_index = HashMap::new();
522
523 let pattern = dir.join("*.toml");
524 let entries = glob::glob(pattern.to_str().unwrap_or(""))
525 .map_err(|e| ManifestError::NoDirectory(e.to_string()))?;
526
527 let specs_dir = dir.parent().map(|p| p.join("specs"));
529
530 for entry in entries {
531 let path = entry.map_err(|e| {
532 ManifestError::Io(format!("{e}"), std::io::Error::other("glob error"))
533 })?;
534 let contents = std::fs::read_to_string(&path)
535 .map_err(|e| ManifestError::Io(path.display().to_string(), e))?;
536 let mut manifest: Manifest = toml::from_str(&contents)
537 .map_err(|e| ManifestError::Parse(path.display().to_string(), e))?;
538
539 if manifest.provider.is_openapi() {
541 if let Some(spec_ref) = &manifest.provider.openapi_spec {
542 match crate::core::openapi::load_and_register(
543 &manifest.provider,
544 spec_ref,
545 specs_dir.as_deref(),
546 ) {
547 Ok(tools) => {
548 manifest.tools = tools;
549 }
550 Err(e) => {
551 tracing::warn!(
552 provider = %manifest.provider.name,
553 error = %e,
554 "failed to load OpenAPI spec for provider"
555 );
556 }
558 }
559 }
560 }
561
562 if manifest.provider.handler == "file_manager" {
566 if let Some(ref default) = manifest.provider.upload_default_destination {
567 if !manifest.provider.upload_destinations.contains_key(default) {
568 return Err(ManifestError::Invalid(
569 path.display().to_string(),
570 format!(
571 "upload_default_destination '{default}' is not present in [provider.upload_destinations]"
572 ),
573 ));
574 }
575 }
576 }
577
578 if manifest.provider.is_cli() && manifest.tools.is_empty() {
580 let tool_name = manifest.provider.name.clone();
581 manifest.tools.push(Tool {
582 name: tool_name.clone(),
583 description: manifest.provider.description.clone(),
584 endpoint: String::new(),
585 method: HttpMethod::Get,
586 scope: Some(format!("tool:{tool_name}")),
587 input_schema: None,
588 response: None,
589 tags: Vec::new(),
590 hint: None,
591 examples: Vec::new(),
592 });
593 }
594
595 let provider_name = &manifest.provider.name;
598 for tool in &mut manifest.tools {
599 if tool.scope.is_none() && !manifest.provider.internal {
600 tool.scope = Some(format!("tool:{}", tool.name));
601 tracing::trace!(
602 tool = %tool.name,
603 provider = %provider_name,
604 scope = ?tool.scope,
605 "auto-assigned scope to tool"
606 );
607 }
608 }
609
610 let mi = manifests.len();
611 for (ti, tool) in manifest.tools.iter().enumerate() {
612 tool_index.insert(tool.name.clone(), (mi, ti));
613 }
614 manifests.push(manifest);
615 }
616
617 if let Some(parent) = dir.parent() {
620 let cache_dir = parent.join("cache").join("providers");
621 if cache_dir.is_dir() {
622 let cache_pattern = cache_dir.join("*.json");
623 if let Ok(cache_entries) = glob::glob(cache_pattern.to_str().unwrap_or("")) {
624 for entry in cache_entries {
625 let path = match entry {
626 Ok(p) => p,
627 Err(_) => continue,
628 };
629 let content = match std::fs::read_to_string(&path) {
630 Ok(c) => c,
631 Err(_) => continue,
632 };
633 let cached: CachedProvider = match serde_json::from_str(&content) {
634 Ok(c) => c,
635 Err(_) => continue,
636 };
637
638 if cached.is_expired() {
640 let _ = std::fs::remove_file(&path);
641 continue;
642 }
643
644 if manifests.iter().any(|m| m.provider.name == cached.name) {
646 continue;
647 }
648
649 let provider = cached.to_provider();
650
651 let mut cached_tools = Vec::new();
652 if cached.provider_type == "openapi" {
653 if let Some(spec_content) = &cached.spec_content {
654 if let Ok(spec) = crate::core::openapi::parse_spec(spec_content) {
655 let filters = crate::core::openapi::OpenApiFilters {
656 include_tags: vec![],
657 exclude_tags: vec![],
658 include_operations: vec![],
659 exclude_operations: vec![],
660 max_operations: None,
661 };
662 let defs = crate::core::openapi::extract_tools(&spec, &filters);
663 cached_tools = defs
664 .into_iter()
665 .map(|def| {
666 crate::core::openapi::to_ati_tool(
667 def,
668 &cached.name,
669 &HashMap::new(),
670 )
671 })
672 .collect();
673 }
674 }
675 }
676 let mi = manifests.len();
679 for (ti, tool) in cached_tools.iter().enumerate() {
680 tool_index.insert(tool.name.clone(), (mi, ti));
681 }
682 manifests.push(Manifest {
683 provider,
684 tools: cached_tools,
685 });
686 }
687 }
688 }
689 }
690
691 let mut registry = ManifestRegistry {
692 manifests,
693 tool_index,
694 };
695 register_file_manager_provider(&mut registry);
696 Ok(registry)
697 }
698
699 pub fn empty() -> Self {
701 let mut registry = ManifestRegistry {
702 manifests: Vec::new(),
703 tool_index: HashMap::new(),
704 };
705 register_file_manager_provider(&mut registry);
706 registry
707 }
708
709 pub fn get_tool(&self, name: &str) -> Option<(&Provider, &Tool)> {
711 self.tool_index.get(name).map(|(mi, ti)| {
712 let m = &self.manifests[*mi];
713 (&m.provider, &m.tools[*ti])
714 })
715 }
716
717 pub fn list_tools(&self) -> Vec<(&Provider, &Tool)> {
719 self.manifests
720 .iter()
721 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
722 .collect()
723 }
724
725 pub fn list_providers(&self) -> Vec<&Provider> {
727 self.manifests.iter().map(|m| &m.provider).collect()
728 }
729
730 pub fn list_public_tools(&self) -> Vec<(&Provider, &Tool)> {
732 self.manifests
733 .iter()
734 .filter(|m| !m.provider.internal)
735 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
736 .collect()
737 }
738
739 pub fn tool_count(&self) -> usize {
741 self.tool_index.len()
742 }
743
744 pub fn provider_count(&self) -> usize {
746 self.manifests.len()
747 }
748
749 pub fn list_mcp_providers(&self) -> Vec<&Provider> {
751 self.manifests
752 .iter()
753 .filter(|m| m.provider.handler == "mcp")
754 .map(|m| &m.provider)
755 .collect()
756 }
757
758 pub fn find_mcp_provider_for_tool(&self, tool_name: &str) -> Option<&Provider> {
760 let prefix = tool_name.split(TOOL_SEP).next()?;
761 self.manifests
762 .iter()
763 .find(|m| m.provider.handler == "mcp" && m.provider.name == prefix)
764 .map(|m| &m.provider)
765 }
766
767 pub fn list_openapi_providers(&self) -> Vec<&Provider> {
769 self.manifests
770 .iter()
771 .filter(|m| m.provider.handler == "openapi")
772 .map(|m| &m.provider)
773 .collect()
774 }
775
776 pub fn has_provider(&self, name: &str) -> bool {
778 self.manifests.iter().any(|m| m.provider.name == name)
779 }
780
781 pub fn tools_by_provider(&self, provider_name: &str) -> Vec<(&Provider, &Tool)> {
783 self.manifests
784 .iter()
785 .filter(|m| m.provider.name == provider_name)
786 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
787 .collect()
788 }
789
790 pub fn list_cli_providers(&self) -> Vec<&Provider> {
792 self.manifests
793 .iter()
794 .filter(|m| m.provider.handler == "cli")
795 .map(|m| &m.provider)
796 .collect()
797 }
798
799 pub fn register_mcp_tools(&mut self, provider_name: &str, mcp_tools: Vec<McpToolDef>) {
802 let mi = match self
804 .manifests
805 .iter()
806 .position(|m| m.provider.name == provider_name)
807 {
808 Some(idx) => idx,
809 None => return,
810 };
811
812 for mcp_tool in mcp_tools {
813 let prefixed_name = format!("{}{}{}", provider_name, TOOL_SEP_STR, mcp_tool.name);
814
815 let tool = Tool {
816 name: prefixed_name.clone(),
817 description: mcp_tool.description.unwrap_or_default(),
818 endpoint: String::new(),
819 method: HttpMethod::Post,
820 scope: Some(format!("tool:{prefixed_name}")),
821 input_schema: mcp_tool.input_schema,
822 response: None,
823 tags: Vec::new(),
824 hint: None,
825 examples: Vec::new(),
826 };
827
828 let ti = self.manifests[mi].tools.len();
829 self.manifests[mi].tools.push(tool);
830 self.tool_index.insert(prefixed_name, (mi, ti));
831 }
832 }
833}
834
835impl Provider {
836 pub fn is_mcp(&self) -> bool {
838 self.handler == "mcp"
839 }
840
841 pub fn is_openapi(&self) -> bool {
843 self.handler == "openapi"
844 }
845
846 pub fn is_cli(&self) -> bool {
848 self.handler == "cli"
849 }
850
851 pub fn mcp_transport_type(&self) -> &str {
853 self.mcp_transport.as_deref().unwrap_or("stdio")
854 }
855
856 pub fn is_file_manager(&self) -> bool {
858 self.handler == "file_manager"
859 }
860}
861
862pub(crate) fn register_file_manager_provider(registry: &mut ManifestRegistry) {
873 let download_tool = build_file_manager_download_tool();
874 let upload_tool = build_file_manager_upload_tool();
875
876 if let Some(mi) = registry
877 .manifests
878 .iter()
879 .position(|m| m.provider.handler == "file_manager")
880 {
881 if registry.manifests[mi].tools.is_empty() {
883 let tools = vec![download_tool, upload_tool];
884 for (ti, tool) in tools.iter().enumerate() {
885 registry.tool_index.insert(tool.name.clone(), (mi, ti));
886 }
887 registry.manifests[mi].tools = tools;
888 }
889 return;
890 }
891
892 let provider = Provider {
893 name: "file_manager".to_string(),
894 description: "Generic binary download/upload for agents".to_string(),
895 base_url: String::new(),
896 auth_type: AuthType::None,
897 auth_key_name: None,
898 auth_header_name: None,
899 auth_query_name: None,
900 auth_value_prefix: None,
901 extra_headers: HashMap::new(),
902 oauth2_token_url: None,
903 auth_secret_name: None,
904 oauth2_basic_auth: false,
905 internal: false,
906 handler: "file_manager".to_string(),
907 mcp_transport: None,
908 mcp_command: None,
909 mcp_args: Vec::new(),
910 mcp_url: None,
911 mcp_env: HashMap::new(),
912 cli_command: None,
913 cli_default_args: Vec::new(),
914 cli_env: HashMap::new(),
915 cli_timeout_secs: None,
916 cli_output_args: Vec::new(),
917 cli_output_positional: HashMap::new(),
918 upload_destinations: HashMap::new(),
919 upload_default_destination: None,
920 openapi_spec: None,
921 openapi_include_tags: Vec::new(),
922 openapi_exclude_tags: Vec::new(),
923 openapi_include_operations: Vec::new(),
924 openapi_exclude_operations: Vec::new(),
925 openapi_max_operations: None,
926 openapi_overrides: HashMap::new(),
927 auth_generator: None,
928 category: Some("file_manager".to_string()),
929 skills: Vec::new(),
930 };
931
932 let tools = vec![download_tool, upload_tool];
933 let mi = registry.manifests.len();
934 for (ti, tool) in tools.iter().enumerate() {
935 registry.tool_index.insert(tool.name.clone(), (mi, ti));
936 }
937 registry.manifests.push(Manifest { provider, tools });
938}
939
940fn build_file_manager_download_tool() -> Tool {
941 let schema = serde_json::json!({
942 "type": "object",
943 "required": ["url"],
944 "properties": {
945 "url": {"type": "string", "description": "URL to fetch bytes from"},
946 "out": {"type": "string", "description": "Local path to write bytes; if omitted, returns base64 inline"},
947 "inline": {"type": "boolean", "description": "Return bytes as base64 in the response instead of writing to disk"},
948 "max_bytes": {"type": "integer", "description": "Abort if body exceeds this many bytes (default 500 MB)"},
949 "timeout": {"type": "integer", "description": "Request timeout in seconds (default 120)"},
950 "headers": {"type": "object", "description": "Extra request headers, e.g. {\"Authorization\": \"Bearer abc\"}"},
951 "follow_redirects": {"type": "boolean", "description": "Follow 3xx redirects (default true)"}
952 }
953 });
954
955 Tool {
956 name: "file_manager:download".to_string(),
957 description: "Download bytes from a URL. Writes to --out <path> or returns base64 inline."
958 .to_string(),
959 endpoint: String::new(),
960 method: HttpMethod::Post,
961 scope: Some("tool:file_manager:download".to_string()),
962 input_schema: Some(schema),
963 response: None,
964 tags: vec![
965 "file".to_string(),
966 "download".to_string(),
967 "binary".to_string(),
968 ],
969 hint: Some(
970 "Use for 'I have a URL, give me the bytes' — images, video, audio, PDFs, CSVs, ZIPs."
971 .to_string(),
972 ),
973 examples: vec![
974 "ati run file_manager:download --url https://example.com/file.mp4 --out /tmp/clip.mp4"
975 .to_string(),
976 "ati run file_manager:download --url https://example.com/data.csv --inline true"
977 .to_string(),
978 ],
979 }
980}
981
982fn build_file_manager_upload_tool() -> Tool {
983 let schema = serde_json::json!({
984 "type": "object",
985 "required": ["path"],
986 "properties": {
987 "path": {"type": "string", "description": "Local file path to upload"},
988 "content_type": {"type": "string", "description": "Override MIME type (default: inferred from extension)"},
989 "object_name": {"type": "string", "description": "Object key (when destination is GCS-style); default: auto-generated"},
990 "destination": {"type": "string", "description": "Allowlist key declared in the operator's file_manager.toml manifest (e.g. \"fal\", \"gcs\"). Omit to use the operator default."}
991 }
992 });
993
994 Tool {
995 name: "file_manager:upload".to_string(),
996 description: "Upload a local file to a manifest-declared destination, return a public URL.".to_string(),
997 endpoint: String::new(),
998 method: HttpMethod::Post,
999 scope: Some("tool:file_manager:upload".to_string()),
1000 input_schema: Some(schema),
1001 response: None,
1002 tags: vec!["file".to_string(), "upload".to_string(), "binary".to_string()],
1003 hint: Some("Upload a local file to a manifest-declared destination (GCS, fal_storage, etc.) and get a public URL.".to_string()),
1004 examples: vec![
1005 "ati run file_manager:upload --path /tmp/narration.mp3".to_string(),
1006 "ati run file_manager:upload --path /tmp/report.pdf --destination gcs".to_string(),
1007 ],
1008 }
1009}