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