Skip to main content

systemprompt_models/services/
teams.rs

1//! Declarative configuration for Microsoft Teams apps.
2//!
3//! Each app describes one Teams bot registration: the Entra tenant it serves,
4//! the Microsoft App Id used as the inbound-token audience, the secret
5//! reference for its app password (client secret), the agent it routes to, and
6//! the roles permitted to drive it. Secrets are never inlined — only references
7//! resolved through the profile's secret source at boot. This type lives in
8//! `models` (not the `teams` domain crate) so it can be embedded in
9//! [`super::ServicesConfig`] without a dependency cycle, mirroring
10//! `SlackAppConfig` and `McpServerSummary`.
11//!
12//! Copyright (c) systemprompt.io — Business Source License 1.1.
13//! See <https://systemprompt.io> for licensing details.
14
15use std::collections::BTreeMap;
16
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19use systemprompt_identifiers::{AgentName, SecretName, TeamsTenantId};
20
21use crate::errors::ConfigValidationError;
22
23#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
24#[serde(deny_unknown_fields)]
25pub struct TeamsAppConfig {
26    pub tenant_id: TeamsTenantId,
27    pub app_id: String,
28    pub app_password_ref: SecretName,
29    #[serde(default = "default_enabled")]
30    pub enabled: bool,
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub default_agent: Option<AgentName>,
33    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
34    pub routing: BTreeMap<String, AgentName>,
35    #[serde(default)]
36    pub authz: TeamsAuthzConfig,
37}
38
39#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
40#[serde(deny_unknown_fields)]
41pub struct TeamsAuthzConfig {
42    #[serde(default)]
43    pub allowed_roles: Vec<String>,
44}
45
46const fn default_enabled() -> bool {
47    true
48}
49
50impl TeamsAppConfig {
51    #[must_use]
52    pub fn agent_for(&self, key: &str) -> Option<&AgentName> {
53        self.routing.get(key).or(self.default_agent.as_ref())
54    }
55
56    pub fn validate(&self, name: &str) -> Result<(), ConfigValidationError> {
57        if self.tenant_id.as_str().is_empty() {
58            return Err(ConfigValidationError::invalid_field(format!(
59                "teams app '{name}' has an empty tenant_id"
60            )));
61        }
62        if self.app_id.is_empty() {
63            return Err(ConfigValidationError::invalid_field(format!(
64                "teams app '{name}' has an empty app_id"
65            )));
66        }
67        if self.default_agent.is_none() && self.routing.is_empty() {
68            return Err(ConfigValidationError::required(format!(
69                "teams app '{name}' must set default_agent or at least one routing entry"
70            )));
71        }
72        Ok(())
73    }
74}