systemprompt_models/services/
bundle.rs1use std::collections::BTreeMap;
17
18use chrono::{DateTime, Utc};
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21
22pub const BUNDLE_MANIFEST_FILE: &str = "bundle.json";
23pub const BUNDLE_FORMAT_VERSION: u32 = 1;
24pub const BUNDLE_MEDIA_TYPE: &str = "application/vnd.systemprompt.services-bundle.v1.tar+gzip";
25pub const BUNDLE_SIGNATURE_ALG: &str = "ed25519";
26
27pub const BUNDLE_ALLOWED_DIRS: &[&str] = &[
28 "access-control",
29 "agents",
30 "ai",
31 "artifacts",
32 "config",
33 "content",
34 "external_agents",
35 "gateway",
36 "governance",
37 "hooks",
38 "marketplaces",
39 "mcp",
40 "plugins",
41 "rules",
42 "scheduler",
43 "skills",
44 "slack",
45 "web",
46];
47
48pub const MARKETPLACE_BUNDLE_DIRS: &[&str] = &[
49 "marketplaces",
50 "plugins",
51 "skills",
52 "rules",
53 "hooks",
54 "artifacts",
55];
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct FileEntry {
60 pub path: String,
61
62 #[serde(rename = "checksum")]
63 pub sha256: String,
64
65 pub size: u64,
66}
67
68#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct BundleSourceInfo {
71 #[serde(default)]
72 pub repo: Option<String>,
73
74 #[serde(default)]
75 pub commit: Option<String>,
76
77 #[serde(default)]
78 pub workflow_run: Option<String>,
79}
80
81#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct BundleOwnership {
84 #[serde(default)]
85 pub marketplaces: Vec<String>,
86
87 #[serde(default)]
88 pub plugins: Vec<String>,
89
90 #[serde(default)]
91 pub skills: Vec<String>,
92
93 #[serde(default)]
94 pub rules: Vec<String>,
95
96 #[serde(default)]
97 pub hooks: Vec<String>,
98
99 #[serde(default)]
100 pub artifacts: Vec<String>,
101
102 #[serde(default)]
103 pub dirs: Vec<String>,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(deny_unknown_fields)]
108pub struct ServicesBundleManifest {
109 pub format: u32,
110
111 pub version: String,
112
113 pub created_at: DateTime<Utc>,
114
115 pub requires_core: String,
116
117 #[serde(default)]
118 pub source: BundleSourceInfo,
119
120 #[serde(default)]
121 pub files: Vec<FileEntry>,
122
123 pub content_hash: String,
124
125 #[serde(default)]
126 pub total_size: u64,
127
128 #[serde(default)]
129 pub owns: BundleOwnership,
130}
131
132impl ServicesBundleManifest {
133 #[must_use]
134 pub fn compute_content_hash(files: &[FileEntry]) -> String {
135 let mut lines: Vec<String> = files
136 .iter()
137 .map(|f| format!("{}\0{}\n", f.path, f.sha256))
138 .collect();
139 lines.sort();
140
141 let mut hasher = Sha256::new();
142 for line in &lines {
143 hasher.update(line.as_bytes());
144 }
145 hex::encode(hasher.finalize())
146 }
147
148 #[must_use]
149 pub fn is_marketplace_only(&self) -> bool {
150 !self.owns.dirs.is_empty()
151 && self
152 .owns
153 .dirs
154 .iter()
155 .all(|d| MARKETPLACE_BUNDLE_DIRS.contains(&d.as_str()))
156 }
157
158 pub fn core_satisfies(&self, core_version: &str) -> Result<bool, semver::Error> {
159 let req = semver::VersionReq::parse(&self.requires_core)?;
160 let version = semver::Version::parse(core_version)?;
161 Ok(req.matches(&version))
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(deny_unknown_fields)]
167pub struct BundleSignature {
168 pub alg: String,
169
170 pub key_id: String,
171
172 pub sig_b64: String,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(deny_unknown_fields)]
177pub struct SignedBundleManifest {
178 pub manifest: ServicesBundleManifest,
179
180 #[serde(default)]
181 pub signature: Option<BundleSignature>,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(deny_unknown_fields)]
186pub struct BundleSourceState {
187 pub digest: String,
188
189 pub version: String,
190
191 pub content_hash: String,
192
193 pub fetched_at: DateTime<Utc>,
194}
195
196#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
197#[serde(deny_unknown_fields)]
198pub struct ServicesBundleState {
199 #[serde(default)]
200 pub composed_hash: String,
201
202 #[serde(default)]
203 pub last_reconciled_hash: Option<String>,
204
205 #[serde(default)]
206 pub sources: BTreeMap<String, BundleSourceState>,
207}