Skip to main content

systemprompt_models/services/agent_config/
mod.rs

1//! Agent configuration models — the on-disk YAML shape, the runtime
2//! shape, and the lightweight summary projection.
3
4mod card;
5mod disk;
6mod summary;
7
8pub use card::{
9    AgentCardConfig, AgentMetadataConfig, AgentProviderInfo, AgentSkillConfig, CapabilitiesConfig,
10    OAuthConfig,
11};
12pub use disk::DiskAgentConfig;
13pub use summary::AgentSummary;
14
15use crate::auth::Permission;
16use crate::errors::ConfigValidationError;
17use serde::{Deserialize, Serialize};
18
19pub const AGENT_CONFIG_FILENAME: &str = "config.yaml";
20pub const DEFAULT_AGENT_SYSTEM_PROMPT_FILE: &str = "system_prompt.md";
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct AgentConfig {
24    pub name: String,
25    pub port: u16,
26    pub endpoint: String,
27    pub enabled: bool,
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 tags: Vec<String>,
36    pub card: AgentCardConfig,
37    pub metadata: AgentMetadataConfig,
38    #[serde(default)]
39    pub oauth: OAuthConfig,
40}
41
42impl AgentConfig {
43    pub fn validate(&self, name: &str) -> Result<(), ConfigValidationError> {
44        if self.name != name {
45            return Err(ConfigValidationError::invalid_field(format!(
46                "Agent config key '{}' does not match name field '{}'",
47                name, self.name
48            )));
49        }
50
51        if !self
52            .name
53            .chars()
54            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
55        {
56            return Err(ConfigValidationError::invalid_field(format!(
57                "Agent name '{}' must be lowercase alphanumeric with underscores only",
58                self.name
59            )));
60        }
61
62        if self.name.len() < 3 || self.name.len() > 50 {
63            return Err(ConfigValidationError::invalid_field(format!(
64                "Agent name '{}' must be between 3 and 50 characters",
65                self.name
66            )));
67        }
68
69        if self.port == 0 {
70            return Err(ConfigValidationError::invalid_field(format!(
71                "Agent '{}' has invalid port {}",
72                self.name, self.port
73            )));
74        }
75
76        Ok(())
77    }
78
79    pub fn extract_oauth_scopes_from_card(&mut self) {
80        if let Some(security_vec) = &self.card.security {
81            for security_obj in security_vec {
82                if let Some(oauth2_scopes) = security_obj.get("oauth2").and_then(|v| v.as_array()) {
83                    let mut permissions = Vec::new();
84                    for scope_val in oauth2_scopes {
85                        if let Some(scope_str) = scope_val.as_str() {
86                            match scope_str {
87                                "admin" => permissions.push(Permission::Admin),
88                                "user" => permissions.push(Permission::User),
89                                "service" => permissions.push(Permission::Service),
90                                "a2a" => permissions.push(Permission::A2a),
91                                "mcp" => permissions.push(Permission::Mcp),
92                                "anonymous" => permissions.push(Permission::Anonymous),
93                                _ => {},
94                            }
95                        }
96                    }
97                    if !permissions.is_empty() {
98                        self.oauth.scopes = permissions;
99                        self.oauth.required = true;
100                    }
101                }
102            }
103        }
104    }
105
106    #[must_use]
107    pub fn construct_url(&self, base_url: &str) -> String {
108        format!(
109            "{}/api/v1/agents/{}",
110            base_url.trim_end_matches('/'),
111            self.name
112        )
113    }
114}