Skip to main content

systemprompt_models/services/
slack.rs

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