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)]
138#[allow(clippy::struct_excessive_bools)]
139pub struct AgentConfig {
140 pub name: String,
141 pub port: u16,
142 pub endpoint: String,
143 pub enabled: bool,
144 #[serde(default)]
145 pub dev_only: bool,
146 #[serde(default)]
147 pub is_primary: bool,
148 #[serde(default)]
149 pub default: bool,
150 pub card: AgentCardConfig,
151 pub metadata: AgentMetadataConfig,
152 #[serde(default)]
153 pub oauth: OAuthConfig,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct AgentCardConfig {
159 pub protocol_version: String,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub name: Option<String>,
162 pub display_name: String,
163 pub description: String,
164 pub version: String,
165 #[serde(default = "default_transport")]
166 pub preferred_transport: String,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub icon_url: Option<String>,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub documentation_url: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub provider: Option<AgentProviderInfo>,
173 #[serde(default)]
174 pub capabilities: CapabilitiesConfig,
175 #[serde(default = "default_input_modes")]
176 pub default_input_modes: Vec<String>,
177 #[serde(default = "default_output_modes")]
178 pub default_output_modes: Vec<String>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub security_schemes: Option<serde_json::Value>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub security: Option<Vec<serde_json::Value>>,
183 #[serde(default)]
184 pub skills: Vec<AgentSkillConfig>,
185 #[serde(default)]
186 pub supports_authenticated_extended_card: bool,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct AgentSkillConfig {
191 pub id: String,
192 pub name: String,
193 pub description: String,
194 #[serde(default)]
195 pub tags: Vec<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub examples: Option<Vec<String>>,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub input_modes: Option<Vec<String>>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub output_modes: Option<Vec<String>>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub security: Option<Vec<serde_json::Value>>,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct AgentProviderInfo {
208 pub organization: String,
209 pub url: String,
210}
211
212#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
213#[serde(rename_all = "camelCase")]
214pub struct CapabilitiesConfig {
215 #[serde(default = "default_true")]
216 pub streaming: bool,
217 #[serde(default)]
218 pub push_notifications: bool,
219 #[serde(default = "default_true")]
220 pub state_transition_history: bool,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225#[derive(Default)]
226pub struct AgentMetadataConfig {
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub system_prompt: Option<String>,
229 #[serde(default)]
230 pub mcp_servers: Vec<String>,
231 #[serde(default)]
232 pub skills: Vec<String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub provider: Option<String>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub model: Option<String>,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub max_output_tokens: Option<u32>,
239 #[serde(default)]
240 pub tool_model_overrides: ToolModelOverrides,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct OAuthConfig {
245 #[serde(default)]
246 pub required: bool,
247 #[serde(default)]
248 pub scopes: Vec<Permission>,
249 #[serde(default = "default_audience")]
250 pub audience: JwtAudience,
251}
252
253impl AgentConfig {
254 pub fn validate(&self, name: &str) -> anyhow::Result<()> {
255 if self.name != name {
256 anyhow::bail!(
257 "Agent config key '{}' does not match name field '{}'",
258 name,
259 self.name
260 );
261 }
262
263 if !self
264 .name
265 .chars()
266 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
267 {
268 anyhow::bail!(
269 "Agent name '{}' must be lowercase alphanumeric with underscores only",
270 self.name
271 );
272 }
273
274 if self.name.len() < 3 || self.name.len() > 50 {
275 anyhow::bail!(
276 "Agent name '{}' must be between 3 and 50 characters",
277 self.name
278 );
279 }
280
281 if self.port == 0 {
282 anyhow::bail!("Agent '{}' has invalid port {}", self.name, self.port);
283 }
284
285 Ok(())
286 }
287
288 pub fn extract_oauth_scopes_from_card(&mut self) {
289 if let Some(security_vec) = &self.card.security {
290 for security_obj in security_vec {
291 if let Some(oauth2_scopes) = security_obj.get("oauth2").and_then(|v| v.as_array()) {
292 let mut permissions = Vec::new();
293 for scope_val in oauth2_scopes {
294 if let Some(scope_str) = scope_val.as_str() {
295 match scope_str {
296 "admin" => permissions.push(Permission::Admin),
297 "user" => permissions.push(Permission::User),
298 "service" => permissions.push(Permission::Service),
299 "a2a" => permissions.push(Permission::A2a),
300 "mcp" => permissions.push(Permission::Mcp),
301 "anonymous" => permissions.push(Permission::Anonymous),
302 _ => {},
303 }
304 }
305 }
306 if !permissions.is_empty() {
307 self.oauth.scopes = permissions;
308 self.oauth.required = true;
309 }
310 }
311 }
312 }
313 }
314
315 pub fn construct_url(&self, base_url: &str) -> String {
316 format!(
317 "{}/api/v1/agents/{}",
318 base_url.trim_end_matches('/'),
319 self.name
320 )
321 }
322}
323
324impl Default for CapabilitiesConfig {
325 fn default() -> Self {
326 Self {
327 streaming: true,
328 push_notifications: false,
329 state_transition_history: true,
330 }
331 }
332}
333
334impl Default for OAuthConfig {
335 fn default() -> Self {
336 Self {
337 required: false,
338 scopes: Vec::new(),
339 audience: JwtAudience::A2a,
340 }
341 }
342}
343
344fn default_transport() -> String {
345 "JSONRPC".to_string()
346}
347
348fn default_input_modes() -> Vec<String> {
349 vec!["text/plain".to_string()]
350}
351
352fn default_output_modes() -> Vec<String> {
353 vec!["text/plain".to_string()]
354}
355
356const fn default_true() -> bool {
357 true
358}
359
360const fn default_audience() -> JwtAudience {
361 JwtAudience::A2a
362}