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