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 #[serde(default)]
91 pub judge: bool,
92 #[serde(default, skip_serializing_if = "Vec::is_empty")]
93 pub include: Vec<String>,
94}
95
96impl PluginHooksRef {
97 #[must_use]
98 pub const fn is_empty(&self) -> bool {
99 !self.governance && self.include.is_empty()
100 }
101}
102
103#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
104pub struct PluginComponentRef {
105 #[serde(default)]
106 pub source: ComponentSource,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub filter: Option<ComponentFilter>,
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
110 pub include: Vec<String>,
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 pub exclude: Vec<String>,
113}