1use std::collections::HashMap;
14use std::fmt;
15use std::str::FromStr;
16
17use hatchling_core::CapabilityName;
18use sha2::{Digest, Sha256};
19
20use crate::{Admission, Error};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub struct CodeHash([u8; 32]);
29
30impl CodeHash {
31 #[must_use]
33 pub fn of(code: &[u8]) -> Self {
34 let mut hasher = Sha256::new();
35 hasher.update(code);
36 let result = hasher.finalize();
37 let mut bytes = [0u8; 32];
38 bytes.copy_from_slice(&result);
39 Self(bytes)
40 }
41
42 #[must_use]
44 pub const fn as_bytes(&self) -> &[u8; 32] {
45 &self.0
46 }
47}
48
49impl fmt::Display for CodeHash {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 for byte in self.0 {
52 write!(f, "{byte:02x}")?;
53 }
54 Ok(())
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64pub struct SkillId(String);
65
66impl SkillId {
67 #[must_use]
69 pub fn new(name: &str, version: &str) -> Self {
70 Self(format!("{name}@{version}"))
71 }
72
73 #[must_use]
75 pub fn as_str(&self) -> &str {
76 &self.0
77 }
78}
79
80impl FromStr for SkillId {
81 type Err = Error;
82
83 fn from_str(raw: &str) -> Result<Self, Self::Err> {
94 match raw.split_once('@') {
95 Some((name, version)) if !name.is_empty() && !version.is_empty() => {
96 Ok(Self::new(name, version))
97 }
98 _ => Err(Error::UnknownSkill(raw.to_owned())),
99 }
100 }
101}
102
103impl fmt::Display for SkillId {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "{}", self.0)
106 }
107}
108
109#[derive(Debug, Clone)]
115pub struct RegisteredSkill {
116 id: SkillId,
117 admission: Admission,
118 code_hash: CodeHash,
119}
120
121impl RegisteredSkill {
122 #[must_use]
124 pub const fn id(&self) -> &SkillId {
125 &self.id
126 }
127
128 #[must_use]
130 pub const fn admission(&self) -> &Admission {
131 &self.admission
132 }
133
134 #[must_use]
136 pub const fn code_hash(&self) -> &CodeHash {
137 &self.code_hash
138 }
139
140 pub fn check_call(&self, capability: &CapabilityName) -> Result<(), Error> {
152 self.admission.check_call(capability)
153 }
154}
155
156#[derive(Debug, Default)]
163pub struct SkillRegistry {
164 skills: HashMap<SkillId, RegisteredSkill>,
165}
166
167impl SkillRegistry {
168 #[must_use]
170 pub fn new() -> Self {
171 Self::default()
172 }
173
174 pub fn register(&mut self, admission: Admission, code: &[u8]) -> Result<SkillId, Error> {
188 let manifest = admission.manifest();
189 let id = SkillId::new(&manifest.name, &manifest.version);
190 let code_hash = CodeHash::of(code);
191
192 if self.skills.contains_key(&id) {
193 return Err(Error::AlreadyRegistered(id.to_string()));
194 }
195
196 self.skills.insert(
197 id.clone(),
198 RegisteredSkill {
199 id: id.clone(),
200 admission,
201 code_hash,
202 },
203 );
204 Ok(id)
205 }
206
207 pub fn unregister(&mut self, id: &SkillId) -> bool {
214 self.skills.remove(id).is_some()
215 }
216
217 #[must_use]
219 pub fn get(&self, id: &SkillId) -> Option<&RegisteredSkill> {
220 self.skills.get(id)
221 }
222
223 pub fn verify_code(&self, id: &SkillId, code: &[u8]) -> Result<(), Error> {
234 let skill = self
235 .skills
236 .get(id)
237 .ok_or_else(|| Error::UnknownSkill(id.to_string()))?;
238 let found = CodeHash::of(code);
239 if skill.code_hash != found {
240 return Err(Error::IdentityDrift {
241 id: id.to_string(),
242 expected: skill.code_hash.to_string(),
243 found: found.to_string(),
244 });
245 }
246 Ok(())
247 }
248
249 #[must_use]
251 pub fn list(&self) -> Vec<SkillId> {
252 self.skills.keys().cloned().collect()
253 }
254
255 #[must_use]
257 pub fn len(&self) -> usize {
258 self.skills.len()
259 }
260
261 #[must_use]
263 pub fn is_empty(&self) -> bool {
264 self.skills.is_empty()
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use crate::SignedManifest;
272 use ed25519_dalek::{Signer, SigningKey};
273
274 fn test_keypair() -> (SigningKey, [u8; 32]) {
275 let secret = [
277 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
278 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
279 0x1d, 0x1e, 0x1f, 0x20,
280 ];
281 let signing = SigningKey::from_bytes(&secret);
282 let verifying = signing.verifying_key().to_bytes();
283 (signing, verifying)
284 }
285
286 fn signed_manifest(signing: &SigningKey, name: &str) -> SignedManifest {
287 use crate::{Manifest, SignedManifest};
288 let manifest = Manifest {
289 name: name.to_string(),
290 version: "1.0.0".to_string(),
291 capabilities: vec!["fs.read".parse().unwrap()],
292 entry_point: "main.py".to_string(),
293 };
294 let canonical = manifest.canonical_bytes().unwrap();
295 let signature = signing.sign(&canonical);
296 SignedManifest {
297 manifest,
298 signature: Some(base64::Engine::encode(
299 &base64::engine::general_purpose::STANDARD,
300 signature.to_bytes(),
301 )),
302 }
303 }
304
305 fn admit(name: &str) -> Admission {
306 let (signing, verify_bytes) = test_keypair();
307 let signed = signed_manifest(&signing, name);
308 let grants: std::collections::HashSet<CapabilityName> =
309 std::iter::once("fs.read".parse().unwrap()).collect();
310 crate::verify_admission(&signed, &verify_bytes, &grants).expect("admit")
311 }
312
313 #[test]
314 fn registry_counts_and_lists_every_skill() {
315 let mut registry = SkillRegistry::new();
316 assert_eq!(registry.len(), 0);
317 assert!(registry.is_empty());
318 assert!(registry.list().is_empty());
319 registry
320 .register(admit("alpha"), b"a")
321 .expect("register alpha");
322 registry
323 .register(admit("beta"), b"b")
324 .expect("register beta");
325 assert_eq!(registry.len(), 2);
326 assert!(!registry.is_empty());
327 let mut ids: Vec<String> = registry.list().iter().map(ToString::to_string).collect();
328 ids.sort();
329 assert_eq!(ids, vec!["alpha@1.0.0", "beta@1.0.0"]);
330 }
331
332 #[test]
333 fn ids_and_hashes_display_canonically() {
334 assert_eq!(SkillId::new("web", "1.0.0").to_string(), "web@1.0.0");
335 assert_eq!(
337 CodeHash::of(b"").to_string(),
338 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
339 );
340 }
341
342 #[test]
343 fn register_and_lookup() {
344 let admission = admit("test-skill");
345 let mut registry = SkillRegistry::new();
346 let code = b"print('hello')";
347 let id = registry.register(admission, code).expect("register");
348 assert_eq!(id.as_str(), "test-skill@1.0.0");
349 let skill = registry.get(&id).expect("lookup");
350 assert_eq!(skill.code_hash(), &CodeHash::of(code));
351 }
352
353 #[test]
354 fn verify_code_detects_drift() {
355 let admission = admit("drift-skill");
356 let mut registry = SkillRegistry::new();
357 let id = registry.register(admission, b"v1 code").expect("register");
358 registry.verify_code(&id, b"v1 code").expect("same code");
360 let err = registry.verify_code(&id, b"v2 code").expect_err("drift");
362 assert!(matches!(err, Error::IdentityDrift { .. }));
363 }
364
365 #[test]
366 fn register_same_id_is_refused_even_with_same_code() {
367 let admission = admit("dup-skill");
368 let mut registry = SkillRegistry::new();
369 registry
370 .register(admission.clone(), b"code a")
371 .expect("first");
372 let err = registry
375 .register(admission.clone(), b"code a")
376 .expect_err("re-register with same code refused");
377 assert!(matches!(err, Error::AlreadyRegistered(_)));
378 let err = registry
380 .register(admission, b"code b")
381 .expect_err("re-register with different code refused");
382 assert!(matches!(err, Error::AlreadyRegistered(_)));
383 }
384
385 #[test]
386 fn unregister_then_register_replaces_the_skill() {
387 let admission = admit("update-skill");
388 let mut registry = SkillRegistry::new();
389 let id = registry
390 .register(admission.clone(), b"code a")
391 .expect("first");
392 assert!(registry.unregister(&id));
394 assert!(!registry.unregister(&id));
395 let id2 = registry
397 .register(admission, b"code b")
398 .expect("re-register after unregister");
399 assert_eq!(id, id2);
400 assert!(registry.verify_code(&id2, b"code b").is_ok());
401 }
402
403 #[test]
404 fn check_call_enforces_declared_capabilities() {
405 let admission = admit("call-skill");
406 let mut registry = SkillRegistry::new();
407 let id = registry.register(admission, b"code").expect("register");
408 let skill = registry.get(&id).expect("lookup");
409 skill
410 .check_call(&"fs.read".parse().unwrap())
411 .expect("declared");
412 let err = skill
413 .check_call(&"fs.write".parse().unwrap())
414 .expect_err("undeclared");
415 assert!(matches!(err, Error::UndeclaredCapability { .. }));
416 }
417
418 #[test]
419 fn skill_ids_parse_exactly_and_round_trip() {
420 let id: SkillId = "web@1.0.0".parse().unwrap();
421 assert_eq!(id, SkillId::new("web", "1.0.0"));
422 let id: SkillId = "a@1@2".parse().unwrap();
425 assert_eq!(id, SkillId::new("a", "1@2"));
426 assert_eq!(id.as_str(), "a@1@2");
427 for raw in ["web", "", "@", "@1.0.0", "web@"] {
428 assert!(
429 matches!(raw.parse::<SkillId>(), Err(Error::UnknownSkill(ref s)) if s == raw),
430 "{raw:?} must name no skill"
431 );
432 }
433 }
434
435 #[test]
436 fn verify_code_unknown_skill() {
437 let registry = SkillRegistry::new();
438 let id = SkillId::new("nope", "1.0.0");
439 let err = registry.verify_code(&id, b"code").expect_err("unknown");
440 assert!(matches!(err, Error::UnknownSkill(_)));
441 }
442}