Skip to main content

systemprompt_models/services/
agent_config.rs

1use super::super::ai::ToolModelOverrides;
2use super::super::auth::{JwtAudience, Permission};
3use serde::{Deserialize, Serialize};
4
5pub const AGENT_CONFIG_FILENAME: &str = "config.yaml";
6pub const DEFAULT_AGENT_SYSTEM_PROMPT_FILE: &str = "system_prompt.md";
7
8fn default_version() -> String {
9    "1.0.0".to_string()
10}
11
12#[derive(Debug, Clone, Deserialize)]
13pub struct DiskAgentConfig {
14    #[serde(default)]
15    pub id: String,
16    pub name: String,
17    pub display_name: String,
18    pub description: String,
19    #[serde(default = "default_version")]
20    pub version: String,
21    #[serde(default = "default_true")]
22    pub enabled: bool,
23    pub port: u16,
24    #[serde(default)]
25    pub endpoint: Option<String>,
26    #[serde(default)]
27    pub dev_only: bool,
28    #[serde(default)]
29    pub is_primary: bool,
30    #[serde(default)]
31    pub default: bool,
32    #[serde(default)]
33    pub system_prompt_file: Option<String>,
34    #[serde(default)]
35    pub tags: Vec<String>,
36    #[serde(default)]
37    pub category: Option<String>,
38    #[serde(default)]
39    pub mcp_servers: Vec<String>,
40    #[serde(default)]
41    pub skills: Vec<String>,
42    #[serde(default)]
43    pub provider: Option<String>,
44    #[serde(default)]
45    pub model: Option<String>,
46    pub card: AgentCardConfig,
47    #[serde(default)]
48    pub oauth: OAuthConfig,
49}
50
51impl DiskAgentConfig {
52    pub fn system_prompt_file(&self) -> &str {
53        self.system_prompt_file
54            .as_deref()
55            .filter(|s| !s.is_empty())
56            .unwrap_or(DEFAULT_AGENT_SYSTEM_PROMPT_FILE)
57    }
58
59    pub fn to_agent_config(&self, base_url: &str, system_prompt: Option<String>) -> AgentConfig {
60        let endpoint = self.endpoint.clone().unwrap_or_else(|| {
61            format!(
62                "{}/api/v1/agents/{}",
63                base_url.trim_end_matches('/'),
64                self.name
65            )
66        });
67
68        let card_name = self
69            .card
70            .name
71            .clone()
72            .unwrap_or_else(|| self.display_name.clone());
73
74        AgentConfig {
75            name: self.name.clone(),
76            port: self.port,
77            endpoint,
78            enabled: self.enabled,
79            dev_only: self.dev_only,
80            is_primary: self.is_primary,
81            default: self.default,
82            card: AgentCardConfig {
83                name: Some(card_name),
84                ..self.card.clone()
85            },
86            metadata: AgentMetadataConfig {
87                system_prompt,
88                mcp_servers: self.mcp_servers.clone(),
89                skills: self.skills.clone(),
90                provider: self.provider.clone(),
91                model: self.model.clone(),
92                ..Default::default()
93            },
94            oauth: self.oauth.clone(),
95        }
96    }
97
98    pub fn validate(&self, dir_name: &str) -> anyhow::Result<()> {
99        if !self.id.is_empty() && self.id != dir_name {
100            anyhow::bail!(
101                "Agent config id '{}' does not match directory name '{}'",
102                self.id,
103                dir_name
104            );
105        }
106
107        if !self
108            .name
109            .chars()
110            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
111        {
112            anyhow::bail!(
113                "Agent name '{}' must be lowercase alphanumeric with underscores only",
114                self.name
115            );
116        }
117
118        if self.name.len() < 3 || self.name.len() > 50 {
119            anyhow::bail!(
120                "Agent name '{}' must be between 3 and 50 characters",
121                self.name
122            );
123        }
124
125        if self.port == 0 {
126            anyhow::bail!("Agent '{}' has invalid port {}", self.name, self.port);
127        }
128
129        if self.display_name.is_empty() {
130            anyhow::bail!("Agent '{}' display_name must not be empty", self.name);
131        }
132
133        Ok(())
134    }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct AgentConfig {
139    pub name: String,
140    pub port: u16,
141    pub endpoint: String,
142    pub enabled: bool,
143    #[serde(default)]
144    pub dev_only: bool,
145    #[serde(default)]
146    pub is_primary: bool,
147    #[serde(default)]
148    pub default: bool,
149    pub card: AgentCardConfig,
150    pub metadata: AgentMetadataConfig,
151    #[serde(default)]
152    pub oauth: OAuthConfig,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct AgentCardConfig {
158    pub protocol_version: String,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub name: Option<String>,
161    pub display_name: String,
162    pub description: String,
163    pub version: String,
164    #[serde(default = "default_transport")]
165    pub preferred_transport: String,
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub icon_url: Option<String>,
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub documentation_url: Option<String>,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub provider: Option<AgentProviderInfo>,
172    #[serde(default)]
173    pub capabilities: CapabilitiesConfig,
174    #[serde(default = "default_input_modes")]
175    pub default_input_modes: Vec<String>,
176    #[serde(default = "default_output_modes")]
177    pub default_output_modes: Vec<String>,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub security_schemes: Option<serde_json::Value>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub security: Option<Vec<serde_json::Value>>,
182    #[serde(default)]
183    pub skills: Vec<AgentSkillConfig>,
184    #[serde(default)]
185    pub supports_authenticated_extended_card: bool,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct AgentSkillConfig {
190    pub id: String,
191    pub name: String,
192    pub description: String,
193    #[serde(default)]
194    pub tags: Vec<String>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub examples: Option<Vec<String>>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub input_modes: Option<Vec<String>>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub output_modes: Option<Vec<String>>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub security: Option<Vec<serde_json::Value>>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct AgentProviderInfo {
207    pub organization: String,
208    pub url: String,
209}
210
211#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct CapabilitiesConfig {
214    #[serde(default = "default_true")]
215    pub streaming: bool,
216    #[serde(default)]
217    pub push_notifications: bool,
218    #[serde(default = "default_true")]
219    pub state_transition_history: bool,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase")]
224#[derive(Default)]
225pub struct AgentMetadataConfig {
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub system_prompt: Option<String>,
228    #[serde(default)]
229    pub mcp_servers: Vec<String>,
230    #[serde(default)]
231    pub skills: Vec<String>,
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub provider: Option<String>,
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub model: Option<String>,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub max_output_tokens: Option<u32>,
238    #[serde(default)]
239    pub tool_model_overrides: ToolModelOverrides,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct OAuthConfig {
244    #[serde(default)]
245    pub required: bool,
246    #[serde(default)]
247    pub scopes: Vec<Permission>,
248    #[serde(default = "default_audience")]
249    pub audience: JwtAudience,
250}
251
252impl AgentConfig {
253    pub fn validate(&self, name: &str) -> anyhow::Result<()> {
254        if self.name != name {
255            anyhow::bail!(
256                "Agent config key '{}' does not match name field '{}'",
257                name,
258                self.name
259            );
260        }
261
262        if !self
263            .name
264            .chars()
265            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
266        {
267            anyhow::bail!(
268                "Agent name '{}' must be lowercase alphanumeric with underscores only",
269                self.name
270            );
271        }
272
273        if self.name.len() < 3 || self.name.len() > 50 {
274            anyhow::bail!(
275                "Agent name '{}' must be between 3 and 50 characters",
276                self.name
277            );
278        }
279
280        if self.port == 0 {
281            anyhow::bail!("Agent '{}' has invalid port {}", self.name, self.port);
282        }
283
284        Ok(())
285    }
286
287    pub fn extract_oauth_scopes_from_card(&mut self) {
288        if let Some(security_vec) = &self.card.security {
289            for security_obj in security_vec {
290                if let Some(oauth2_scopes) = security_obj.get("oauth2").and_then(|v| v.as_array()) {
291                    let mut permissions = Vec::new();
292                    for scope_val in oauth2_scopes {
293                        if let Some(scope_str) = scope_val.as_str() {
294                            match scope_str {
295                                "admin" => permissions.push(Permission::Admin),
296                                "user" => permissions.push(Permission::User),
297                                "service" => permissions.push(Permission::Service),
298                                "a2a" => permissions.push(Permission::A2a),
299                                "mcp" => permissions.push(Permission::Mcp),
300                                "anonymous" => permissions.push(Permission::Anonymous),
301                                _ => {},
302                            }
303                        }
304                    }
305                    if !permissions.is_empty() {
306                        self.oauth.scopes = permissions;
307                        self.oauth.required = true;
308                    }
309                }
310            }
311        }
312    }
313
314    pub fn construct_url(&self, base_url: &str) -> String {
315        format!(
316            "{}/api/v1/agents/{}",
317            base_url.trim_end_matches('/'),
318            self.name
319        )
320    }
321}
322
323impl Default for CapabilitiesConfig {
324    fn default() -> Self {
325        Self {
326            streaming: true,
327            push_notifications: false,
328            state_transition_history: true,
329        }
330    }
331}
332
333impl Default for OAuthConfig {
334    fn default() -> Self {
335        Self {
336            required: false,
337            scopes: Vec::new(),
338            audience: JwtAudience::A2a,
339        }
340    }
341}
342
343fn default_transport() -> String {
344    "JSONRPC".to_string()
345}
346
347fn default_input_modes() -> Vec<String> {
348    vec!["text/plain".to_string()]
349}
350
351fn default_output_modes() -> Vec<String> {
352    vec!["text/plain".to_string()]
353}
354
355const fn default_true() -> bool {
356    true
357}
358
359const fn default_audience() -> JwtAudience {
360    JwtAudience::A2a
361}