1pub trait FacetTerm: Copy + Eq + Sized + 'static {
21 fn all() -> &'static [Self];
23 fn as_str(self) -> &'static str;
25 fn from_term(s: &str) -> Option<Self>;
27 fn hazard() -> Self;
39}
40
41macro_rules! ordinal_term {
42 (
43 $(#[$meta:meta])*
44 $name:ident { $first:ident => $fs:literal $(, $rest:ident => $rs:literal)* $(,)? }
45 $(hazard = $hz:ident;)?
46 ) => {
47 $(#[$meta])*
48 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
49 pub enum $name {
50 #[default]
51 $first,
52 $($rest),*
53 }
54 impl FacetTerm for $name {
55 fn all() -> &'static [Self] { &[Self::$first $(, Self::$rest)*] }
56 fn as_str(self) -> &'static str {
57 match self { Self::$first => $fs $(, Self::$rest => $rs)* }
58 }
59 fn from_term(s: &str) -> Option<Self> {
60 match s { $fs => Some(Self::$first), $($rs => Some(Self::$rest),)* _ => None }
61 }
62 fn hazard() -> Self {
63 $( return Self::$hz; )?
67 #[allow(unreachable_code)]
68 { *Self::all().last().expect("a facet has at least one term") }
69 }
70 }
71 };
72}
73
74macro_rules! categorical_term {
75 (
76 $(#[$meta:meta])*
77 $name:ident { $first:ident => $fs:literal $(, $rest:ident => $rs:literal)* $(,)? }
78 hazard = $hz:ident;
79 ) => {
80 $(#[$meta])*
81 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
82 pub enum $name {
83 #[default]
84 $first,
85 $($rest),*
86 }
87 impl FacetTerm for $name {
88 fn all() -> &'static [Self] { &[Self::$first $(, Self::$rest)*] }
89 fn as_str(self) -> &'static str {
90 match self { Self::$first => $fs $(, Self::$rest => $rs)* }
91 }
92 fn from_term(s: &str) -> Option<Self> {
93 match s { $fs => Some(Self::$first), $($rs => Some(Self::$rest),)* _ => None }
94 }
95 fn hazard() -> Self { Self::$hz }
96 }
97 };
98}
99
100categorical_term! {
103 Operation {
105 Observe => "observe",
106 Create => "create",
107 Mutate => "mutate",
108 Destroy => "destroy",
109 Execute => "execute",
110 Communicate => "communicate",
111 Configure => "configure", Authorize => "authorize", Control => "control", }
115 hazard = Communicate;
116}
117
118ordinal_term! {
121 LocalLocus {
124 Process => "process",
125 Temp => "temp",
126 SandboxScope => "sandbox-scope",
127 Worktree => "worktree",
128 Adjacent => "adjacent", WorktreeTrusted => "worktree-trusted", User => "user", Machine => "machine", SystemIntegrity => "system-integrity", Device => "device", Kernel => "kernel", }
141}
142
143ordinal_term! {
144 RemoteReach {
146 None => "none",
147 Fixed => "fixed",
148 Arbitrary => "arbitrary",
149 }
150}
151
152categorical_term! {
153 RemoteBinding {
156 Na => "n/a", Pinned => "pinned", Ambient => "ambient",
159 }
160 hazard = Ambient;
161}
162
163ordinal_term! {
164 Provenance {
171 Na => "n/a", Established => "established", Literal => "literal", Opaque => "opaque", }
179}
180
181ordinal_term! {
182 Anchoring {
192 Literal => "literal", Anchored => "anchored", Opaque => "opaque", }
196}
197
198ordinal_term! {
199 Scale {
201 Single => "single",
202 Bounded => "bounded", Unbounded => "unbounded", }
205}
206
207ordinal_term! {
208 RetrievalGranularity {
218 Metadata => "metadata", Record => "record", BulkContent => "bulk-content", }
222}
223
224ordinal_term! {
225 Authority {
227 User => "user",
228 Elevated => "elevated", Root => "root",
230 OtherUser => "other-user", }
232}
233
234ordinal_term! {
235 Isolation {
238 None => "none",
239 View => "view", Namespace => "namespace",
241 Userns => "userns",
242 Vm => "vm",
243 Ocap => "ocap",
244 }
245 hazard = None;
246}
247
248ordinal_term! {
251 Reversibility {
254 None => "none", Trivial => "trivial", Recoverable => "recoverable",Effortful => "effortful", Irreversible => "irreversible",
259 }
260}
261
262ordinal_term! {
263 PersistenceLevel {
265 Transient => "transient",
266 Data => "data",
267 Reconfiguring => "reconfiguring", Installing => "installing", }
270}
271
272ordinal_term! {
273 TriggerEscape {
275 Immediate => "immediate", Detached => "detached", Recurring => "recurring", Boot => "boot", }
280}
281
282categorical_term! {
283 TriggerKind {
286 None => "none", Clock => "clock", Event => "event", }
290 hazard = Clock;
291}
292
293ordinal_term! {
296 DisclosureAudience {
299 None => "none",
300 LocalProcess => "local-process", LocalPersistent => "local-persistent", TrustedRemote => "trusted-remote",
303 SharedRemote => "shared-remote",
304 Public => "public",
305 }
306}
307
308ordinal_term! {
309 SecretLevel {
311 None => "none",
312 UsesAmbient => "uses-ambient",
313 Reads => "reads",
314 Writes => "writes",
315 Transmits => "transmits",
316 }
317}
318
319categorical_term! {
320 Channel {
323 None => "none",
324 Filesystem => "filesystem",
325 StdoutToModel => "stdout-to-model",
326 Network => "network",
327 Clipboard => "clipboard", Ipc => "ipc",
329 CredentialStore => "credential-store", CrossProcess => "cross-process", Unknown => "unknown",
332 }
333 hazard = Unknown;
334}
335
336categorical_term! {
337 Principal {
340 Own => "own",
341 Cross => "cross",
342 }
343 hazard = Cross;
344}
345
346ordinal_term! {
349 NetDirection {
351 None => "none",
352 Loopback => "loopback",
353 Outbound => "outbound",
354 InboundListen => "inbound-listen",
355 }
356}
357
358ordinal_term! {
359 NetDestination {
361 Na => "n/a",
362 Fixed => "fixed",
363 Arbitrary => "arbitrary",
364 }
365}
366
367ordinal_term! {
368 NetPayload {
370 None => "none",
371 Fetches => "fetches",
372 SendsHostData => "sends-host-data",
373 }
374}
375
376ordinal_term! {
379 ExecutionTrust {
382 None => "none",
383 SelfCode => "self",
384 CallerInline => "caller-inline",
385 CallerFile => "caller-file",
386 AmbientConfig => "ambient-config", NetworkSourced => "network-sourced",
388 }
389}
390
391categorical_term! {
392 SupplySource {
395 UnverifiedUrl => "unverified-url",
396 PublicRegistry => "public-registry",
397 SignedRepo => "signed-repo",
398 PrivateRegistry => "private-registry",
399 Vendored => "vendored",
400 }
401 hazard = UnverifiedUrl;
402}
403
404ordinal_term! {
405 Pinning {
408 Floating => "floating",
409 Version => "version",
410 HashVerified => "hash-verified",
411 Digest => "digest",
412 }
413 hazard = Floating;
414}
415
416categorical_term! {
417 ExecSurface {
421 None => "none",
422 InstallHook => "install-hook", BuildScript => "build-script", CallTime => "call-time", RunArtifact => "run-artifact", }
427 hazard = InstallHook;
428}
429
430ordinal_term! {
433 Cost {
435 None => "none",
436 LocalResource => "local-resource",
437 Metered => "metered", Quota => "quota",
439 }
440}
441
442#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
446pub struct Locus {
447 pub local: LocalLocus,
448 pub remote: RemoteReach,
449 pub binding: RemoteBinding,
450 pub provenance: Provenance,
451}
452
453#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
455pub struct Trigger {
456 pub escape: TriggerEscape,
457 pub kind: TriggerKind,
458}
459
460#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
462pub struct Persistence {
463 pub level: PersistenceLevel,
464 pub trigger: Trigger,
465}
466
467#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
469pub struct Disclosure {
470 pub audience: DisclosureAudience,
471 pub channel: Channel,
472 pub principal: Principal,
473}
474
475#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
477pub struct Secret {
478 pub level: SecretLevel,
479 pub channel: Channel,
480 pub principal: Principal,
481}
482
483#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
485pub struct Network {
486 pub direction: NetDirection,
487 pub destination: NetDestination,
488 pub payload: NetPayload,
489}
490
491#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
493pub struct SupplyChain {
494 pub source: SupplySource,
495 pub pinning: Pinning,
496 pub exec_surface: ExecSurface,
497}
498
499#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
504pub struct Execution {
505 pub trust: ExecutionTrust,
506 pub supply_chain: Option<SupplyChain>,
507}
508
509#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
514pub struct Capability {
515 pub operation: Operation,
516 pub locus: Locus,
517 pub scale: Scale,
518 pub retrieval: RetrievalGranularity,
519 pub authority: Authority,
520 pub isolation: Isolation,
521 pub reversibility: Reversibility,
522 pub persistence: Persistence,
523 pub disclosure: Disclosure,
524 pub secret: Secret,
525 pub network: Network,
526 pub execution: Execution,
527 pub cost: Cost,
528 pub because: String,
529}
530
531impl Capability {
532 pub fn new(operation: Operation) -> Self {
534 Self { operation, ..Self::default() }
535 }
536
537 pub fn set_facets(&self) -> Vec<(&'static str, &'static str)> {
543 let d = Self::default();
544 let mut out: Vec<(&'static str, &'static str)> = Vec::new();
545 macro_rules! push {
546 ($name:literal, $field:expr, $dflt:expr) => {
547 if $field != $dflt {
548 out.push(($name, $field.as_str()));
549 }
550 };
551 }
552 out.push(("operation", self.operation.as_str()));
554 push!("locus.local", self.locus.local, d.locus.local);
555 push!("locus.remote", self.locus.remote, d.locus.remote);
556 push!("locus.binding", self.locus.binding, d.locus.binding);
557 push!("locus.provenance", self.locus.provenance, d.locus.provenance);
558 push!("scale", self.scale, d.scale);
559 push!("retrieval", self.retrieval, d.retrieval);
560 push!("authority", self.authority, d.authority);
561 push!("isolation", self.isolation, d.isolation);
562 push!("reversibility", self.reversibility, d.reversibility);
563 push!("persistence.level", self.persistence.level, d.persistence.level);
564 push!("persistence.trigger.escape", self.persistence.trigger.escape, d.persistence.trigger.escape);
565 push!("persistence.trigger.kind", self.persistence.trigger.kind, d.persistence.trigger.kind);
566 push!("disclosure.audience", self.disclosure.audience, d.disclosure.audience);
567 push!("disclosure.channel", self.disclosure.channel, d.disclosure.channel);
568 push!("disclosure.principal", self.disclosure.principal, d.disclosure.principal);
569 push!("secret.level", self.secret.level, d.secret.level);
570 push!("secret.channel", self.secret.channel, d.secret.channel);
571 push!("secret.principal", self.secret.principal, d.secret.principal);
572 push!("network.direction", self.network.direction, d.network.direction);
573 push!("network.destination", self.network.destination, d.network.destination);
574 push!("network.payload", self.network.payload, d.network.payload);
575 push!("execution.trust", self.execution.trust, d.execution.trust);
576 push!("cost", self.cost, d.cost);
577 if let Some(sc) = self.execution.supply_chain {
578 out.push(("supply_chain.source", sc.source.as_str()));
579 out.push(("supply_chain.pinning", sc.pinning.as_str()));
580 out.push(("supply_chain.exec_surface", sc.exec_surface.as_str()));
581 }
582 out
583 }
584
585 pub fn worst(because: impl Into<String>) -> Self {
600 Self {
601 operation: Operation::hazard(),
602 locus: Locus {
603 local: LocalLocus::hazard(),
604 remote: RemoteReach::hazard(),
605 binding: RemoteBinding::hazard(),
606 provenance: Provenance::hazard(),
607 },
608 scale: Scale::hazard(),
609 retrieval: RetrievalGranularity::hazard(),
610 authority: Authority::hazard(),
611 isolation: Isolation::hazard(),
612 reversibility: Reversibility::hazard(),
613 persistence: Persistence {
614 level: PersistenceLevel::hazard(),
615 trigger: Trigger { escape: TriggerEscape::hazard(), kind: TriggerKind::hazard() },
616 },
617 disclosure: Disclosure {
618 audience: DisclosureAudience::hazard(),
619 channel: Channel::hazard(),
620 principal: Principal::hazard(),
621 },
622 secret: Secret {
623 level: SecretLevel::hazard(),
624 channel: Channel::hazard(),
625 principal: Principal::hazard(),
626 },
627 network: Network {
628 direction: NetDirection::hazard(),
629 destination: NetDestination::hazard(),
630 payload: NetPayload::hazard(),
631 },
632 execution: Execution {
633 trust: ExecutionTrust::hazard(),
634 supply_chain: Some(SupplyChain {
638 source: SupplySource::hazard(),
639 pinning: Pinning::hazard(),
640 exec_surface: ExecSurface::hazard(),
641 }),
642 },
643 cost: Cost::hazard(),
644 because: because.into(),
645 }
646 }
647}
648
649#[derive(Clone, Debug, Default, PartialEq, Eq)]
652pub struct Profile {
653 pub capabilities: Vec<Capability>,
654}
655
656impl Profile {
657 pub fn of(capabilities: Vec<Capability>) -> Self {
659 Self { capabilities }
660 }
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666
667 fn assert_term_strings_roundtrip<T: FacetTerm + std::fmt::Debug>() {
668 for &term in T::all() {
669 assert_eq!(
670 T::from_term(term.as_str()),
671 Some(term),
672 "term {:?} did not round-trip through {:?}",
673 term,
674 term.as_str(),
675 );
676 }
677 for (i, &a) in T::all().iter().enumerate() {
678 for &b in &T::all()[i + 1..] {
679 assert_ne!(a.as_str(), b.as_str(), "two variants share a TOML spelling");
680 }
681 }
682 assert_eq!(T::from_term("definitely-not-a-term"), None);
683 }
684
685 fn assert_zero_is_minimum<T: FacetTerm + Ord + Default + std::fmt::Debug>() {
686 let zero = T::all()[0];
687 assert_eq!(T::default(), zero, "Default must be the zero term (first variant)");
688 for &term in T::all() {
689 assert!(zero <= term, "zero term {zero:?} is not <= {term:?}");
690 }
691 }
692
693 #[test]
694 fn every_term_roundtrips_and_is_uniquely_spelled() {
695 assert_term_strings_roundtrip::<Operation>();
696 assert_term_strings_roundtrip::<LocalLocus>();
697 assert_term_strings_roundtrip::<RemoteReach>();
698 assert_term_strings_roundtrip::<RemoteBinding>();
699 assert_term_strings_roundtrip::<Provenance>();
700 assert_term_strings_roundtrip::<Anchoring>();
701 assert_term_strings_roundtrip::<Scale>();
702 assert_term_strings_roundtrip::<RetrievalGranularity>();
703 assert_term_strings_roundtrip::<Authority>();
704 assert_term_strings_roundtrip::<Isolation>();
705 assert_term_strings_roundtrip::<Reversibility>();
706 assert_term_strings_roundtrip::<PersistenceLevel>();
707 assert_term_strings_roundtrip::<TriggerEscape>();
708 assert_term_strings_roundtrip::<TriggerKind>();
709 assert_term_strings_roundtrip::<DisclosureAudience>();
710 assert_term_strings_roundtrip::<SecretLevel>();
711 assert_term_strings_roundtrip::<Channel>();
712 assert_term_strings_roundtrip::<Principal>();
713 assert_term_strings_roundtrip::<NetDirection>();
714 assert_term_strings_roundtrip::<NetDestination>();
715 assert_term_strings_roundtrip::<NetPayload>();
716 assert_term_strings_roundtrip::<ExecutionTrust>();
717 assert_term_strings_roundtrip::<SupplySource>();
718 assert_term_strings_roundtrip::<Pinning>();
719 assert_term_strings_roundtrip::<ExecSurface>();
720 assert_term_strings_roundtrip::<Cost>();
721 }
722
723 #[test]
724 fn ordinal_zero_terms_are_the_minimum() {
725 assert_zero_is_minimum::<LocalLocus>();
726 assert_zero_is_minimum::<RemoteReach>();
727 assert_zero_is_minimum::<Provenance>();
728 assert_zero_is_minimum::<Anchoring>();
729 assert_zero_is_minimum::<Scale>();
730 assert_zero_is_minimum::<RetrievalGranularity>();
731 assert_zero_is_minimum::<Authority>();
732 assert_zero_is_minimum::<Isolation>();
733 assert_zero_is_minimum::<Reversibility>();
734 assert_zero_is_minimum::<PersistenceLevel>();
735 assert_zero_is_minimum::<TriggerEscape>();
736 assert_zero_is_minimum::<DisclosureAudience>();
737 assert_zero_is_minimum::<SecretLevel>();
738 assert_zero_is_minimum::<NetDirection>();
739 assert_zero_is_minimum::<NetDestination>();
740 assert_zero_is_minimum::<NetPayload>();
741 assert_zero_is_minimum::<ExecutionTrust>();
742 assert_zero_is_minimum::<Pinning>();
743 assert_zero_is_minimum::<Cost>();
744 }
745
746 #[test]
747 fn ordinal_ladders_match_the_spec() {
748 assert!(LocalLocus::Process < LocalLocus::Worktree);
749 assert!(LocalLocus::Worktree < LocalLocus::Adjacent);
750 assert!(LocalLocus::Adjacent < LocalLocus::WorktreeTrusted);
751 assert!(LocalLocus::WorktreeTrusted < LocalLocus::User);
752 assert!(LocalLocus::Worktree < LocalLocus::Machine);
753 assert!(LocalLocus::Machine < LocalLocus::SystemIntegrity);
754 assert!(LocalLocus::SystemIntegrity < LocalLocus::Device);
755 assert!(LocalLocus::Device < LocalLocus::Kernel);
756 assert!(Scale::Single < Scale::Bounded && Scale::Bounded < Scale::Unbounded);
757 assert!(RetrievalGranularity::Metadata < RetrievalGranularity::Record);
758 assert!(RetrievalGranularity::Record < RetrievalGranularity::BulkContent);
759 assert!(Authority::User < Authority::Root && Authority::Root < Authority::OtherUser);
760 assert!(Reversibility::Recoverable < Reversibility::Irreversible);
761 assert!(PersistenceLevel::Data < PersistenceLevel::Installing);
762 assert!(TriggerEscape::Immediate < TriggerEscape::Boot);
763 assert!(DisclosureAudience::LocalProcess < DisclosureAudience::Public);
764 assert!(Provenance::Na < Provenance::Established);
765 assert!(Provenance::Established < Provenance::Literal);
766 assert!(Provenance::Literal < Provenance::Opaque);
767 assert!(SecretLevel::Reads < SecretLevel::Transmits);
768 assert!(ExecutionTrust::SelfCode < ExecutionTrust::NetworkSourced);
769 assert!(Pinning::Floating < Pinning::HashVerified);
770 }
771
772 #[test]
773 fn capability_new_leaves_all_other_facets_at_zero() {
774 let cap = Capability::new(Operation::Destroy);
775 assert_eq!(cap.operation, Operation::Destroy);
776 assert_eq!(cap.locus, Locus::default());
777 assert_eq!(cap.locus.local, LocalLocus::Process);
778 assert_eq!(cap.scale, Scale::Single);
779 assert_eq!(cap.retrieval, RetrievalGranularity::Metadata);
780 assert_eq!(cap.authority, Authority::User);
781 assert_eq!(cap.reversibility, Reversibility::None);
782 assert_eq!(cap.secret.level, SecretLevel::None);
783 assert_eq!(cap.disclosure.audience, DisclosureAudience::None);
784 assert_eq!(cap.network.direction, NetDirection::None);
785 assert_eq!(cap.execution.trust, ExecutionTrust::None);
786 assert!(cap.execution.supply_chain.is_none());
787 assert_eq!(cap.cost, Cost::None);
788 assert!(cap.because.is_empty());
789 }
790
791 #[test]
792 fn default_capability_is_a_zero_observe() {
793 assert_eq!(Capability::default().operation, Operation::Observe);
794 assert_eq!(Capability::default(), Capability::new(Operation::Observe));
795 }
796}