clawft_kernel/model_manifest/
resolve.rs1use super::adopt::{FileRole, allowed_roots, contained};
6use super::body::{ModelError, ModelPackageBody, verify_model};
7use super::check::{CheckMode, ModelState};
8use super::registry::{LocalFile, ModelEntry, ModelRegistry, ModelTrust, ResolvedModel};
9use crate::workload_pkg::codec::hex_encode;
10
11impl ModelRegistry {
12 pub fn set_advertised(&self, id_or_name: &str, advertised: bool) -> Result<(), ModelError> {
15 let mut g = self.lock();
16 let id = Self::find(&g, id_or_name)?;
17 if let Some(e) = g.models.get_mut(&id) {
18 e.hidden = !advertised;
19 }
20 self.save(&g)
21 }
22
23 pub(crate) fn trust_problem(&self, id: &str, entry: &ModelEntry, trust: &ModelTrust) -> Option<String> {
28 if let Err(why) = consistency(id, entry) {
29 return Some(why);
30 }
31 let verified = match verify_model(&entry.envelope, &trust.anchors) {
32 Ok(v) => v,
33 Err(e) => return Some(format!("attestation does not verify: {e}")),
34 };
35 if verified.package_id != id {
36 return Some("package id differs from the registry key".into());
37 }
38 let list = trust.revocations.as_ref()?;
39 let signers: Vec<String> = verified
40 .signers
41 .iter()
42 .filter_map(|s| trust.anchors.signers.iter().find(|k| k.key_id == s.key_id))
43 .map(|k| hex_encode(&k.public_key))
44 .collect();
45 let hashes: Vec<String> = verified.body.shard_hashes().map(String::from).collect();
46 list.first_revoked(Some(id), signers.iter(), hashes.iter())
47 .map(|r| format!("revoked: {r:?}"))
48 }
49
50 pub(crate) fn untrusted(&self, id: &str, entry: &ModelEntry) -> bool {
54 let trust = self.trust.lock().unwrap_or_else(|p| p.into_inner()).clone();
55 match trust {
56 Some(t) => self.trust_problem(id, entry, &t).is_some(),
57 None => consistency(id, entry).is_err(),
58 }
59 }
60
61 pub fn resolve(&self, id_or_name: &str) -> Result<ResolvedModel, ModelError> {
82 let trust = self.trust.lock().unwrap_or_else(|p| p.into_inner()).clone();
83 let id = self.get(id_or_name).map(|(id, _)| id)?;
84 let Some(trust) = trust else {
85 return Err(ModelError::NotReady {
86 id,
87 reason: "the registry has no trust anchors configured".into(),
88 });
89 };
90 self.resolve_with(id_or_name, &trust)
91 }
92
93 pub fn resolve_with(&self, id_or_name: &str, trust: &ModelTrust) -> Result<ResolvedModel, ModelError> {
96 let (id, entry) = self.get(id_or_name)?;
97 let not_ready = |reason: String| ModelError::NotReady { id: id.clone(), reason };
98 if let Some(why) = self.trust_problem(&id, &entry, trust) {
99 return Err(not_ready(format!("untrusted: {why}")));
100 }
101 let first = !self
102 .fully_verified
103 .lock()
104 .unwrap_or_else(|p| p.into_inner())
105 .contains(&id);
106 let mode = if first { CheckMode::Full } else { CheckMode::Lazy };
107 let check = self.check(&id, mode)?;
108 let entry = self.get(&id)?.1;
109 if let Some(why) = &entry.refused {
110 return Err(not_ready(format!("refused: {why}")));
111 }
112 match &check.state {
113 ModelState::Ready => {}
114 ModelState::Degraded { reason, .. } => return Err(not_ready(format!("degraded: {reason}"))),
115 ModelState::Refused { reason } => return Err(not_ready(format!("refused: {reason}"))),
116 }
117 if first {
118 self.fully_verified
119 .lock()
120 .unwrap_or_else(|p| p.into_inner())
121 .insert(id.clone());
122 }
123 let body = entry.body()?;
124 let allowed = allowed_roots(&entry.root);
125 let real = |rel: &str| contained(&entry.root.join(rel), &allowed);
126 let shards = body
127 .shards
128 .iter()
129 .map(|s| real(&s.path))
130 .collect::<Result<Vec<_>, _>>()?;
131 let tokenizer = body.tokenizer_path.as_deref().map(real).transpose()?;
132 Ok(ResolvedModel {
133 package_id: id,
134 name: body.name.clone(),
135 format: body.format,
136 shards,
137 root: entry.root.clone(),
138 tokenizer,
139 body,
140 })
141 }
142}
143
144pub(crate) fn files_of_body(body: &ModelPackageBody) -> Vec<LocalFile> {
146 let mut v: Vec<LocalFile> = body
147 .shards
148 .iter()
149 .map(|s| LocalFile {
150 path: s.path.clone(),
151 role: FileRole::Shard,
152 size: s.size,
153 blake3: s.blake3.clone(),
154 verified: None,
155 })
156 .collect();
157 for (path, hash, role) in [
158 (&body.tokenizer_path, &body.tokenizer_blake3, FileRole::Tokenizer),
159 (&body.template_path, &body.template_blake3, FileRole::Template),
160 ] {
161 if let (Some(p), Some(h)) = (path, hash) {
162 v.push(LocalFile {
163 path: p.clone(),
164 role,
165 size: 0,
166 blake3: h.clone(),
167 verified: None,
168 });
169 }
170 }
171 v
172}
173
174pub(crate) fn consistency(id: &str, e: &ModelEntry) -> Result<(), String> {
178 let pid = e.envelope.package_id().map_err(|x| x.to_string())?;
179 if pid != id {
180 return Err("registry key is not the envelope's package id".into());
181 }
182 let body = e.body().map_err(|x| x.to_string())?;
183 body.validate().map_err(|x| x.to_string())?;
184 let want = files_of_body(&body);
185 if want.len() != e.files.len() {
186 return Err("file list differs from the attested body".into());
187 }
188 for (w, f) in want.iter().zip(&e.files) {
189 let size_ok = w.role != FileRole::Shard || w.size == f.size;
190 if w.path != f.path || w.role != f.role || w.blake3 != f.blake3 || !size_ok {
191 return Err(format!("file {} differs from the attested body", f.path));
192 }
193 }
194 Ok(())
195}