Skip to main content

systemprompt_security/authz/parent_chain/
sources.rs

1//! Pure membership data behind a [`super::ParentChainIndex`]: which plugins
2//! each enabled marketplace parents, which plugins select each skill, and
3//! which agents and MCP servers each marketplace names directly. Derived from
4//! a [`ServicesConfig`] by [`ChainSources::from_services`], or assembled by a
5//! caller that already holds the resolved catalogue.
6//!
7//! Membership is many-to-many: a plugin listed by two enabled marketplaces
8//! belongs to both, and the resolver is handed one chain per owner so any
9//! admitting marketplace admits the entity.
10//!
11//! Copyright (c) systemprompt.io — Business Source License 1.1.
12//! See <https://systemprompt.io> for licensing details.
13
14use std::collections::{BTreeMap, BTreeSet};
15
16use systemprompt_identifiers::{MarketplaceId, PluginId, SkillId};
17use systemprompt_models::services::{MarketplaceConfig, MarketplaceMemberKind, ServicesConfig};
18
19use crate::authz::types::EntityKind;
20
21static EMPTY_MARKETPLACES: BTreeSet<MarketplaceId> = BTreeSet::new();
22
23#[derive(Debug, Clone)]
24pub struct MarketplaceSource {
25    pub id: MarketplaceId,
26    pub fallback_default_included: Option<bool>,
27}
28
29#[derive(Debug, Clone, Default)]
30pub struct ChainSources {
31    pub marketplaces: BTreeMap<MarketplaceId, MarketplaceSource>,
32    pub plugins: BTreeMap<PluginId, BTreeSet<MarketplaceId>>,
33    pub skill_owners: BTreeMap<SkillId, BTreeSet<PluginId>>,
34    pub marketplace_members: BTreeMap<EntityKind, BTreeMap<String, BTreeSet<MarketplaceId>>>,
35}
36
37impl ChainSources {
38    #[must_use]
39    pub fn from_services(services: &ServicesConfig) -> Self {
40        let mut out = Self::default();
41        for marketplace in services.enabled_marketplaces() {
42            out.absorb(services, marketplace);
43        }
44        out
45    }
46
47    fn absorb(&mut self, services: &ServicesConfig, marketplace: &MarketplaceConfig) {
48        let id = marketplace.id.clone();
49        self.marketplaces.insert(
50            id.clone(),
51            MarketplaceSource {
52                id: id.clone(),
53                fallback_default_included: Some(marketplace.access.default_included),
54            },
55        );
56
57        for plugin in services.marketplace_plugin_configs(marketplace) {
58            self.plugins
59                .entry(plugin.id.clone())
60                .or_default()
61                .insert(id.clone());
62            for skill in services.plugin_selected_skill_ids(plugin) {
63                self.skill_owners
64                    .entry(SkillId::new(skill))
65                    .or_default()
66                    .insert(plugin.id.clone());
67            }
68        }
69
70        for (kind, member_kind, catalogue) in [
71            (
72                EntityKind::Agent,
73                MarketplaceMemberKind::Agents,
74                services.agents.keys().cloned().collect::<Vec<String>>(),
75            ),
76            (
77                EntityKind::McpServer,
78                MarketplaceMemberKind::McpServers,
79                services
80                    .mcp_servers
81                    .keys()
82                    .cloned()
83                    .collect::<Vec<String>>(),
84            ),
85        ] {
86            // Why: an empty `include:` means "every member of that catalogue",
87            // the same rule the manifest scoper applies — validation rejects an
88            // explicit ref with an empty include, so empty here is never
89            // "nothing".
90            let include = &marketplace.members(member_kind).include;
91            let members: Vec<String> = if include.is_empty() {
92                catalogue
93            } else {
94                include.clone()
95            };
96            let band = self.marketplace_members.entry(kind).or_default();
97            for member in members {
98                band.entry(member).or_default().insert(id.clone());
99            }
100        }
101    }
102
103    #[must_use]
104    pub fn plugin_ids_to_load(&self) -> Vec<String> {
105        let mut ids: BTreeSet<&str> = self.plugins.keys().map(PluginId::as_str).collect();
106        for owners in self.skill_owners.values() {
107            ids.extend(owners.iter().map(PluginId::as_str));
108        }
109        ids.into_iter().map(str::to_owned).collect()
110    }
111
112    #[must_use]
113    pub fn marketplace_ids_to_load(&self) -> Vec<MarketplaceId> {
114        self.marketplaces.keys().cloned().collect()
115    }
116
117    #[must_use]
118    pub fn marketplaces_of(&self, kind: EntityKind, id: &str) -> &BTreeSet<MarketplaceId> {
119        self.marketplace_members
120            .get(&kind)
121            .and_then(|band| band.get(id))
122            .unwrap_or(&EMPTY_MARKETPLACES)
123    }
124
125    #[must_use]
126    pub fn plugin_marketplaces(&self, id: &PluginId) -> &BTreeSet<MarketplaceId> {
127        self.plugins.get(id).unwrap_or(&EMPTY_MARKETPLACES)
128    }
129
130    #[must_use]
131    pub fn is_marketplace_member(&self, kind: EntityKind, id: &str) -> bool {
132        !self.marketplaces_of(kind, id).is_empty()
133    }
134}