1use std::collections::{BTreeMap, BTreeSet};
13use std::path::{Path, PathBuf};
14use std::sync::{Arc, Mutex};
15
16use serde::{Deserialize, Serialize};
17
18use super::adopt::{FileRole, ScannedModel, io_err};
19use super::resolve::{consistency, files_of_body};
20use super::check::{CheckMode, ModelCheck, ModelState, check_entry};
21use super::body::{ModelError, ModelFormat, ModelPackageBody, VerifiedModel, attest, verify_model};
22use crate::revocation::RevocationList;
23use crate::workload_pkg::{ManifestEnvelope, TrustAnchors};
24
25pub const REGISTRY_SCHEMA: &str = "weftos.model-registry.v1";
27pub const MAX_REGISTRY_BYTES: u64 = 16 * 1024 * 1024;
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct LocalFile {
33 pub path: String,
35 pub role: FileRole,
37 pub size: u64,
39 pub blake3: String,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub verified: Option<(u64, u64)>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ModelEntry {
49 pub envelope: ManifestEnvelope,
51 pub root: PathBuf,
53 pub files: Vec<LocalFile>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub refused: Option<String>,
59 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
62 pub hidden: bool,
63}
64
65#[derive(Clone)]
69pub struct ModelTrust {
70 pub anchors: TrustAnchors,
72 pub revocations: Option<Arc<RevocationList>>,
74}
75
76impl std::fmt::Debug for ModelTrust {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.debug_struct("ModelTrust")
79 .field("signers", &self.anchors.signers.len())
80 .field("revocations", &self.revocations.is_some())
81 .finish()
82 }
83}
84
85impl ModelTrust {
86 pub fn new(anchors: TrustAnchors) -> Self {
88 Self { anchors, revocations: None }
89 }
90}
91
92impl ModelEntry {
93 pub fn body(&self) -> Result<ModelPackageBody, ModelError> {
95 serde_json::from_value(self.envelope.body.clone())
96 .map_err(|e| ModelError::Registry(format!("stored body: {e}")))
97 }
98
99 pub fn package_id(&self) -> Result<String, ModelError> {
101 Ok(self.envelope.package_id()?)
102 }
103}
104
105#[derive(Debug, Default, Serialize, Deserialize)]
106pub(super) struct RegistryFile {
107 pub(super) schema: String,
108 pub(super) models: BTreeMap<String, ModelEntry>,
109}
110
111#[derive(Debug, Clone)]
113pub struct ResolvedModel {
114 pub package_id: String,
116 pub name: String,
118 pub format: ModelFormat,
120 pub root: PathBuf,
122 pub shards: Vec<PathBuf>,
124 pub tokenizer: Option<PathBuf>,
126 pub body: ModelPackageBody,
128}
129
130#[derive(Debug, Clone)]
132pub struct AdoptedModel {
133 pub package_id: String,
135 pub envelope: ManifestEnvelope,
137 pub root: PathBuf,
139 pub verified: VerifiedModel,
141}
142
143pub struct ModelRegistry {
145 path: Option<PathBuf>,
146 pub(super) inner: Mutex<RegistryFile>,
147 pub(super) trust: Mutex<Option<ModelTrust>>,
148 pub(super) fully_verified: Mutex<BTreeSet<String>>,
151}
152
153impl std::fmt::Debug for ModelRegistry {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 f.debug_struct("ModelRegistry").field("path", &self.path).finish()
156 }
157}
158
159impl ModelRegistry {
160 pub fn in_memory() -> Self {
162 Self {
163 path: None,
164 inner: Mutex::new(RegistryFile {
165 schema: REGISTRY_SCHEMA.into(),
166 models: BTreeMap::new(),
167 }),
168 trust: Mutex::new(None),
169 fully_verified: Mutex::new(BTreeSet::new()),
170 }
171 }
172
173 pub fn with_trust(self, trust: ModelTrust) -> Self {
175 *self.trust.lock().unwrap_or_else(|p| p.into_inner()) = Some(trust);
176 self
177 }
178
179 pub fn set_trust(&self, trust: ModelTrust) {
182 *self.trust.lock().unwrap_or_else(|p| p.into_inner()) = Some(trust);
183 }
184
185 pub fn open(path: impl Into<PathBuf>) -> Result<Self, ModelError> {
187 let path = path.into();
188 let file = match std::fs::metadata(&path) {
189 Ok(m) if m.len() > MAX_REGISTRY_BYTES => {
190 return Err(ModelError::Registry("registry file too large".into()));
191 }
192 Ok(_) => {
193 let bytes = std::fs::read(&path).map_err(|e| io_err(&path, e))?;
194 let f: RegistryFile = serde_json::from_slice(&bytes)
195 .map_err(|e| ModelError::Registry(format!("{}: {e}", path.display())))?;
196 if f.schema != REGISTRY_SCHEMA {
197 return Err(ModelError::Registry(format!("unknown schema {:?}", f.schema)));
198 }
199 f
200 }
201 Err(e) if e.kind() == std::io::ErrorKind::NotFound => RegistryFile {
202 schema: REGISTRY_SCHEMA.into(),
203 models: BTreeMap::new(),
204 },
205 Err(e) => return Err(io_err(&path, e)),
206 };
207 for (id, e) in &file.models {
208 consistency(id, e).map_err(|why| {
209 ModelError::Registry(format!("{}: entry {id} is inconsistent: {why}", path.display()))
210 })?;
211 }
212 Ok(Self {
213 path: Some(path),
214 inner: Mutex::new(file),
215 trust: Mutex::new(None),
216 fully_verified: Mutex::new(BTreeSet::new()),
217 })
218 }
219
220 pub(super) fn lock(&self) -> std::sync::MutexGuard<'_, RegistryFile> {
221 self.inner.lock().unwrap_or_else(|p| p.into_inner())
222 }
223
224 pub(super) fn save(&self, f: &RegistryFile) -> Result<(), ModelError> {
225 let Some(path) = &self.path else { return Ok(()) };
226 if let Some(dir) = path.parent() {
227 std::fs::create_dir_all(dir).map_err(|e| io_err(dir, e))?;
228 }
229 let bytes = serde_json::to_vec_pretty(f).map_err(|e| ModelError::Registry(e.to_string()))?;
230 let tmp = path.with_extension("json.tmp");
231 std::fs::write(&tmp, &bytes).map_err(|e| io_err(&tmp, e))?;
232 #[cfg(unix)]
233 {
234 use std::os::unix::fs::PermissionsExt;
235 std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600))
236 .map_err(|e| io_err(&tmp, e))?;
237 }
238 std::fs::rename(&tmp, path).map_err(|e| io_err(path, e))
239 }
240
241 fn insert(
242 &self,
243 verified: &VerifiedModel,
244 root: PathBuf,
245 files: Vec<LocalFile>,
246 replace: bool,
247 ) -> Result<AdoptedModel, ModelError> {
248 let entry = ModelEntry {
249 envelope: verified.envelope.clone(),
250 root: root.clone(),
251 files: files.clone(),
252 refused: None,
253 hidden: false,
254 };
255 consistency(&verified.package_id, &entry).map_err(ModelError::Registry)?;
256 let mut g = self.lock();
257 let name = &verified.body.name;
258 let clash: Vec<String> = g
259 .models
260 .iter()
261 .filter(|(id, e)| {
262 **id != verified.package_id
263 && e.body().map(|b| &b.name == name).unwrap_or(false)
264 })
265 .map(|(id, _)| id.clone())
266 .collect();
267 if !clash.is_empty() && !replace {
268 return Err(ModelError::NameConflict(name.clone()));
269 }
270 for id in clash {
271 g.models.remove(&id);
272 }
273 g.models.insert(
274 verified.package_id.clone(),
275 ModelEntry {
276 envelope: verified.envelope.clone(),
277 root: root.clone(),
278 files,
279 refused: None,
280 hidden: false,
281 },
282 );
283 self.save(&g)?;
284 Ok(AdoptedModel {
285 package_id: verified.package_id.clone(),
286 envelope: verified.envelope.clone(),
287 root,
288 verified: verified.clone(),
289 })
290 }
291
292 pub fn adopt(
297 &self,
298 scanned: ScannedModel,
299 key: &ed25519_dalek::SigningKey,
300 key_id: &str,
301 anchors: &TrustAnchors,
302 replace: bool,
303 ) -> Result<AdoptedModel, ModelError> {
304 let envelope = attest(&scanned.body, key, key_id)?;
305 let verified = verify_model(&envelope, anchors)?;
306 let files = scanned
307 .files
308 .iter()
309 .map(|f| LocalFile {
310 path: f.path.clone(),
311 role: f.role,
312 size: f.size,
313 blake3: f.blake3.clone(),
314 verified: Some((f.size, f.mtime_ns)),
315 })
316 .collect();
317 let adopted = self.insert(&verified, scanned.root, files, replace)?;
318 self.fully_verified
320 .lock()
321 .unwrap_or_else(|p| p.into_inner())
322 .insert(adopted.package_id.clone());
323 Ok(adopted)
324 }
325
326 pub fn attach(
330 &self,
331 envelope: &ManifestEnvelope,
332 root: &Path,
333 anchors: &TrustAnchors,
334 replace: bool,
335 ) -> Result<AdoptedModel, ModelError> {
336 let verified = verify_model(envelope, anchors)?;
337 let root = root.canonicalize().map_err(|e| io_err(root, e))?;
338 let files = files_of_body(&verified.body);
339 self.insert(&verified, root, files, replace)
340 }
341
342 pub fn remove(&self, id_or_name: &str) -> Result<(), ModelError> {
344 let mut g = self.lock();
345 let id = Self::find(&g, id_or_name)?;
346 g.models.remove(&id);
347 self.save(&g)
348 }
349
350 pub(super) fn find(g: &RegistryFile, id_or_name: &str) -> Result<String, ModelError> {
351 if g.models.contains_key(id_or_name) {
352 return Ok(id_or_name.to_string());
353 }
354 g.models
355 .iter()
356 .find(|(_, e)| e.body().map(|b| b.name == id_or_name).unwrap_or(false))
357 .map(|(id, _)| id.clone())
358 .ok_or_else(|| ModelError::Unknown(id_or_name.chars().take(80).collect()))
359 }
360
361 pub fn entries(&self) -> Vec<(String, ModelEntry)> {
363 self.lock().models.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
364 }
365
366 pub fn get(&self, id_or_name: &str) -> Result<(String, ModelEntry), ModelError> {
368 let g = self.lock();
369 let id = Self::find(&g, id_or_name)?;
370 Ok((id.clone(), g.models[&id].clone()))
371 }
372
373 pub fn check(&self, id_or_name: &str, mode: CheckMode) -> Result<ModelCheck, ModelError> {
375 let (id, entry) = self.get(id_or_name)?;
376 let (check, stamps) = check_entry(&id, &entry, mode)?;
377 if mode != CheckMode::Stat {
378 let mut g = self.lock();
379 if let Some(e) = g.models.get_mut(&id) {
380 let mut changed = false;
381 for (f, s) in e.files.iter_mut().zip(stamps) {
382 if f.verified != s {
383 f.verified = s;
384 changed = true;
385 }
386 }
387 match &check.state {
388 ModelState::Refused { reason } if e.refused.as_deref() != Some(reason) => {
389 e.refused = Some(reason.clone());
390 changed = true;
391 }
392 ModelState::Ready if mode == CheckMode::Full && e.refused.is_some() => {
393 e.refused = None;
394 changed = true;
395 }
396 _ => {}
397 }
398 if changed {
399 self.save(&g)?;
400 }
401 }
402 }
403 Ok(check)
404 }
405}