systemprompt_models/services/
agent_config.rs

1use 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/// Agent skill definition for A2A Agent Card.
58#[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/// Information about the organization providing this agent.
76///
77/// This is metadata about the provider, not configuration for calling AI
78/// providers. For AI provider configuration, see
79/// `crates/modules/ai/src/services/providers/provider_factory.
80/// rs::AiProviderConfig`.
81#[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/// OAuth configuration for A2A agent authentication requirements.
119///
120/// Defines the permissions and audience required to access this agent.
121/// Corresponds to the `security` field in the A2A `AgentCard` specification.
122#[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}