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#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct SignedManifestEnvelope {
41    pub payload: String,
42    pub signature: ManifestSignature,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct SignedManifest {
47    #[serde(default)]
48    pub min_schema_version: u32,
49    pub manifest_version: ManifestVersion,
50    pub issued_at: String,
51    pub not_before: String,
52    pub user_id: UserId,
53    pub tenant_id: Option<TenantId>,
54    #[serde(default)]
55    pub user: Option<UserInfo>,
56    pub plugins: Vec<PluginEntry>,
57    #[serde(default)]
58    pub skills: Vec<SkillEntry>,
59    #[serde(default)]
60    pub agents: Vec<AgentEntry>,
61    #[serde(default)]
62    pub hooks: Vec<HookEntry>,
63    pub managed_mcp_servers: Vec<ManagedMcpServer>,
64    pub revocations: Vec<String>,
65    #[serde(default)]
66    pub enabled_hosts: Vec<String>,
67    #[serde(default)]
68    pub host_model_protocols: BTreeMap<String, Vec<String>>,
69    #[serde(default)]
70    pub artifacts: Vec<ArtifactEntry>,
71    #[serde(default)]
72    pub allow_claude_ai_connectors: bool,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct UserInfo {
77    pub id: UserId,
78    pub name: String,
79    pub email: String,
80    #[serde(default)]
81    pub display_name: Option<String>,
82    #[serde(default)]
83    pub roles: Vec<String>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct PluginEntry {
88    pub id: PluginId,
89    pub version: String,
90    pub sha256: Sha256Digest,
91    pub files: Vec<PluginFile>,
92    #[serde(default)]
93    pub hooks: PluginHooksRef,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct PluginFile {
98    pub path: String,
99    pub sha256: Sha256Digest,
100    pub size: u64,
101}
102
103/// A Cowork-native library document (raw HTML in the desktop app's Artifacts
104/// library) — not one of the in-chat MCP artifacts in [`crate::artifacts`].
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ArtifactEntry {
107    pub id: LibraryArtifactId,
108    pub name: String,
109    pub description: String,
110    pub version: String,
111    pub mcp_tools: Vec<String>,
112    pub content: String,
113    pub starred: bool,
114    pub sha256: Sha256Digest,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct SkillEntry {
119    pub id: SkillId,
120    pub name: SkillName,
121    pub description: String,
122    pub file_path: String,
123    #[serde(default)]
124    pub tags: Vec<String>,
125    pub sha256: Sha256Digest,
126    pub instructions: String,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct AgentEntry {
131    pub id: AgentId,
132    pub name: AgentName,
133    pub display_name: String,
134    pub description: String,
135    pub version: String,
136    pub endpoint: String,
137    pub enabled: bool,
138    pub is_default: bool,
139    pub is_primary: bool,
140    #[serde(default)]
141    pub provider: Option<String>,
142    #[serde(default)]
143    pub model: Option<String>,
144    #[serde(default)]
145    pub mcp_servers: PluginComponentRef,
146    #[serde(default)]
147    pub skills: PluginComponentRef,
148    #[serde(default)]
149    pub tags: Vec<String>,
150    #[serde(default)]
151    pub system_prompt: Option<String>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct HookEntry {
156    pub id: HookId,
157    pub name: String,
158    pub description: String,
159    pub version: String,
160    pub event: HookEvent,
161    pub matcher: String,
162    pub command: String,
163    #[serde(default)]
164    pub is_async: bool,
165    pub category: HookCategory,
166    #[serde(default)]
167    pub tags: Vec<String>,
168    pub sha256: Sha256Digest,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct ManagedMcpServer {
173    pub name: ManagedMcpServerName,
174    pub url: ValidatedUrl,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub transport: Option<String>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub headers: Option<BTreeMap<String, String>>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub oauth: Option<bool>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub tool_policy: Option<BTreeMap<ToolName, ToolPolicy>>,
183}