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, the roles permitted to
5//! drive it, and whether senders are linked to existing accounts by their
6//! workspace email. Secrets are never inlined — only references resolved
7//! through the profile's secret source at boot. This type lives in `models`
8//! (not the `slack` domain crate) so it can be embedded in
9//! [`super::ServicesConfig`] without a dependency cycle, mirroring
10//! `AgentConfig` 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, SlackWorkspaceId};
20
21use crate::errors::ConfigValidationError;
22
23#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
24#[serde(deny_unknown_fields)]
25pub struct SlackAppConfig {
26    pub workspace_id: SlackWorkspaceId,
27    pub signing_secret_ref: SecretName,
28    pub bot_token_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: SlackAuthzConfig,
37}
38
39#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
40#[serde(deny_unknown_fields)]
41pub struct SlackAuthzConfig {
42    #[serde(default)]
43    pub allowed_roles: Vec<String>,
44    // Why: attaches the sender to the account holding the same email as their
45    // Slack workspace profile instead of minting a role-less user on first
46    // contact. Requires the `users:read.email` bot scope; an app without it
47    // must leave this off and link identities explicitly.
48    #[serde(default)]
49    pub link_by_workspace_email: bool,
50}
51
52const fn default_enabled() -> bool {
53    true
54}
55
56impl SlackAppConfig {
57    #[must_use]
58    pub fn agent_for(&self, key: &str) -> Option<&AgentName> {
59        self.routing.get(key).or(self.default_agent.as_ref())
60    }
61
62    pub fn validate(&self, name: &str) -> Result<(), ConfigValidationError> {
63        if self.workspace_id.as_str().is_empty() {
64            return Err(ConfigValidationError::invalid_field(format!(
65                "slack app '{name}' has an empty workspace_id"
66            )));
67        }
68        if self.default_agent.is_none() && self.routing.is_empty() {
69            return Err(ConfigValidationError::required(format!(
70                "slack app '{name}' must set default_agent or at least one routing entry"
71            )));
72        }
73        Ok(())
74    }
75}