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