systemprompt_models/services/
agent_config.rs1use super::super::ai::ToolModelOverrides;
2use super::super::auth::{JwtAudience, Permission};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[allow(clippy::struct_excessive_bools)]
7pub struct AgentConfig {
8 pub name: String,
9 pub port: u16,
10 pub endpoint: String,
11 pub enabled: bool,
12 #[serde(default)]
13 pub dev_only: bool,
14 #[serde(default)]
15 pub is_primary: bool,
16 #[serde(default)]
17 pub default: bool,
18 pub card: AgentCardConfig,
19 pub metadata: AgentMetadataConfig,
20 #[serde(default)]
21 pub oauth: OAuthConfig,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct AgentCardConfig {
27 pub protocol_version: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub name: Option<String>,
30 pub display_name: String,
31 pub description: String,
32 pub version: String,
33 #[serde(default = "default_transport")]
34 pub preferred_transport: String,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub icon_url: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub documentation_url: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub provider: Option<AgentProviderInfo>,
41 #[serde(default)]
42 pub capabilities: CapabilitiesConfig,
43 #[serde(default = "default_input_modes")]
44 pub default_input_modes: Vec<String>,
45 #[serde(default = "default_output_modes")]
46 pub default_output_modes: Vec<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub security_schemes: Option<serde_json::Value>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub security: Option<Vec<serde_json::Value>>,
51 #[serde(default)]
52 pub skills: Vec<AgentSkillConfig>,
53 #[serde(default)]
54 pub supports_authenticated_extended_card: bool,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct AgentSkillConfig {
60 pub id: String,
61 pub name: String,
62 pub description: String,
63 #[serde(default)]
64 pub tags: Vec<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub examples: Option<Vec<String>>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub input_modes: Option<Vec<String>>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub output_modes: Option<Vec<String>>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub security: Option<Vec<serde_json::Value>>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct AgentProviderInfo {
83 pub organization: String,
84 pub url: String,
85}
86
87#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct CapabilitiesConfig {
90 #[serde(default = "default_true")]
91 pub streaming: bool,
92 #[serde(default)]
93 pub push_notifications: bool,
94 #[serde(default = "default_true")]
95 pub state_transition_history: bool,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100#[derive(Default)]
101pub struct AgentMetadataConfig {
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub system_prompt: Option<String>,
104 #[serde(default)]
105 pub mcp_servers: Vec<String>,
106 #[serde(default)]
107 pub skills: Vec<String>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub provider: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub model: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub max_output_tokens: Option<u32>,
114 #[serde(default)]
115 pub tool_model_overrides: ToolModelOverrides,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct OAuthConfig {
124 #[serde(default)]
125 pub required: bool,
126 #[serde(default)]
127 pub scopes: Vec<Permission>,
128 #[serde(default = "default_audience")]
129 pub audience: JwtAudience,
130}
131
132impl AgentConfig {
133 pub fn validate(&self, name: &str) -> anyhow::Result<()> {
134 if self.name != name {
135 anyhow::bail!(
136 "Agent config key '{}' does not match name field '{}'",
137 name,
138 self.name
139 );
140 }
141
142 if !self
143 .name
144 .chars()
145 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
146 {
147 anyhow::bail!(
148 "Agent name '{}' must be lowercase alphanumeric with underscores only",
149 self.name
150 );
151 }
152
153 if self.name.len() < 3 || self.name.len() > 50 {
154 anyhow::bail!(
155 "Agent name '{}' must be between 3 and 50 characters",
156 self.name
157 );
158 }
159
160 if self.port == 0 {
161 anyhow::bail!("Agent '{}' has invalid port {}", self.name, self.port);
162 }
163
164 Ok(())
165 }
166
167 pub fn extract_oauth_scopes_from_card(&mut self) {
168 if let Some(security_vec) = &self.card.security {
169 for security_obj in security_vec {
170 if let Some(oauth2_scopes) = security_obj.get("oauth2").and_then(|v| v.as_array()) {
171 let mut permissions = Vec::new();
172 for scope_val in oauth2_scopes {
173 if let Some(scope_str) = scope_val.as_str() {
174 match scope_str {
175 "admin" => permissions.push(Permission::Admin),
176 "user" => permissions.push(Permission::User),
177 "service" => permissions.push(Permission::Service),
178 "a2a" => permissions.push(Permission::A2a),
179 "mcp" => permissions.push(Permission::Mcp),
180 "anonymous" => permissions.push(Permission::Anonymous),
181 _ => {},
182 }
183 }
184 }
185 if !permissions.is_empty() {
186 self.oauth.scopes = permissions;
187 self.oauth.required = true;
188 }
189 }
190 }
191 }
192 }
193
194 pub fn construct_url(&self, base_url: &str) -> String {
195 format!(
196 "{}/api/v1/agents/{}",
197 base_url.trim_end_matches('/'),
198 self.name
199 )
200 }
201}
202
203impl Default for CapabilitiesConfig {
204 fn default() -> Self {
205 Self {
206 streaming: true,
207 push_notifications: false,
208 state_transition_history: true,
209 }
210 }
211}
212
213impl Default for OAuthConfig {
214 fn default() -> Self {
215 Self {
216 required: false,
217 scopes: Vec::new(),
218 audience: JwtAudience::A2a,
219 }
220 }
221}
222
223fn default_transport() -> String {
224 "JSONRPC".to_string()
225}
226
227fn default_input_modes() -> Vec<String> {
228 vec!["text/plain".to_string()]
229}
230
231fn default_output_modes() -> Vec<String> {
232 vec!["text/plain".to_string()]
233}
234
235const fn default_true() -> bool {
236 true
237}
238
239const fn default_audience() -> JwtAudience {
240 JwtAudience::A2a
241}