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