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