1use 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::{
36 AgentId, AgentName, HookId, McpServerId, TenantId, UserId, ValidatedUrl,
37};
38
39pub const MANIFEST_SCHEMA_VERSION: u32 = 1;
40
41pub const MIN_BRIDGE_VERSION: &str = "0.28.0";
45
46#[must_use]
47pub fn bridge_version_is_supported(reported: &str, floor: &str) -> bool {
48 match (
49 semver::Version::parse(reported),
50 semver::Version::parse(floor),
51 ) {
52 (Ok(reported), Ok(floor)) => reported >= floor,
53 _ => true,
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SignedManifestEnvelope {
61 pub payload: String,
62 pub signature: ManifestSignature,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct SignedManifest {
67 #[serde(default)]
68 pub min_schema_version: u32,
69 #[serde(default)]
70 pub min_bridge_version: Option<String>,
71 pub manifest_version: ManifestVersion,
72 pub issued_at: String,
73 pub not_before: String,
74 pub user_id: UserId,
75 pub tenant_id: Option<TenantId>,
76 #[serde(default)]
77 pub user: Option<UserInfo>,
78 pub plugins: Vec<PluginEntry>,
79 #[serde(default)]
80 pub skills: Vec<SkillEntry>,
81 #[serde(default)]
82 pub agents: Vec<AgentEntry>,
83 #[serde(default)]
84 pub hooks: Vec<HookEntry>,
85 pub managed_mcp_servers: Vec<ManagedMcpServer>,
86 pub revocations: Vec<String>,
87 #[serde(default)]
88 pub enabled_hosts: Vec<String>,
89 #[serde(default)]
90 pub host_model_protocols: BTreeMap<String, Vec<String>>,
91 #[serde(default)]
92 pub artifacts: Vec<ArtifactEntry>,
93 #[serde(default)]
94 pub allow_claude_ai_connectors: bool,
95 #[serde(default, skip_serializing_if = "Vec::is_empty")]
96 pub diagnostics: Vec<String>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct UserInfo {
101 pub id: UserId,
102 pub name: String,
103 pub email: String,
104 #[serde(default)]
105 pub display_name: Option<String>,
106 #[serde(default)]
107 pub roles: Vec<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct PluginEntry {
112 pub id: PluginId,
113 pub version: String,
114 pub sha256: Sha256Digest,
115 pub files: Vec<PluginFile>,
116 #[serde(default)]
117 pub hooks: PluginHooksRef,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct PluginFile {
122 pub path: String,
123 pub sha256: Sha256Digest,
124 pub size: u64,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ArtifactEntry {
131 pub id: LibraryArtifactId,
132 pub name: String,
133 pub description: String,
134 pub version: String,
135 pub mcp_tools: Vec<String>,
136 pub content: String,
137 pub starred: bool,
138 pub sha256: Sha256Digest,
139}
140
141#[derive(Debug, Serialize)]
145pub struct CoworkLibraryArtifactRecord<'a> {
146 pub id: &'a str,
147 pub name: &'a str,
148 pub description: &'a str,
149 pub version: &'a str,
150 pub content: &'a str,
151 #[serde(rename = "isStarred")]
152 pub is_starred: bool,
153 #[serde(rename = "mcpTools")]
154 pub mcp_tools: &'a [String],
155}
156
157impl<'a> From<&'a ArtifactEntry> for CoworkLibraryArtifactRecord<'a> {
158 fn from(a: &'a ArtifactEntry) -> Self {
159 Self {
160 id: a.id.as_str(),
161 name: &a.name,
162 description: &a.description,
163 version: &a.version,
164 content: &a.content,
165 is_starred: a.starred,
166 mcp_tools: &a.mcp_tools,
167 }
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct CoworkArtifactBundleManifest {
176 pub artifacts: Vec<CoworkArtifactBundleRecord>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct CoworkArtifactBundleRecord {
181 pub id: LibraryArtifactId,
182 pub name: String,
183 pub description: String,
184 pub version: String,
185 #[serde(rename = "isStarred")]
186 pub is_starred: bool,
187 #[serde(rename = "mcpTools")]
188 pub mcp_tools: Vec<String>,
189}
190
191impl From<&ArtifactEntry> for CoworkArtifactBundleRecord {
192 fn from(a: &ArtifactEntry) -> Self {
193 Self {
194 id: a.id.clone(),
195 name: a.name.clone(),
196 description: a.description.clone(),
197 version: a.version.clone(),
198 is_starred: a.starred,
199 mcp_tools: a.mcp_tools.clone(),
200 }
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct SkillEntry {
206 pub id: SkillId,
207 pub name: SkillName,
208 pub description: String,
209 pub file_path: String,
210 #[serde(default)]
211 pub tags: Vec<String>,
212 pub sha256: Sha256Digest,
213 pub instructions: String,
214 #[serde(default, skip_serializing_if = "Vec::is_empty")]
215 pub hosts: Vec<String>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct AgentEntry {
220 pub id: AgentId,
221 pub name: AgentName,
222 pub display_name: String,
223 pub description: String,
224 pub version: String,
225 pub endpoint: String,
226 pub enabled: bool,
227 pub is_default: bool,
228 pub is_primary: bool,
229 #[serde(default)]
230 pub provider: Option<String>,
231 #[serde(default)]
232 pub model: Option<String>,
233 #[serde(default)]
234 pub mcp_servers: PluginComponentRef,
235 #[serde(default)]
236 pub skills: PluginComponentRef,
237 #[serde(default)]
238 pub tags: Vec<String>,
239 #[serde(default)]
240 pub system_prompt: Option<String>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct HookEntry {
245 pub id: HookId,
246 pub name: String,
247 pub description: String,
248 pub version: String,
249 pub event: HookEvent,
250 pub matcher: String,
251 pub command: String,
252 #[serde(default)]
253 pub is_async: bool,
254 pub category: HookCategory,
255 #[serde(default)]
256 pub tags: Vec<String>,
257 pub sha256: Sha256Digest,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
261#[serde(from = "ManagedMcpServerWire")]
262pub struct ManagedMcpServer {
263 pub id: McpServerId,
264 pub name: ManagedMcpServerName,
265 pub url: ValidatedUrl,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub transport: Option<String>,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub headers: Option<BTreeMap<String, String>>,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub oauth: Option<bool>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub tool_policy: Option<BTreeMap<ToolName, ToolPolicy>>,
274}
275
276#[derive(Deserialize)]
279struct ManagedMcpServerWire {
280 #[serde(default)]
281 id: Option<McpServerId>,
282 name: ManagedMcpServerName,
283 url: ValidatedUrl,
284 #[serde(default)]
285 transport: Option<String>,
286 #[serde(default)]
287 headers: Option<BTreeMap<String, String>>,
288 #[serde(default)]
289 oauth: Option<bool>,
290 #[serde(default)]
291 tool_policy: Option<BTreeMap<ToolName, ToolPolicy>>,
292}
293
294impl From<ManagedMcpServerWire> for ManagedMcpServer {
295 fn from(wire: ManagedMcpServerWire) -> Self {
296 let id = wire
297 .id
298 .unwrap_or_else(|| McpServerId::new(wire.name.as_str()));
299 Self {
300 id,
301 name: wire.name,
302 url: wire.url,
303 transport: wire.transport,
304 headers: wire.headers,
305 oauth: wire.oauth,
306 tool_policy: wire.tool_policy,
307 }
308 }
309}