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
37/// Schema level this build of the codebase emits and understands.
38pub const MANIFEST_SCHEMA_VERSION: u32 = 1;
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct SignedManifestEnvelope {
42    /// JCS-canonical [`SignedManifest`] JSON, signed byte-for-byte. Consumers
43    /// must verify the signature over this exact string before parsing it.
44    pub payload: String,
45    /// Detached ed25519 signature over `payload`; the empty string on
46    /// unsigned installations.
47    pub signature: ManifestSignature,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct SignedManifest {
52    /// Oldest schema level that can safely consume this manifest. Additive
53    /// fields leave it unchanged; only semantic breaks raise it.
54    #[serde(default)]
55    pub min_schema_version: u32,
56    pub manifest_version: ManifestVersion,
57    pub issued_at: String,
58    pub not_before: String,
59    pub user_id: UserId,
60    pub tenant_id: Option<TenantId>,
61    #[serde(default)]
62    pub user: Option<UserInfo>,
63    pub plugins: Vec<PluginEntry>,
64    #[serde(default)]
65    pub skills: Vec<SkillEntry>,
66    #[serde(default)]
67    pub agents: Vec<AgentEntry>,
68    #[serde(default)]
69    pub hooks: Vec<HookEntry>,
70    pub managed_mcp_servers: Vec<ManagedMcpServer>,
71    pub revocations: Vec<String>,
72    #[serde(default)]
73    pub enabled_hosts: Vec<String>,
74    /// Optional per-host wire-protocol filter, keyed by host id. A present
75    /// entry overrides the host's built-in default `accepted_protocols`; an
76    /// empty value means "all models" (no restriction). An absent entry leaves
77    /// the host on its default.
78    #[serde(default)]
79    pub host_model_protocols: BTreeMap<String, Vec<String>>,
80    /// Cowork global-library HTML documents — distinct from the in-chat MCP
81    /// artifacts in [`crate::artifacts`].
82    #[serde(default)]
83    pub artifacts: Vec<ArtifactEntry>,
84    /// Instructs the bridge's Claude Code managed-MCP policy to emit
85    /// `allowAllClaudeAiMcps`, re-allowing claude.ai first-party connectors
86    /// that `managed-mcp.json` would otherwise suppress.
87    #[serde(default)]
88    pub allow_claude_ai_connectors: bool,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct UserInfo {
93    pub id: UserId,
94    pub name: String,
95    pub email: String,
96    #[serde(default)]
97    pub display_name: Option<String>,
98    #[serde(default)]
99    pub roles: Vec<String>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct PluginEntry {
104    pub id: PluginId,
105    pub version: String,
106    pub sha256: Sha256Digest,
107    pub files: Vec<PluginFile>,
108    #[serde(default)]
109    pub hooks: PluginHooksRef,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct PluginFile {
114    pub path: String,
115    pub sha256: Sha256Digest,
116    pub size: u64,
117}
118
119/// A Cowork-native library document (raw HTML in the desktop app's Artifacts
120/// library) — not one of the in-chat MCP artifacts in [`crate::artifacts`].
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ArtifactEntry {
123    pub id: LibraryArtifactId,
124    pub name: String,
125    pub description: String,
126    pub version: String,
127    pub mcp_tools: Vec<String>,
128    pub content: String,
129    pub starred: bool,
130    pub sha256: Sha256Digest,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct SkillEntry {
135    pub id: SkillId,
136    pub name: SkillName,
137    pub description: String,
138    pub file_path: String,
139    #[serde(default)]
140    pub tags: Vec<String>,
141    pub sha256: Sha256Digest,
142    pub instructions: String,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct AgentEntry {
147    pub id: AgentId,
148    pub name: AgentName,
149    pub display_name: String,
150    pub description: String,
151    pub version: String,
152    pub endpoint: String,
153    pub enabled: bool,
154    pub is_default: bool,
155    pub is_primary: bool,
156    #[serde(default)]
157    pub provider: Option<String>,
158    #[serde(default)]
159    pub model: Option<String>,
160    #[serde(default)]
161    pub mcp_servers: PluginComponentRef,
162    #[serde(default)]
163    pub skills: PluginComponentRef,
164    #[serde(default)]
165    pub tags: Vec<String>,
166    #[serde(default)]
167    pub system_prompt: Option<String>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct HookEntry {
172    pub id: HookId,
173    pub name: String,
174    pub description: String,
175    pub version: String,
176    pub event: HookEvent,
177    pub matcher: String,
178    pub command: String,
179    #[serde(default)]
180    pub is_async: bool,
181    pub category: HookCategory,
182    #[serde(default)]
183    pub tags: Vec<String>,
184    pub sha256: Sha256Digest,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct ManagedMcpServer {
189    pub name: ManagedMcpServerName,
190    pub url: ValidatedUrl,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub transport: Option<String>,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub headers: Option<BTreeMap<String, String>>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub oauth: Option<bool>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub tool_policy: Option<BTreeMap<ToolName, ToolPolicy>>,
199}