systemprompt_models/services/
agent_config.rs1use 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}