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}