Skip to main content

systemprompt_models/bridge/
manifest.rs

1//! Signed manifest wire format.
2//!
3//! `GET /v1/bridge/manifest` returns a [`SignedManifestEnvelope`]: the
4//! JCS-canonical serialization of a [`SignedManifest`] carried verbatim as
5//! `payload`, plus a detached ed25519 signature over those exact bytes. The
6//! bridge verifies the signature against the raw `payload` string *before*
7//! deserialising it, so fields added to [`SignedManifest`] in newer gateways
8//! never invalidate the signature on older bridges — unknown fields are
9//! simply ignored at parse time. Semantic breaks that an older bridge cannot
10//! safely ignore are declared by raising `min_schema_version` above
11//! [`MANIFEST_SCHEMA_VERSION`] of the consuming bridge, which then refuses
12//! with an upgrade message instead of a signature error.
13//!
14//! Signing, signature verification, and manifest construction live in
15//! the bridge crate (`bin/bridge/src/gateway/manifest.rs`) alongside
16//! the gateway client. Those layers pull in `ed25519-dalek` and
17//! `serde_jcs` which are not appropriate dependencies for this
18//! foundation crate.
19//!
20//! Copyright (c) systemprompt.io — Business Source License 1.1.
21//! See <https://systemprompt.io> for licensing details.
22
23use std::collections::BTreeMap;
24
25use serde::{Deserialize, Serialize};
26
27pub use crate::bridge::ids::ManifestSignature;
28use crate::bridge::ids::{
29    LibraryArtifactId, ManagedMcpServerName, PluginId, Sha256Digest, SkillId, SkillName, ToolName,
30    ToolPolicy,
31};
32use crate::bridge::manifest_version::ManifestVersion;
33use crate::services::hooks::{HookCategory, HookEvent};
34use crate::services::plugin::{PluginComponentRef, PluginHooksRef};
35use systemprompt_identifiers::{AgentId, AgentName, HookId, TenantId, UserId, ValidatedUrl};
36
37pub const MANIFEST_SCHEMA_VERSION: u32 = 1;
38
39// Why: not tied to the release version, and never swept by a version-bump
40// script. Raising it strands every client below it until they update, so it
41// moves only when the gateway makes a change an older bridge cannot handle.
42pub const MIN_BRIDGE_VERSION: &str = "0.28.0";
43
44#[must_use]
45pub fn bridge_version_is_supported(reported: &str, floor: &str) -> bool {
46    match (
47        semver::Version::parse(reported),
48        semver::Version::parse(floor),
49    ) {
50        (Ok(reported), Ok(floor)) => reported >= floor,
51        // Why: an unparseable version is almost always a local dev build;
52        // refusing those would make the gateway untestable against a work tree.
53        _ => true,
54    }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct SignedManifestEnvelope {
59    pub payload: String,
60    pub signature: ManifestSignature,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct SignedManifest {
65    #[serde(default)]
66    pub min_schema_version: u32,
67    #[serde(default)]
68    pub min_bridge_version: Option<String>,
69    pub manifest_version: ManifestVersion,
70    pub issued_at: String,
71    pub not_before: String,
72    pub user_id: UserId,
73    pub tenant_id: Option<TenantId>,
74    #[serde(default)]
75    pub user: Option<UserInfo>,
76    pub plugins: Vec<PluginEntry>,
77    #[serde(default)]
78    pub skills: Vec<SkillEntry>,
79    #[serde(default)]
80    pub agents: Vec<AgentEntry>,
81    #[serde(default)]
82    pub hooks: Vec<HookEntry>,
83    pub managed_mcp_servers: Vec<ManagedMcpServer>,
84    pub revocations: Vec<String>,
85    #[serde(default)]
86    pub enabled_hosts: Vec<String>,
87    #[serde(default)]
88    pub host_model_protocols: BTreeMap<String, Vec<String>>,
89    #[serde(default)]
90    pub artifacts: Vec<ArtifactEntry>,
91    #[serde(default)]
92    pub allow_claude_ai_connectors: bool,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct UserInfo {
97    pub id: UserId,
98    pub name: String,
99    pub email: String,
100    #[serde(default)]
101    pub display_name: Option<String>,
102    #[serde(default)]
103    pub roles: Vec<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct PluginEntry {
108    pub id: PluginId,
109    pub version: String,
110    pub sha256: Sha256Digest,
111    pub files: Vec<PluginFile>,
112    #[serde(default)]
113    pub hooks: PluginHooksRef,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct PluginFile {
118    pub path: String,
119    pub sha256: Sha256Digest,
120    pub size: u64,
121}
122
123/// A Cowork-native library document (raw HTML in the desktop app's Artifacts
124/// library) — not one of the in-chat MCP artifacts in [`crate::artifacts`].
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ArtifactEntry {
127    pub id: LibraryArtifactId,
128    pub name: String,
129    pub description: String,
130    pub version: String,
131    pub mcp_tools: Vec<String>,
132    pub content: String,
133    pub starred: bool,
134    pub sha256: Sha256Digest,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct SkillEntry {
139    pub id: SkillId,
140    pub name: SkillName,
141    pub description: String,
142    pub file_path: String,
143    #[serde(default)]
144    pub tags: Vec<String>,
145    pub sha256: Sha256Digest,
146    pub instructions: String,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct AgentEntry {
151    pub id: AgentId,
152    pub name: AgentName,
153    pub display_name: String,
154    pub description: String,
155    pub version: String,
156    pub endpoint: String,
157    pub enabled: bool,
158    pub is_default: bool,
159    pub is_primary: bool,
160    #[serde(default)]
161    pub provider: Option<String>,
162    #[serde(default)]
163    pub model: Option<String>,
164    #[serde(default)]
165    pub mcp_servers: PluginComponentRef,
166    #[serde(default)]
167    pub skills: PluginComponentRef,
168    #[serde(default)]
169    pub tags: Vec<String>,
170    #[serde(default)]
171    pub system_prompt: Option<String>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct HookEntry {
176    pub id: HookId,
177    pub name: String,
178    pub description: String,
179    pub version: String,
180    pub event: HookEvent,
181    pub matcher: String,
182    pub command: String,
183    #[serde(default)]
184    pub is_async: bool,
185    pub category: HookCategory,
186    #[serde(default)]
187    pub tags: Vec<String>,
188    pub sha256: Sha256Digest,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct ManagedMcpServer {
193    pub name: ManagedMcpServerName,
194    pub url: ValidatedUrl,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub transport: Option<String>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub headers: Option<BTreeMap<String, String>>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub oauth: Option<bool>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub tool_policy: Option<BTreeMap<ToolName, ToolPolicy>>,
203}