Skip to main content

systemprompt_security/authz/parent_chain/
sources.rs

1//! Pure membership data behind a [`super::ParentChainIndex`]: which plugins a
2//! marketplace parents, which plugins select each skill, and which agents
3//! and MCP servers the marketplace names directly. Derived from a
4//! [`ServicesConfig`] by [`ChainSources::from_services`], or assembled by a
5//! caller that already holds the resolved catalogue.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use std::collections::{BTreeMap, BTreeSet};
11
12use systemprompt_identifiers::{MarketplaceId, PluginId, SkillId};
13use systemprompt_models::services::{MarketplaceMemberKind, ServicesConfig};
14
15use crate::authz::marketplace_floor::active_marketplace;
16use crate::authz::types::EntityKind;
17
18#[derive(Debug, Clone)]
19pub struct MarketplaceSource {
20    pub id: MarketplaceId,
21    pub fallback_default_included: Option<bool>,
22}
23
24#[derive(Debug, Clone, Default)]
25pub struct ChainSources {
26    pub marketplace: Option<MarketplaceSource>,
27    pub plugins: BTreeSet<PluginId>,
28    pub skill_owners: BTreeMap<SkillId, BTreeSet<PluginId>>,
29    pub marketplace_members: BTreeMap<EntityKind, BTreeSet<String>>,
30}
31
32impl ChainSources {
33    #[must_use]
34    pub fn from_services(services: &ServicesConfig) -> Self {
35        let Some(marketplace) = active_marketplace(services) else {
36            return Self::default();
37        };
38        let plugins = services.marketplace_plugin_configs(marketplace);
39
40        let mut skill_owners: BTreeMap<SkillId, BTreeSet<PluginId>> = BTreeMap::new();
41        for plugin in &plugins {
42            for skill in services.plugin_selected_skill_ids(plugin) {
43                skill_owners
44                    .entry(SkillId::new(skill))
45                    .or_default()
46                    .insert(plugin.id.clone());
47            }
48        }
49
50        let marketplace_members = [
51            (EntityKind::Agent, MarketplaceMemberKind::Agents),
52            (EntityKind::McpServer, MarketplaceMemberKind::McpServers),
53        ]
54        .into_iter()
55        .map(|(kind, member_kind)| {
56            (
57                kind,
58                marketplace
59                    .members(member_kind)
60                    .include
61                    .iter()
62                    .cloned()
63                    .collect(),
64            )
65        })
66        .collect();
67
68        Self {
69            marketplace: Some(MarketplaceSource {
70                id: marketplace.id.clone(),
71                fallback_default_included: Some(marketplace.access.default_included),
72            }),
73            plugins: plugins.iter().map(|plugin| plugin.id.clone()).collect(),
74            skill_owners,
75            marketplace_members,
76        }
77    }
78
79    #[must_use]
80    pub fn plugin_ids_to_load(&self) -> Vec<String> {
81        let mut ids: BTreeSet<&str> = self.plugins.iter().map(PluginId::as_str).collect();
82        for owners in self.skill_owners.values() {
83            ids.extend(owners.iter().map(PluginId::as_str));
84        }
85        ids.into_iter().map(str::to_owned).collect()
86    }
87
88    #[must_use]
89    pub fn is_marketplace_member(&self, kind: EntityKind, id: &str) -> bool {
90        self.marketplace_members
91            .get(&kind)
92            .is_some_and(|members| members.contains(id))
93    }
94}