telos_agent/integrations/plugin/
mod.rs1pub mod errors;
11pub mod manifest;
12pub mod marketplace;
13pub mod policy_loader;
14pub mod registry;
15pub mod sources;
16pub mod tool_loader;
17
18use serde::{Deserialize, Serialize};
19use std::fmt;
20
21pub use errors::{DependencyReason, PluginError};
22pub use manifest::{
23 CommandPolicyDef, ConfigOptionType, DependencyRef, LspServerEntry, LspServersConfig,
24 MarketplaceEntry, McpServerEntry, McpServersConfig, PartialPluginManifest, PluginAuthor,
25 PluginManifest, PluginSource, PoliciesConfig, SessionPolicyDef, ToolPolicyDef,
26 UserConfigOption,
27};
28pub use marketplace::{Marketplace, MarketplaceRegistry};
29pub use registry::{LoadedPlugin, PluginEntry, PluginRegistry, PluginStatus};
30pub use sources::MarketplaceSource;
31pub use tool_loader::{CommandTool, ToolPermission, ToolSpec, load_tool_spec};
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41pub struct PluginId {
42 pub name: String,
43 pub marketplace: String,
44}
45
46pub const BUILTIN_MARKETPLACE: &str = "builtin";
48
49pub struct PluginPromptSection {
51 pub name: String,
52 pub template: String,
53}
54
55#[async_trait::async_trait]
56impl crate::agent::prompt::PromptSection for PluginPromptSection {
57 fn name(&self) -> &str {
58 &self.name
59 }
60 fn stability(&self) -> crate::agent::prompt::PromptStability {
61 crate::agent::prompt::PromptStability::Static
62 }
63 async fn render(&self, _ctx: &()) -> String {
64 self.template.clone()
65 }
66}
67
68impl PluginId {
69 pub fn parse(raw: &str) -> Option<Self> {
73 let (name, marketplace) = raw.split_once('@')?;
74 if name.is_empty() || marketplace.is_empty() {
75 return None;
76 }
77 if marketplace.contains('@') {
79 return None;
80 }
81 Some(Self { name: name.to_string(), marketplace: marketplace.to_string() })
82 }
83}
84
85impl fmt::Display for PluginId {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 write!(f, "{}@{}", self.name, self.marketplace)
88 }
89}
90
91impl Serialize for PluginId {
92 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
93 self.to_string().serialize(serializer)
94 }
95}
96
97impl<'de> Deserialize<'de> for PluginId {
98 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
99 let s = String::deserialize(deserializer)?;
100 PluginId::parse(&s).ok_or_else(|| {
101 serde::de::Error::custom(format!(
102 "invalid PluginId '{s}': expected 'name@marketplace' format"
103 ))
104 })
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn parse_valid_plugin_id() {
114 let id = PluginId::parse("foo@bar").unwrap();
115 assert_eq!(id.name, "foo");
116 assert_eq!(id.marketplace, "bar");
117 }
118
119 #[test]
120 fn parse_with_dots_and_hyphens() {
121 let id = PluginId::parse("code-formatter@telos-official").unwrap();
122 assert_eq!(id.name, "code-formatter");
123 assert_eq!(id.marketplace, "telos-official");
124 }
125
126 #[test]
127 fn parse_missing_at_returns_none() {
128 assert!(PluginId::parse("foobar").is_none());
129 }
130
131 #[test]
132 fn parse_empty_name_returns_none() {
133 assert!(PluginId::parse("@bar").is_none());
134 }
135
136 #[test]
137 fn parse_empty_marketplace_returns_none() {
138 assert!(PluginId::parse("foo@").is_none());
139 }
140
141 #[test]
142 fn parse_multiple_at_returns_none() {
143 assert!(PluginId::parse("foo@bar@baz").is_none());
144 }
145
146 #[test]
147 fn display_roundtrips() {
148 let id = PluginId { name: "foo".into(), marketplace: "bar".into() };
149 assert_eq!(id.to_string(), "foo@bar");
150 }
151
152 #[test]
153 fn serde_roundtrip() {
154 let id = PluginId { name: "foo".into(), marketplace: "bar".into() };
155 let json = serde_json::to_string(&id).unwrap();
156 assert_eq!(json, r#""foo@bar""#);
157 let parsed: PluginId = serde_json::from_str(&json).unwrap();
158 assert_eq!(parsed, id);
159 }
160
161 #[test]
162 fn serde_invalid_rejected() {
163 let result: Result<PluginId, _> = serde_json::from_str(r#""no-at-sign""#);
164 assert!(result.is_err());
165 }
166}