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
11use std::collections::BTreeMap;
12
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use systemprompt_identifiers::{AgentName, SecretName, SlackWorkspaceId};
16
17use crate::errors::ConfigValidationError;
18
19/// A single configured Slack app, keyed by a human name in the manifest.
20#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
21#[serde(deny_unknown_fields)]
22pub struct SlackAppConfig {
23    /// The Slack workspace (team) id this app serves.
24    pub workspace_id: SlackWorkspaceId,
25    /// Reference into the profile secret store for the signing secret.
26    pub signing_secret_ref: SecretName,
27    /// Reference into the profile secret store for the bot OAuth token.
28    pub bot_token_ref: SecretName,
29    #[serde(default = "default_enabled")]
30    pub enabled: bool,
31    /// Agent used when no `routing` entry matches.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub default_agent: Option<AgentName>,
34    /// Per-channel or per-command agent overrides (key: channel id or `/cmd`).
35    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
36    pub routing: BTreeMap<String, AgentName>,
37    #[serde(default)]
38    pub authz: SlackAuthzConfig,
39}
40
41/// Authorization seed for an app — the roles granted access to its surfaces.
42#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
43#[serde(deny_unknown_fields)]
44pub struct SlackAuthzConfig {
45    #[serde(default)]
46    pub allowed_roles: Vec<String>,
47}
48
49const fn default_enabled() -> bool {
50    true
51}
52
53impl SlackAppConfig {
54    /// Resolve the agent for a routing key (channel id or `/command`), falling
55    /// back to `default_agent`.
56    #[must_use]
57    pub fn agent_for(&self, key: &str) -> Option<&AgentName> {
58        self.routing.get(key).or(self.default_agent.as_ref())
59    }
60
61    pub fn validate(&self, name: &str) -> Result<(), ConfigValidationError> {
62        if self.workspace_id.as_str().is_empty() {
63            return Err(ConfigValidationError::invalid_field(format!(
64                "slack app '{name}' has an empty workspace_id"
65            )));
66        }
67        if self.default_agent.is_none() && self.routing.is_empty() {
68            return Err(ConfigValidationError::required(format!(
69                "slack app '{name}' must set default_agent or at least one routing entry"
70            )));
71        }
72        Ok(())
73    }
74}