1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9pub const API_VERSION: u32 = 2;
13
14#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Capabilities {
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
38 pub fs_read: Vec<PathPattern>,
39
40 #[serde(default, skip_serializing_if = "Vec::is_empty")]
42 pub fs_write: Vec<PathPattern>,
43
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub env_read: Vec<String>,
48
49 #[serde(default, skip_serializing_if = "Vec::is_empty")]
51 pub net: Vec<NetPattern>,
52
53 #[serde(default, skip_serializing_if = "StdioCapability::is_none")]
55 pub stdio: StdioCapability,
56}
57
58impl Capabilities {
59 pub fn none() -> Self {
61 Self::default()
62 }
63
64 pub fn is_empty(&self) -> bool {
66 self.fs_read.is_empty()
67 && self.fs_write.is_empty()
68 && self.env_read.is_empty()
69 && self.net.is_empty()
70 && self.stdio.is_none()
71 }
72
73 pub fn with_fs_read(mut self, paths: Vec<PathPattern>) -> Self {
75 self.fs_read = paths;
76 self
77 }
78
79 pub fn with_fs_write(mut self, paths: Vec<PathPattern>) -> Self {
81 self.fs_write = paths;
82 self
83 }
84
85 pub fn with_env_read(mut self, patterns: Vec<String>) -> Self {
87 self.env_read = patterns;
88 self
89 }
90
91 pub fn with_net(mut self, patterns: Vec<NetPattern>) -> Self {
93 self.net = patterns;
94 self
95 }
96
97 pub fn with_stdio(mut self, stdio: StdioCapability) -> Self {
99 self.stdio = stdio;
100 self
101 }
102
103 pub fn is_subset_of(&self, other: &Capabilities) -> bool {
105 for path in &self.fs_read {
107 if !other.fs_read.iter().any(|p| p.contains(path)) {
108 return false;
109 }
110 }
111
112 for path in &self.fs_write {
114 if !other.fs_write.iter().any(|p| p.contains(path)) {
115 return false;
116 }
117 }
118
119 for env in &self.env_read {
121 if !other.env_read.contains(env) {
122 return false;
123 }
124 }
125
126 for net in &self.net {
128 if !other.net.iter().any(|n| n.contains(net)) {
129 return false;
130 }
131 }
132
133 if self.stdio.stdin && !other.stdio.stdin {
135 return false;
136 }
137 if self.stdio.stdout && !other.stdio.stdout {
138 return false;
139 }
140 if self.stdio.stderr && !other.stdio.stderr {
141 return false;
142 }
143
144 true
145 }
146
147 pub fn compute_hash(&self) -> String {
152 match rmp_serde::to_vec(self) {
154 Ok(bytes) => {
155 let hash = blake3::hash(&bytes);
156 hash.to_hex()[..16].to_string()
158 }
159 Err(_) => {
160 "0000000000000000".to_string()
162 }
163 }
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169pub struct PathPattern {
170 pub pattern: String,
172
173 #[serde(default)]
175 pub recursive: bool,
176}
177
178impl PathPattern {
179 pub fn new(pattern: impl Into<String>) -> Self {
181 Self {
182 pattern: pattern.into(),
183 recursive: false,
184 }
185 }
186
187 pub fn recursive(mut self) -> Self {
189 self.recursive = true;
190 self
191 }
192
193 pub fn contains(&self, other: &PathPattern) -> bool {
195 if self.pattern == other.pattern {
196 return self.recursive || !other.recursive;
198 }
199
200 if self.recursive {
202 let self_path = PathBuf::from(&self.pattern);
203 let other_path = PathBuf::from(&other.pattern);
204
205 let self_normalized = self_path.components().collect::<Vec<_>>();
207 let other_normalized = other_path.components().collect::<Vec<_>>();
208
209 if other_normalized.len() >= self_normalized.len() {
210 return other_normalized
211 .iter()
212 .take(self_normalized.len())
213 .eq(self_normalized.iter());
214 }
215 }
216
217 false
218 }
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223pub struct NetPattern {
224 pub host: String,
226
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub port: Option<u16>,
230
231 #[serde(default)]
233 pub protocol: NetProtocol,
234}
235
236impl NetPattern {
237 pub fn https(host: impl Into<String>) -> Self {
239 Self {
240 host: host.into(),
241 port: None,
242 protocol: NetProtocol::Https,
243 }
244 }
245
246 pub fn https_port(host: impl Into<String>, port: u16) -> Self {
248 Self {
249 host: host.into(),
250 port: Some(port),
251 protocol: NetProtocol::Https,
252 }
253 }
254
255 pub fn tcp(host: impl Into<String>, port: u16) -> Self {
257 Self {
258 host: host.into(),
259 port: Some(port),
260 protocol: NetProtocol::Tcp,
261 }
262 }
263
264 pub fn contains(&self, other: &NetPattern) -> bool {
266 if self.protocol != other.protocol {
268 return false;
269 }
270
271 let host_matches = if self.host.starts_with("*.") {
273 let suffix = &self.host[1..]; other.host.ends_with(suffix) || other.host == self.host[2..]
275 } else {
276 self.host == other.host
277 };
278
279 if !host_matches {
280 return false;
281 }
282
283 match (self.port, other.port) {
285 (None, _) => true,
286 (Some(sp), Some(op)) => sp == op,
287 (Some(_), None) => false,
288 }
289 }
290}
291
292#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(rename_all = "snake_case")]
295#[repr(u8)]
296pub enum NetProtocol {
297 #[default]
298 Https = 0,
299 Http = 1,
300 Tcp = 2,
301}
302
303#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
305pub struct StdioCapability {
306 #[serde(default)]
308 pub stdin: bool,
309
310 #[serde(default)]
312 pub stdout: bool,
313
314 #[serde(default)]
316 pub stderr: bool,
317}
318
319impl StdioCapability {
320 pub fn none() -> Self {
322 Self::default()
323 }
324
325 pub fn is_none(&self) -> bool {
327 !self.stdin && !self.stdout && !self.stderr
328 }
329
330 pub fn all() -> Self {
332 Self {
333 stdin: true,
334 stdout: true,
335 stderr: true,
336 }
337 }
338
339 pub fn stdout_stderr() -> Self {
341 Self {
342 stdin: false,
343 stdout: true,
344 stderr: true,
345 }
346 }
347
348 pub fn stdout_only() -> Self {
350 Self {
351 stdin: false,
352 stdout: true,
353 stderr: false,
354 }
355 }
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct CommandSpec {
365 pub name: String,
367
368 pub about: String,
370
371 #[serde(default)]
373 pub version: Option<String>,
374
375 #[serde(default)]
377 pub author: Option<String>,
378
379 #[serde(default)]
381 pub args: Vec<ArgSpec>,
382
383 #[serde(default)]
385 pub subcommands: Vec<CommandSpec>,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct ArgSpec {
391 pub name: String,
393
394 #[serde(default)]
396 pub long: Option<String>,
397
398 #[serde(default)]
400 pub short: Option<char>,
401
402 #[serde(default)]
404 pub required: bool,
405
406 #[serde(default)]
408 pub help: String,
409
410 #[serde(default)]
412 pub value_name: Option<String>,
413
414 #[serde(default)]
416 pub default_value: Option<String>,
417
418 #[serde(default)]
420 pub possible_values: Option<Vec<String>>,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
425pub enum ExecuteResult {
426 Success(String),
428
429 Error(ExecuteError),
431
432 Effect(Effect),
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct ExecuteError {
442 pub code: u8,
444
445 pub message: String,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
467pub enum Effect {
468 HttpGet {
470 id: u32,
472 url: String,
474 #[serde(default, skip_serializing_if = "Vec::is_empty")]
476 headers: Vec<(String, String)>,
477 },
478
479 HttpPost {
481 id: u32,
483 url: String,
485 body: String,
487 #[serde(default, skip_serializing_if = "Option::is_none")]
489 content_type: Option<String>,
490 #[serde(default, skip_serializing_if = "Vec::is_empty")]
492 headers: Vec<(String, String)>,
493 },
494
495 Sleep {
497 id: u32,
499 duration_ms: u64,
501 },
502}
503
504impl Effect {
505 pub fn id(&self) -> u32 {
507 match self {
508 Effect::HttpGet { id, .. } => *id,
509 Effect::HttpPost { id, .. } => *id,
510 Effect::Sleep { id, .. } => *id,
511 }
512 }
513
514 pub fn http_get(id: u32, url: impl Into<String>) -> Self {
516 Effect::HttpGet {
517 id,
518 url: url.into(),
519 headers: vec![],
520 }
521 }
522
523 pub fn http_post(id: u32, url: impl Into<String>, body: impl Into<String>) -> Self {
525 Effect::HttpPost {
526 id,
527 url: url.into(),
528 body: body.into(),
529 content_type: None,
530 headers: vec![],
531 }
532 }
533
534 pub fn sleep(id: u32, duration_ms: u64) -> Self {
536 Effect::Sleep { id, duration_ms }
537 }
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize)]
542pub enum EffectResult {
543 Http(HttpResponse),
545
546 SleepComplete,
548
549 Error(String),
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct HttpResponse {
556 pub status: u16,
558
559 pub body: String,
561
562 #[serde(default, skip_serializing_if = "Vec::is_empty")]
564 pub headers: Vec<(String, String)>,
565}
566
567impl HttpResponse {
568 pub fn is_success(&self) -> bool {
570 (200..300).contains(&self.status)
571 }
572}
573
574impl ExecuteResult {
575 pub fn http_get(id: u32, url: impl Into<String>) -> Self {
577 ExecuteResult::Effect(Effect::http_get(id, url))
578 }
579
580 pub fn http_post(id: u32, url: impl Into<String>, body: impl Into<String>) -> Self {
582 ExecuteResult::Effect(Effect::http_post(id, url, body))
583 }
584
585 pub fn sleep(id: u32, duration_ms: u64) -> Self {
587 ExecuteResult::Effect(Effect::sleep(id, duration_ms))
588 }
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct PluginManifest {
594 pub api_version: u32,
596
597 pub command: CommandSpec,
599
600 #[serde(default, skip_serializing_if = "Capabilities::is_empty")]
602 pub capabilities: Capabilities,
603}
604
605impl PluginManifest {
606 pub fn new(command: CommandSpec) -> Self {
608 Self {
609 api_version: API_VERSION,
610 command,
611 capabilities: Capabilities::default(),
612 }
613 }
614
615 pub fn with_capabilities(command: CommandSpec, capabilities: Capabilities) -> Self {
617 Self {
618 api_version: API_VERSION,
619 command,
620 capabilities,
621 }
622 }
623
624 pub fn capabilities(mut self, caps: Capabilities) -> Self {
626 self.capabilities = caps;
627 self
628 }
629}
630
631impl ExecuteResult {
632 pub fn success(output: impl Into<String>) -> Self {
634 Self::Success(output.into())
635 }
636
637 pub fn user_error(message: impl Into<String>) -> Self {
639 Self::Error(ExecuteError {
640 code: 1,
641 message: message.into(),
642 })
643 }
644
645 pub fn system_error(message: impl Into<String>) -> Self {
647 Self::Error(ExecuteError {
648 code: 101,
649 message: message.into(),
650 })
651 }
652}
653
654impl CommandSpec {
655 pub fn new(name: impl Into<String>, about: impl Into<String>) -> Self {
657 Self {
658 name: name.into(),
659 about: about.into(),
660 version: None,
661 author: None,
662 args: Vec::new(),
663 subcommands: Vec::new(),
664 }
665 }
666
667 pub fn version(mut self, version: impl Into<String>) -> Self {
669 self.version = Some(version.into());
670 self
671 }
672
673 pub fn arg(mut self, arg: ArgSpec) -> Self {
675 self.args.push(arg);
676 self
677 }
678
679 pub fn subcommand(mut self, cmd: CommandSpec) -> Self {
681 self.subcommands.push(cmd);
682 self
683 }
684}
685
686impl ArgSpec {
687 pub fn positional(name: impl Into<String>) -> Self {
689 Self {
690 name: name.into(),
691 long: None,
692 short: None,
693 required: false,
694 help: String::new(),
695 value_name: None,
696 default_value: None,
697 possible_values: None,
698 }
699 }
700
701 pub fn option(name: impl Into<String>, long: impl Into<String>) -> Self {
703 Self {
704 name: name.into(),
705 long: Some(long.into()),
706 short: None,
707 required: false,
708 help: String::new(),
709 value_name: None,
710 default_value: None,
711 possible_values: None,
712 }
713 }
714
715 pub fn required(mut self) -> Self {
717 self.required = true;
718 self
719 }
720
721 pub fn help(mut self, help: impl Into<String>) -> Self {
723 self.help = help.into();
724 self
725 }
726
727 pub fn short(mut self, short: char) -> Self {
729 self.short = Some(short);
730 self
731 }
732
733 pub fn default(mut self, value: impl Into<String>) -> Self {
735 self.default_value = Some(value.into());
736 self
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743
744 #[test]
745 fn test_command_spec_serialization() {
746 let spec = CommandSpec::new("hello", "Says hello")
747 .version("1.0.0")
748 .arg(
749 ArgSpec::positional("name")
750 .help("Name to greet")
751 .default("World"),
752 );
753
754 let bytes = rmp_serde::to_vec(&spec).unwrap();
755 let decoded: CommandSpec = rmp_serde::from_slice(&bytes).unwrap();
756
757 assert_eq!(decoded.name, "hello");
758 assert_eq!(decoded.about, "Says hello");
759 assert_eq!(decoded.args.len(), 1);
760 }
761
762 #[test]
763 fn test_execute_result_serialization() {
764 let result = ExecuteResult::success("Hello, World!");
765 let bytes = rmp_serde::to_vec(&result).unwrap();
766 let decoded: ExecuteResult = rmp_serde::from_slice(&bytes).unwrap();
767
768 match decoded {
769 ExecuteResult::Success(s) => assert_eq!(s, "Hello, World!"),
770 _ => panic!("Expected success"),
771 }
772 }
773
774 #[test]
779 fn test_capabilities_empty() {
780 let caps = Capabilities::none();
781 assert!(caps.is_empty());
782
783 let caps_with_fs = Capabilities::default().with_fs_read(vec![PathPattern::new("./data")]);
784 assert!(!caps_with_fs.is_empty());
785 }
786
787 #[test]
788 fn test_capabilities_serialization() {
789 let caps = Capabilities::default()
790 .with_fs_read(vec![PathPattern::new("./data").recursive()])
791 .with_fs_write(vec![PathPattern::new("./output")])
792 .with_env_read(vec!["HOME".into(), "PATH".into()])
793 .with_net(vec![NetPattern::https("api.example.com")])
794 .with_stdio(StdioCapability::stdout_stderr());
795
796 let bytes = rmp_serde::to_vec_named(&caps).unwrap();
798 let decoded: Capabilities = rmp_serde::from_slice(&bytes).unwrap();
799
800 assert_eq!(decoded.fs_read.len(), 1);
801 assert!(decoded.fs_read[0].recursive);
802 assert_eq!(decoded.fs_write.len(), 1);
803 assert_eq!(decoded.env_read.len(), 2);
804 assert_eq!(decoded.net.len(), 1);
805 assert!(decoded.stdio.stdout);
806 assert!(!decoded.stdio.stdin);
807 }
808
809 #[test]
810 fn test_capabilities_subset() {
811 let requested = Capabilities::default()
812 .with_fs_read(vec![PathPattern::new("./data")])
813 .with_stdio(StdioCapability::stdout_only());
814
815 let granted = Capabilities::default()
816 .with_fs_read(vec![PathPattern::new("./data").recursive()])
817 .with_fs_write(vec![PathPattern::new("./output")])
818 .with_stdio(StdioCapability::stdout_stderr());
819
820 assert!(requested.is_subset_of(&granted));
821
822 let over_requested =
824 Capabilities::default().with_fs_read(vec![PathPattern::new("./secret")]);
825
826 assert!(!over_requested.is_subset_of(&granted));
827 }
828
829 #[test]
830 fn test_path_pattern_contains() {
831 let parent = PathPattern::new("./data").recursive();
832 let child = PathPattern::new("./data/subdir");
833
834 assert!(parent.contains(&child));
835 assert!(!child.contains(&parent));
836
837 let same = PathPattern::new("./data");
838 assert!(parent.contains(&same));
839 assert!(!same.contains(&parent)); }
841
842 #[test]
843 fn test_net_pattern_contains() {
844 let wildcard = NetPattern::https("*.github.com");
845 let specific = NetPattern::https("api.github.com");
846
847 assert!(wildcard.contains(&specific));
848 assert!(!specific.contains(&wildcard));
849
850 let with_port = NetPattern::https_port("api.example.com", 443);
851 let any_port = NetPattern::https("api.example.com");
852
853 assert!(any_port.contains(&with_port));
854 assert!(!with_port.contains(&any_port));
855 }
856
857 #[test]
858 fn test_manifest_with_capabilities() {
859 let caps = Capabilities::default().with_fs_read(vec![PathPattern::new("./data")]);
860
861 let manifest =
862 PluginManifest::with_capabilities(CommandSpec::new("data-export", "Export data"), caps);
863
864 assert_eq!(manifest.api_version, API_VERSION);
865 assert_eq!(manifest.capabilities.fs_read.len(), 1);
866
867 let bytes = rmp_serde::to_vec(&manifest).unwrap();
869 let decoded: PluginManifest = rmp_serde::from_slice(&bytes).unwrap();
870
871 assert_eq!(decoded.capabilities.fs_read.len(), 1);
872 }
873
874 #[test]
875 fn test_capabilities_hash() {
876 let caps1 = Capabilities::default().with_fs_read(vec![PathPattern::new("./data")]);
877 let caps2 = Capabilities::default().with_fs_read(vec![PathPattern::new("./data")]);
878 let caps3 = Capabilities::default().with_fs_read(vec![PathPattern::new("./other")]);
879
880 assert_eq!(caps1.compute_hash(), caps2.compute_hash());
881 assert_ne!(caps1.compute_hash(), caps3.compute_hash());
882 }
883}