Skip to main content

systemprompt_models/
plugin.rs

1//! Plugin component references carried in the signed bridge manifest.
2//!
3//! A plugin's skill/agent/MCP/content references are [`PluginComponentRef`]s
4//! resolved against the instance ([`ComponentSource`]); [`PluginHooksRef`]
5//! selects the hooks a plugin materialises and [`PluginDependency`] names a
6//! plugin it requires. The services manifest authors them; the gateway and the
7//! bridge exchange them at runtime.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use std::fmt;
13
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
18#[serde(rename_all = "lowercase")]
19pub enum ComponentSource {
20    Instance,
21    #[default]
22    Explicit,
23}
24
25impl fmt::Display for ComponentSource {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            Self::Instance => write!(f, "instance"),
29            Self::Explicit => write!(f, "explicit"),
30        }
31    }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
35#[serde(rename_all = "lowercase")]
36pub enum ComponentFilter {
37    Enabled,
38}
39
40impl fmt::Display for ComponentFilter {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            Self::Enabled => write!(f, "enabled"),
44        }
45    }
46}
47
48/// One plugin this plugin requires, in Claude Code's `plugin.json`
49/// `dependencies` vocabulary.
50///
51/// A bare `name` resolves inside the marketplace that carries the dependant;
52/// `marketplace` points at another marketplace, which the carrying
53/// `MarketplaceConfig` must list in
54/// `allow_cross_marketplace_dependencies_on` and, when it is not one of this
55/// instance's own marketplaces, declare under `external_marketplaces`.
56/// `version` is a semver range Claude Code checks at install time.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
58#[serde(deny_unknown_fields)]
59pub struct PluginDependency {
60    pub name: String,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub marketplace: Option<String>,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub version: Option<String>,
65}
66
67/// Selects which hooks a plugin materialises into its `hooks/hooks.json`.
68///
69/// Claude Code executes plugin hooks session-globally — a `PreToolUse` hook
70/// with a `*` matcher fires for every tool call regardless of which plugin
71/// contributed the tool. One plugin therefore carries the governance hooks for
72/// the whole instance; every other plugin emits an empty hooks file.
73///
74/// `comms` is an opt-in on that same owner: when set, the bridge also installs
75/// the `UserPromptSubmit`/`Stop` hooks that drain gateway announcements into
76/// the session. Off by default — a tenant that publishes no announcements has
77/// no reason to run a command on every prompt.
78///
79/// `judge` is server-side only: it switches on the conversation judge for the
80/// sessions this plugin's track hook reports, and installs no client hook of
81/// its own. Like `comms` it rides on the governance owner, so `is_empty`
82/// ignores it.
83#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(deny_unknown_fields)]
85pub struct PluginHooksRef {
86    #[serde(default)]
87    pub governance: bool,
88    #[serde(default)]
89    pub comms: bool,
90    // Why: `judge` was `evaluation` until 5c8d3ae9e. The struct denies unknown
91    // fields, so a published kit or a customer config still using the old key
92    // is a fatal load failure, not a warning — and kits are pinned OCI
93    // artifacts we do not get to edit in lockstep with a core release. The
94    // alias keeps them loading; drop it once every pinned kit has been
95    // republished on the new name.
96    #[serde(default, alias = "evaluation")]
97    pub judge: bool,
98    #[serde(default, skip_serializing_if = "Vec::is_empty")]
99    pub include: Vec<String>,
100}
101
102impl PluginHooksRef {
103    #[must_use]
104    pub const fn is_empty(&self) -> bool {
105        !self.governance && self.include.is_empty()
106    }
107}
108
109#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
110pub struct PluginComponentRef {
111    #[serde(default)]
112    pub source: ComponentSource,
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub filter: Option<ComponentFilter>,
115    #[serde(default, skip_serializing_if = "Vec::is_empty")]
116    pub include: Vec<String>,
117    #[serde(default, skip_serializing_if = "Vec::is_empty")]
118    pub exclude: Vec<String>,
119}