1use std::collections::BTreeMap;
15use std::fs;
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18
19use super::frontmatter::{parse_frontmatter, split_frontmatter, SkillManifest};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub enum Layer {
26 Cli,
27 Env,
28 Project,
29 Manifest,
30 User,
31 Package,
32 System,
33 Host,
34}
35
36impl Layer {
37 pub fn label(self) -> &'static str {
38 match self {
39 Layer::Cli => "cli",
40 Layer::Env => "env",
41 Layer::Project => "project",
42 Layer::Manifest => "manifest",
43 Layer::User => "user",
44 Layer::Package => "package",
45 Layer::System => "system",
46 Layer::Host => "host",
47 }
48 }
49
50 pub fn from_label(label: &str) -> Option<Layer> {
51 match label {
52 "cli" => Some(Layer::Cli),
53 "env" => Some(Layer::Env),
54 "project" => Some(Layer::Project),
55 "manifest" => Some(Layer::Manifest),
56 "user" => Some(Layer::User),
57 "package" => Some(Layer::Package),
58 "system" => Some(Layer::System),
59 "host" => Some(Layer::Host),
60 _ => None,
61 }
62 }
63
64 pub const fn all() -> &'static [Layer] {
65 &[
66 Layer::Cli,
67 Layer::Env,
68 Layer::Project,
69 Layer::Manifest,
70 Layer::User,
71 Layer::Package,
72 Layer::System,
73 Layer::Host,
74 ]
75 }
76}
77
78#[derive(Debug, Clone)]
81pub struct Skill {
82 pub manifest: SkillManifest,
83 pub body: String,
87 pub skill_dir: Option<PathBuf>,
90 pub layer: Layer,
92 pub namespace: Option<String>,
94 pub unknown_fields: Vec<String>,
97}
98
99impl Skill {
100 pub fn id(&self) -> String {
104 match &self.namespace {
105 Some(ns) if !ns.is_empty() => format!("{ns}/{}", self.manifest.name),
106 _ => self.manifest.name.clone(),
107 }
108 }
109}
110
111pub trait SkillSource: Send + Sync {
114 fn list(&self) -> Vec<SkillManifestRef>;
117
118 fn fetch(&self, id: &str) -> Result<Skill, String>;
121
122 fn layer(&self) -> Layer;
124
125 fn describe(&self) -> String;
127}
128
129#[derive(Debug, Clone)]
132pub struct SkillManifestRef {
133 pub id: String,
134 pub manifest: SkillManifest,
135 pub layer: Layer,
136 pub namespace: Option<String>,
137 pub origin: String,
138 pub unknown_fields: Vec<String>,
139}
140
141const COMMAND_FRONTMATTER_FIELDS: &[&str] = &["hooks", "command", "run"];
142
143pub fn strip_untrusted_command_frontmatter(
146 entry: &mut BTreeMap<String, crate::value::VmValue>,
147) -> bool {
148 if !has_failed_provenance(entry) {
149 return false;
150 }
151 let mut stripped = false;
152 for key in COMMAND_FRONTMATTER_FIELDS {
153 stripped |= entry.remove(*key).is_some();
154 }
155 stripped
156}
157
158fn has_failed_provenance(entry: &BTreeMap<String, crate::value::VmValue>) -> bool {
159 let Some(provenance) = entry
160 .get("provenance")
161 .and_then(crate::value::VmValue::as_dict)
162 else {
163 return false;
164 };
165 let signed = matches!(
166 provenance.get("signed"),
167 Some(crate::value::VmValue::Bool(true))
168 );
169 let trusted = matches!(
170 provenance.get("trusted"),
171 Some(crate::value::VmValue::Bool(true))
172 );
173 let verified_status = match provenance.get("status") {
174 Some(crate::value::VmValue::String(status)) => &**status == "verified",
175 Some(_) => false,
176 None => signed && trusted,
177 };
178 !(signed && trusted && verified_status)
179}
180
181#[derive(Debug, Clone)]
188pub struct FsSkillSource {
189 pub root: PathBuf,
190 pub layer: Layer,
191 pub namespace: Option<String>,
196}
197
198impl FsSkillSource {
199 pub fn new(root: impl Into<PathBuf>, layer: Layer) -> Self {
200 Self {
201 root: root.into(),
202 layer,
203 namespace: None,
204 }
205 }
206
207 pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
208 let ns = namespace.into();
209 self.namespace = if ns.is_empty() { None } else { Some(ns) };
210 self
211 }
212
213 fn iter_skill_dirs(&self) -> Vec<PathBuf> {
214 let mut results = Vec::new();
215 if !self.root.is_dir() {
216 return results;
217 }
218 if self.root.join("SKILL.md").is_file() {
221 results.push(self.root.clone());
222 return results;
223 }
224 let Ok(entries) = fs::read_dir(&self.root) else {
226 return results;
227 };
228 for entry in entries.flatten() {
229 let path = entry.path();
230 if !path.is_dir() {
231 continue;
232 }
233 if path.join("SKILL.md").is_file() {
234 results.push(path);
235 }
236 }
237 results.sort();
238 results
239 }
240
241 fn finalize_manifest(
242 &self,
243 dir: &Path,
244 skill_file: &Path,
245 manifest: &mut SkillManifest,
246 ) -> Result<(), String> {
247 if manifest.name.is_empty() {
248 if let Some(name) = dir.file_name().and_then(|n| n.to_str()) {
249 manifest.name = name.to_string();
250 }
251 }
252 if manifest.name.is_empty() {
253 return Err(format!(
254 "{}: SKILL.md has no `name` field and directory has no basename",
255 skill_file.display()
256 ));
257 }
258 if manifest.short.trim().is_empty() {
259 return Err(format!(
260 "{}: SKILL.md requires a non-empty `short` field",
261 skill_file.display()
262 ));
263 }
264 Ok(())
265 }
266
267 fn load_manifest_from_dir(&self, dir: &Path) -> Result<SkillManifestRef, String> {
268 let skill_file = dir.join("SKILL.md");
269 let source = fs::read_to_string(&skill_file)
270 .map_err(|e| format!("failed to read {}: {e}", skill_file.display()))?;
271 let (fm, _) = split_frontmatter(&source);
272 let parsed = parse_frontmatter(fm).map_err(|e| format!("{}: {e}", skill_file.display()))?;
273 let mut manifest = parsed.manifest;
274 self.finalize_manifest(dir, &skill_file, &mut manifest)?;
275 let id = match &self.namespace {
276 Some(ns) if !ns.is_empty() => format!("{ns}/{}", manifest.name),
277 _ => manifest.name.clone(),
278 };
279 Ok(SkillManifestRef {
280 id,
281 manifest,
282 layer: self.layer,
283 namespace: self.namespace.clone(),
284 origin: dir.display().to_string(),
285 unknown_fields: parsed.unknown_fields,
286 })
287 }
288
289 fn load_from_dir(&self, dir: &Path) -> Result<Skill, String> {
290 let skill_file = dir.join("SKILL.md");
291 let source = fs::read_to_string(&skill_file)
292 .map_err(|e| format!("failed to read {}: {e}", skill_file.display()))?;
293 let (fm, body) = split_frontmatter(&source);
294 let parsed = parse_frontmatter(fm).map_err(|e| format!("{}: {e}", skill_file.display()))?;
295 let mut manifest = parsed.manifest;
296 self.finalize_manifest(dir, &skill_file, &mut manifest)?;
297 let skill = Skill {
298 body: body.to_string(),
299 skill_dir: Some(dir.to_path_buf()),
300 layer: self.layer,
301 namespace: self.namespace.clone(),
302 unknown_fields: parsed.unknown_fields,
303 manifest,
304 };
305 Ok(skill)
306 }
307}
308
309impl SkillSource for FsSkillSource {
310 fn list(&self) -> Vec<SkillManifestRef> {
311 let mut out = Vec::new();
312 for dir in self.iter_skill_dirs() {
313 match self.load_manifest_from_dir(&dir) {
314 Ok(skill) => {
315 out.push(skill);
316 }
317 Err(err) => {
318 eprintln!("warning: skills: {err}");
319 }
320 }
321 }
322 out
323 }
324
325 fn fetch(&self, id: &str) -> Result<Skill, String> {
326 for dir in self.iter_skill_dirs() {
327 let skill = self.load_from_dir(&dir)?;
328 if skill.id() == id || (self.namespace.is_none() && skill.manifest.name == id) {
329 return Ok(skill);
330 }
331 }
332 Err(format!(
333 "skill '{id}' not found under {}",
334 self.root.display()
335 ))
336 }
337
338 fn layer(&self) -> Layer {
339 self.layer
340 }
341
342 fn describe(&self) -> String {
343 match &self.namespace {
344 Some(ns) => format!("{} [{}] ns={ns}", self.root.display(), self.layer.label()),
345 None => format!("{} [{}]", self.root.display(), self.layer.label()),
346 }
347 }
348}
349
350pub type HostSkillLister = Arc<dyn Fn() -> Vec<SkillManifestRef> + Send + Sync>;
353
354pub type HostSkillFetcher = Arc<dyn Fn(&str) -> Result<Skill, String> + Send + Sync>;
357
358pub struct HostSkillSource {
362 loader: HostSkillLister,
363 fetcher: HostSkillFetcher,
364}
365
366impl HostSkillSource {
367 pub fn new<L, F>(loader: L, fetcher: F) -> Self
368 where
369 L: Fn() -> Vec<SkillManifestRef> + Send + Sync + 'static,
370 F: Fn(&str) -> Result<Skill, String> + Send + Sync + 'static,
371 {
372 Self {
373 loader: Arc::new(loader),
374 fetcher: Arc::new(fetcher),
375 }
376 }
377}
378
379impl std::fmt::Debug for HostSkillSource {
380 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381 f.debug_struct("HostSkillSource").finish_non_exhaustive()
382 }
383}
384
385impl SkillSource for HostSkillSource {
386 fn list(&self) -> Vec<SkillManifestRef> {
387 (self.loader)()
388 }
389
390 fn fetch(&self, id: &str) -> Result<Skill, String> {
391 (self.fetcher)(id)
392 }
393
394 fn layer(&self) -> Layer {
395 Layer::Host
396 }
397
398 fn describe(&self) -> String {
399 "host-provided [host]".to_string()
400 }
401}
402
403pub fn skill_entry_to_vm(skill: &Skill) -> crate::value::VmValue {
407 use crate::value::VmValue;
408
409 let mut entry: BTreeMap<String, VmValue> = BTreeMap::new();
410 entry.insert(
411 "name".to_string(),
412 VmValue::String(std::sync::Arc::from(skill.manifest.name.as_str())),
413 );
414 entry.insert(
415 "short".to_string(),
416 VmValue::String(std::sync::Arc::from(skill.manifest.short.as_str())),
417 );
418 entry.insert(
419 "description".to_string(),
420 VmValue::String(std::sync::Arc::from(
421 if skill.manifest.description.is_empty() {
422 skill.manifest.short.as_str()
423 } else {
424 skill.manifest.description.as_str()
425 },
426 )),
427 );
428 if let Some(when) = &skill.manifest.when_to_use {
429 entry.insert(
430 "when_to_use".to_string(),
431 VmValue::String(std::sync::Arc::from(when.as_str())),
432 );
433 }
434 if skill.manifest.disable_model_invocation {
435 entry.insert("disable_model_invocation".to_string(), VmValue::Bool(true));
436 }
437 if !skill.manifest.allowed_tools.is_empty() {
438 entry.insert(
439 "allowed_tools".to_string(),
440 VmValue::List(std::sync::Arc::new(
441 skill
442 .manifest
443 .allowed_tools
444 .iter()
445 .map(|t| VmValue::String(std::sync::Arc::from(t.as_str())))
446 .collect(),
447 )),
448 );
449 }
450 if skill.manifest.user_invocable {
451 entry.insert("user_invocable".to_string(), VmValue::Bool(true));
452 }
453 if !skill.manifest.paths.is_empty() {
454 entry.insert(
455 "paths".to_string(),
456 VmValue::List(std::sync::Arc::new(
457 skill
458 .manifest
459 .paths
460 .iter()
461 .map(|p| VmValue::String(std::sync::Arc::from(p.as_str())))
462 .collect(),
463 )),
464 );
465 }
466 if let Some(context) = &skill.manifest.context {
467 entry.insert(
468 "context".to_string(),
469 VmValue::String(std::sync::Arc::from(context.as_str())),
470 );
471 }
472 if let Some(agent) = &skill.manifest.agent {
473 entry.insert(
474 "agent".to_string(),
475 VmValue::String(std::sync::Arc::from(agent.as_str())),
476 );
477 }
478 if !skill.manifest.hooks.is_empty() {
479 let mut hooks: BTreeMap<String, VmValue> = BTreeMap::new();
480 for (k, v) in &skill.manifest.hooks {
481 hooks.insert(k.clone(), VmValue::String(std::sync::Arc::from(v.as_str())));
482 }
483 entry.insert(
484 "hooks".to_string(),
485 VmValue::Dict(std::sync::Arc::new(hooks)),
486 );
487 }
488 if let Some(model) = &skill.manifest.model {
489 entry.insert(
490 "model".to_string(),
491 VmValue::String(std::sync::Arc::from(model.as_str())),
492 );
493 }
494 if let Some(effort) = &skill.manifest.effort {
495 entry.insert(
496 "effort".to_string(),
497 VmValue::String(std::sync::Arc::from(effort.as_str())),
498 );
499 }
500 if skill.manifest.require_signature {
501 entry.insert("require_signature".to_string(), VmValue::Bool(true));
502 }
503 if !skill.manifest.trusted_signers.is_empty() {
504 entry.insert(
505 "trusted_signers".to_string(),
506 VmValue::List(std::sync::Arc::new(
507 skill
508 .manifest
509 .trusted_signers
510 .iter()
511 .map(|fingerprint| VmValue::String(std::sync::Arc::from(fingerprint.as_str())))
512 .collect(),
513 )),
514 );
515 }
516 if let Some(shell) = &skill.manifest.shell {
517 entry.insert(
518 "shell".to_string(),
519 VmValue::String(std::sync::Arc::from(shell.as_str())),
520 );
521 }
522 if let Some(hint) = &skill.manifest.argument_hint {
523 entry.insert(
524 "argument_hint".to_string(),
525 VmValue::String(std::sync::Arc::from(hint.as_str())),
526 );
527 }
528 entry.insert(
529 "body".to_string(),
530 VmValue::String(std::sync::Arc::from(skill.body.as_str())),
531 );
532 if let Some(dir) = &skill.skill_dir {
533 entry.insert(
534 "skill_dir".to_string(),
535 VmValue::String(std::sync::Arc::from(dir.display().to_string())),
536 );
537 }
538 entry.insert(
539 "source".to_string(),
540 VmValue::String(std::sync::Arc::from(skill.layer.label())),
541 );
542 if let Some(ns) = &skill.namespace {
543 entry.insert(
544 "namespace".to_string(),
545 VmValue::String(std::sync::Arc::from(ns.as_str())),
546 );
547 }
548 VmValue::Dict(std::sync::Arc::new(entry))
549}
550
551pub fn skill_manifest_ref_to_vm(skill: &SkillManifestRef) -> crate::value::VmValue {
552 use crate::value::VmValue;
553
554 let mut entry: BTreeMap<String, VmValue> = BTreeMap::new();
555 entry.insert(
556 "name".to_string(),
557 VmValue::String(std::sync::Arc::from(skill.manifest.name.as_str())),
558 );
559 entry.insert(
560 "short".to_string(),
561 VmValue::String(std::sync::Arc::from(skill.manifest.short.as_str())),
562 );
563 entry.insert(
564 "description".to_string(),
565 VmValue::String(std::sync::Arc::from(
566 if skill.manifest.description.is_empty() {
567 skill.manifest.short.as_str()
568 } else {
569 skill.manifest.description.as_str()
570 },
571 )),
572 );
573 if let Some(when) = &skill.manifest.when_to_use {
574 entry.insert(
575 "when_to_use".to_string(),
576 VmValue::String(std::sync::Arc::from(when.as_str())),
577 );
578 }
579 if skill.manifest.disable_model_invocation {
580 entry.insert("disable_model_invocation".to_string(), VmValue::Bool(true));
581 }
582 if !skill.manifest.allowed_tools.is_empty() {
583 entry.insert(
584 "allowed_tools".to_string(),
585 VmValue::List(std::sync::Arc::new(
586 skill
587 .manifest
588 .allowed_tools
589 .iter()
590 .map(|tool| VmValue::String(std::sync::Arc::from(tool.as_str())))
591 .collect(),
592 )),
593 );
594 }
595 if skill.manifest.user_invocable {
596 entry.insert("user_invocable".to_string(), VmValue::Bool(true));
597 }
598 if !skill.manifest.paths.is_empty() {
599 entry.insert(
600 "paths".to_string(),
601 VmValue::List(std::sync::Arc::new(
602 skill
603 .manifest
604 .paths
605 .iter()
606 .map(|path| VmValue::String(std::sync::Arc::from(path.as_str())))
607 .collect(),
608 )),
609 );
610 }
611 if let Some(context) = &skill.manifest.context {
612 entry.insert(
613 "context".to_string(),
614 VmValue::String(std::sync::Arc::from(context.as_str())),
615 );
616 }
617 if let Some(agent) = &skill.manifest.agent {
618 entry.insert(
619 "agent".to_string(),
620 VmValue::String(std::sync::Arc::from(agent.as_str())),
621 );
622 }
623 if !skill.manifest.hooks.is_empty() {
624 let mut hooks: BTreeMap<String, VmValue> = BTreeMap::new();
625 for (key, value) in &skill.manifest.hooks {
626 hooks.insert(
627 key.clone(),
628 VmValue::String(std::sync::Arc::from(value.as_str())),
629 );
630 }
631 entry.insert(
632 "hooks".to_string(),
633 VmValue::Dict(std::sync::Arc::new(hooks)),
634 );
635 }
636 if let Some(model) = &skill.manifest.model {
637 entry.insert(
638 "model".to_string(),
639 VmValue::String(std::sync::Arc::from(model.as_str())),
640 );
641 }
642 if let Some(effort) = &skill.manifest.effort {
643 entry.insert(
644 "effort".to_string(),
645 VmValue::String(std::sync::Arc::from(effort.as_str())),
646 );
647 }
648 if let Some(shell) = &skill.manifest.shell {
649 entry.insert(
650 "shell".to_string(),
651 VmValue::String(std::sync::Arc::from(shell.as_str())),
652 );
653 }
654 if let Some(hint) = &skill.manifest.argument_hint {
655 entry.insert(
656 "argument_hint".to_string(),
657 VmValue::String(std::sync::Arc::from(hint.as_str())),
658 );
659 }
660 entry.insert(
661 "source".to_string(),
662 VmValue::String(std::sync::Arc::from(skill.layer.label())),
663 );
664 if let Some(ns) = &skill.namespace {
665 entry.insert(
666 "namespace".to_string(),
667 VmValue::String(std::sync::Arc::from(ns.as_str())),
668 );
669 }
670 VmValue::Dict(std::sync::Arc::new(entry))
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use std::fs;
677
678 fn write(tmp: &Path, rel: &str, body: &str) {
679 let p = tmp.join(rel);
680 fs::create_dir_all(p.parent().unwrap()).unwrap();
681 fs::write(p, body).unwrap();
682 }
683
684 #[test]
685 fn fs_source_walks_one_level_deep() {
686 let tmp = tempfile::tempdir().unwrap();
687 write(
688 tmp.path(),
689 "deploy/SKILL.md",
690 "---\nname: deploy\nshort: deploy the service\ndescription: ship it\n---\nrun deploy",
691 );
692 write(
693 tmp.path(),
694 "review/SKILL.md",
695 "---\nname: review\nshort: review a pull request\n---\nbody",
696 );
697 write(tmp.path(), "not-a-skill.txt", "no");
698
699 let src = FsSkillSource::new(tmp.path(), Layer::Project);
700 let listed = src.list();
701 assert_eq!(listed.len(), 2);
702 let names: Vec<_> = listed.iter().map(|s| s.manifest.name.clone()).collect();
703 assert!(names.contains(&"deploy".to_string()));
704 assert!(names.contains(&"review".to_string()));
705
706 let skill = src.fetch("deploy").unwrap();
707 assert_eq!(skill.manifest.short, "deploy the service");
708 assert_eq!(skill.manifest.description, "ship it");
709 assert_eq!(skill.body, "run deploy");
710 }
711
712 #[test]
713 fn fs_source_accepts_root_as_single_skill() {
714 let tmp = tempfile::tempdir().unwrap();
715 write(
716 tmp.path(),
717 "SKILL.md",
718 "---\nname: solo\nshort: single skill bundle\n---\n(body)",
719 );
720 let src = FsSkillSource::new(tmp.path(), Layer::Cli);
721 let listed = src.list();
722 assert_eq!(listed.len(), 1);
723 assert_eq!(listed[0].manifest.name, "solo");
724 }
725
726 #[test]
727 fn fs_source_defaults_name_to_directory() {
728 let tmp = tempfile::tempdir().unwrap();
729 write(
730 tmp.path(),
731 "nameless/SKILL.md",
732 "---\nshort: fallback to the directory name\n---\nbody only",
733 );
734 let src = FsSkillSource::new(tmp.path(), Layer::User);
735 let skill = src.fetch("nameless").unwrap();
736 assert_eq!(skill.manifest.name, "nameless");
737 }
738
739 #[test]
740 fn fs_source_namespace_prefixes_id() {
741 let tmp = tempfile::tempdir().unwrap();
742 write(
743 tmp.path(),
744 "deploy/SKILL.md",
745 "---\nname: deploy\nshort: deploy the service\n---\nbody",
746 );
747 let src = FsSkillSource::new(tmp.path(), Layer::Manifest).with_namespace("acme/ops");
748 let listed = src.list();
749 assert_eq!(listed[0].id, "acme/ops/deploy");
750 let skill = src.fetch("acme/ops/deploy").unwrap();
751 assert_eq!(skill.id(), "acme/ops/deploy");
752 }
753
754 #[test]
755 fn fs_source_namespaced_fetch_requires_qualified_id() {
756 let tmp = tempfile::tempdir().unwrap();
757 write(
758 tmp.path(),
759 "deploy/SKILL.md",
760 "---\nname: deploy\nshort: deploy the service\n---\nbody",
761 );
762 let src = FsSkillSource::new(tmp.path(), Layer::Manifest).with_namespace("acme/ops");
763
764 assert!(src.fetch("deploy").is_err());
765 assert!(src.fetch("other/deploy").is_err());
766 assert_eq!(
767 src.fetch("acme/ops/deploy").unwrap().id(),
768 "acme/ops/deploy"
769 );
770 }
771
772 #[test]
773 fn fs_source_missing_root_is_empty_not_error() {
774 let src = FsSkillSource::new("/does/not/exist/anywhere", Layer::System);
775 assert!(src.list().is_empty());
776 assert!(src.fetch("nope").is_err());
777 }
778
779 #[test]
780 fn fs_source_requires_short_card() {
781 let tmp = tempfile::tempdir().unwrap();
782 write(
783 tmp.path(),
784 "broken/SKILL.md",
785 "---\nname: broken\n---\nbody",
786 );
787 let src = FsSkillSource::new(tmp.path(), Layer::Project);
788 assert!(src.list().is_empty());
789 let err = src.fetch("broken").unwrap_err();
790 assert!(err.contains("`short`"), "{err}");
791 }
792
793 #[test]
794 fn host_source_wraps_closures() {
795 let host = HostSkillSource::new(
796 || {
797 vec![SkillManifestRef {
798 id: "h1".into(),
799 manifest: SkillManifest {
800 name: "h1".into(),
801 short: "host-provided skill".into(),
802 ..Default::default()
803 },
804 layer: Layer::Host,
805 namespace: None,
806 origin: "host".into(),
807 unknown_fields: Vec::new(),
808 }]
809 },
810 |id| {
811 Ok(Skill {
812 manifest: SkillManifest {
813 name: id.to_string(),
814 short: "host-provided skill".into(),
815 ..Default::default()
816 },
817 body: "host body".into(),
818 skill_dir: None,
819 layer: Layer::Host,
820 namespace: None,
821 unknown_fields: Vec::new(),
822 })
823 },
824 );
825 assert_eq!(host.list().len(), 1);
826 let s = host.fetch("h1").unwrap();
827 assert_eq!(s.body, "host body");
828 assert_eq!(s.layer, Layer::Host);
829 }
830
831 #[test]
832 fn layer_label_roundtrips() {
833 for layer in Layer::all() {
834 assert_eq!(Layer::from_label(layer.label()), Some(*layer));
835 }
836 }
837}