use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use super::adopt::{FileRole, ScannedModel, io_err};
use super::resolve::{consistency, files_of_body};
use super::check::{CheckMode, ModelCheck, ModelState, check_entry};
use super::body::{ModelError, ModelFormat, ModelPackageBody, VerifiedModel, attest, verify_model};
use crate::revocation::RevocationList;
use crate::workload_pkg::{ManifestEnvelope, TrustAnchors};
pub const REGISTRY_SCHEMA: &str = "weftos.model-registry.v1";
pub const MAX_REGISTRY_BYTES: u64 = 16 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LocalFile {
pub path: String,
pub role: FileRole,
pub size: u64,
pub blake3: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verified: Option<(u64, u64)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelEntry {
pub envelope: ManifestEnvelope,
pub root: PathBuf,
pub files: Vec<LocalFile>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refused: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub hidden: bool,
}
#[derive(Clone)]
pub struct ModelTrust {
pub anchors: TrustAnchors,
pub revocations: Option<Arc<RevocationList>>,
}
impl std::fmt::Debug for ModelTrust {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModelTrust")
.field("signers", &self.anchors.signers.len())
.field("revocations", &self.revocations.is_some())
.finish()
}
}
impl ModelTrust {
pub fn new(anchors: TrustAnchors) -> Self {
Self { anchors, revocations: None }
}
}
impl ModelEntry {
pub fn body(&self) -> Result<ModelPackageBody, ModelError> {
serde_json::from_value(self.envelope.body.clone())
.map_err(|e| ModelError::Registry(format!("stored body: {e}")))
}
pub fn package_id(&self) -> Result<String, ModelError> {
Ok(self.envelope.package_id()?)
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub(super) struct RegistryFile {
pub(super) schema: String,
pub(super) models: BTreeMap<String, ModelEntry>,
}
#[derive(Debug, Clone)]
pub struct ResolvedModel {
pub package_id: String,
pub name: String,
pub format: ModelFormat,
pub root: PathBuf,
pub shards: Vec<PathBuf>,
pub tokenizer: Option<PathBuf>,
pub body: ModelPackageBody,
}
#[derive(Debug, Clone)]
pub struct AdoptedModel {
pub package_id: String,
pub envelope: ManifestEnvelope,
pub root: PathBuf,
pub verified: VerifiedModel,
}
pub struct ModelRegistry {
path: Option<PathBuf>,
pub(super) inner: Mutex<RegistryFile>,
pub(super) trust: Mutex<Option<ModelTrust>>,
pub(super) fully_verified: Mutex<BTreeSet<String>>,
}
impl std::fmt::Debug for ModelRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModelRegistry").field("path", &self.path).finish()
}
}
impl ModelRegistry {
pub fn in_memory() -> Self {
Self {
path: None,
inner: Mutex::new(RegistryFile {
schema: REGISTRY_SCHEMA.into(),
models: BTreeMap::new(),
}),
trust: Mutex::new(None),
fully_verified: Mutex::new(BTreeSet::new()),
}
}
pub fn with_trust(self, trust: ModelTrust) -> Self {
*self.trust.lock().unwrap_or_else(|p| p.into_inner()) = Some(trust);
self
}
pub fn set_trust(&self, trust: ModelTrust) {
*self.trust.lock().unwrap_or_else(|p| p.into_inner()) = Some(trust);
}
pub fn open(path: impl Into<PathBuf>) -> Result<Self, ModelError> {
let path = path.into();
let file = match std::fs::metadata(&path) {
Ok(m) if m.len() > MAX_REGISTRY_BYTES => {
return Err(ModelError::Registry("registry file too large".into()));
}
Ok(_) => {
let bytes = std::fs::read(&path).map_err(|e| io_err(&path, e))?;
let f: RegistryFile = serde_json::from_slice(&bytes)
.map_err(|e| ModelError::Registry(format!("{}: {e}", path.display())))?;
if f.schema != REGISTRY_SCHEMA {
return Err(ModelError::Registry(format!("unknown schema {:?}", f.schema)));
}
f
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => RegistryFile {
schema: REGISTRY_SCHEMA.into(),
models: BTreeMap::new(),
},
Err(e) => return Err(io_err(&path, e)),
};
for (id, e) in &file.models {
consistency(id, e).map_err(|why| {
ModelError::Registry(format!("{}: entry {id} is inconsistent: {why}", path.display()))
})?;
}
Ok(Self {
path: Some(path),
inner: Mutex::new(file),
trust: Mutex::new(None),
fully_verified: Mutex::new(BTreeSet::new()),
})
}
pub(super) fn lock(&self) -> std::sync::MutexGuard<'_, RegistryFile> {
self.inner.lock().unwrap_or_else(|p| p.into_inner())
}
pub(super) fn save(&self, f: &RegistryFile) -> Result<(), ModelError> {
let Some(path) = &self.path else { return Ok(()) };
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).map_err(|e| io_err(dir, e))?;
}
let bytes = serde_json::to_vec_pretty(f).map_err(|e| ModelError::Registry(e.to_string()))?;
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, &bytes).map_err(|e| io_err(&tmp, e))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600))
.map_err(|e| io_err(&tmp, e))?;
}
std::fs::rename(&tmp, path).map_err(|e| io_err(path, e))
}
fn insert(
&self,
verified: &VerifiedModel,
root: PathBuf,
files: Vec<LocalFile>,
replace: bool,
) -> Result<AdoptedModel, ModelError> {
let entry = ModelEntry {
envelope: verified.envelope.clone(),
root: root.clone(),
files: files.clone(),
refused: None,
hidden: false,
};
consistency(&verified.package_id, &entry).map_err(ModelError::Registry)?;
let mut g = self.lock();
let name = &verified.body.name;
let clash: Vec<String> = g
.models
.iter()
.filter(|(id, e)| {
**id != verified.package_id
&& e.body().map(|b| &b.name == name).unwrap_or(false)
})
.map(|(id, _)| id.clone())
.collect();
if !clash.is_empty() && !replace {
return Err(ModelError::NameConflict(name.clone()));
}
for id in clash {
g.models.remove(&id);
}
g.models.insert(
verified.package_id.clone(),
ModelEntry {
envelope: verified.envelope.clone(),
root: root.clone(),
files,
refused: None,
hidden: false,
},
);
self.save(&g)?;
Ok(AdoptedModel {
package_id: verified.package_id.clone(),
envelope: verified.envelope.clone(),
root,
verified: verified.clone(),
})
}
pub fn adopt(
&self,
scanned: ScannedModel,
key: &ed25519_dalek::SigningKey,
key_id: &str,
anchors: &TrustAnchors,
replace: bool,
) -> Result<AdoptedModel, ModelError> {
let envelope = attest(&scanned.body, key, key_id)?;
let verified = verify_model(&envelope, anchors)?;
let files = scanned
.files
.iter()
.map(|f| LocalFile {
path: f.path.clone(),
role: f.role,
size: f.size,
blake3: f.blake3.clone(),
verified: Some((f.size, f.mtime_ns)),
})
.collect();
let adopted = self.insert(&verified, scanned.root, files, replace)?;
self.fully_verified
.lock()
.unwrap_or_else(|p| p.into_inner())
.insert(adopted.package_id.clone());
Ok(adopted)
}
pub fn attach(
&self,
envelope: &ManifestEnvelope,
root: &Path,
anchors: &TrustAnchors,
replace: bool,
) -> Result<AdoptedModel, ModelError> {
let verified = verify_model(envelope, anchors)?;
let root = root.canonicalize().map_err(|e| io_err(root, e))?;
let files = files_of_body(&verified.body);
self.insert(&verified, root, files, replace)
}
pub fn remove(&self, id_or_name: &str) -> Result<(), ModelError> {
let mut g = self.lock();
let id = Self::find(&g, id_or_name)?;
g.models.remove(&id);
self.save(&g)
}
pub(super) fn find(g: &RegistryFile, id_or_name: &str) -> Result<String, ModelError> {
if g.models.contains_key(id_or_name) {
return Ok(id_or_name.to_string());
}
g.models
.iter()
.find(|(_, e)| e.body().map(|b| b.name == id_or_name).unwrap_or(false))
.map(|(id, _)| id.clone())
.ok_or_else(|| ModelError::Unknown(id_or_name.chars().take(80).collect()))
}
pub fn entries(&self) -> Vec<(String, ModelEntry)> {
self.lock().models.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
}
pub fn get(&self, id_or_name: &str) -> Result<(String, ModelEntry), ModelError> {
let g = self.lock();
let id = Self::find(&g, id_or_name)?;
Ok((id.clone(), g.models[&id].clone()))
}
pub fn check(&self, id_or_name: &str, mode: CheckMode) -> Result<ModelCheck, ModelError> {
let (id, entry) = self.get(id_or_name)?;
let (check, stamps) = check_entry(&id, &entry, mode)?;
if mode != CheckMode::Stat {
let mut g = self.lock();
if let Some(e) = g.models.get_mut(&id) {
let mut changed = false;
for (f, s) in e.files.iter_mut().zip(stamps) {
if f.verified != s {
f.verified = s;
changed = true;
}
}
match &check.state {
ModelState::Refused { reason } if e.refused.as_deref() != Some(reason) => {
e.refused = Some(reason.clone());
changed = true;
}
ModelState::Ready if mode == CheckMode::Full && e.refused.is_some() => {
e.refused = None;
changed = true;
}
_ => {}
}
if changed {
self.save(&g)?;
}
}
}
Ok(check)
}
}