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 Scale {
184 Single => "single",
185 Bounded => "bounded", Unbounded => "unbounded", }
188}
189
190ordinal_term! {
191 RetrievalGranularity {
201 Metadata => "metadata", Record => "record", BulkContent => "bulk-content", }
205}
206
207ordinal_term! {
208 Authority {
210 User => "user",
211 Elevated => "elevated", Root => "root",
213 OtherUser => "other-user", }
215}
216
217ordinal_term! {
218 Isolation {
221 None => "none",
222 View => "view", Namespace => "namespace",
224 Userns => "userns",
225 Vm => "vm",
226 Ocap => "ocap",
227 }
228 hazard = None;
229}
230
231ordinal_term! {
234 Reversibility {
237 None => "none", Trivial => "trivial", Recoverable => "recoverable",Effortful => "effortful", Irreversible => "irreversible",
242 }
243}
244
245ordinal_term! {
246 PersistenceLevel {
248 Transient => "transient",
249 Data => "data",
250 Reconfiguring => "reconfiguring", Installing => "installing", }
253}
254
255ordinal_term! {
256 TriggerEscape {
258 Immediate => "immediate", Detached => "detached", Recurring => "recurring", Boot => "boot", }
263}
264
265categorical_term! {
266 TriggerKind {
269 None => "none", Clock => "clock", Event => "event", }
273 hazard = Clock;
274}
275
276ordinal_term! {
279 DisclosureAudience {
282 None => "none",
283 LocalProcess => "local-process", LocalPersistent => "local-persistent", TrustedRemote => "trusted-remote",
286 SharedRemote => "shared-remote",
287 Public => "public",
288 }
289}
290
291ordinal_term! {
292 SecretLevel {
294 None => "none",
295 UsesAmbient => "uses-ambient",
296 Reads => "reads",
297 Writes => "writes",
298 Transmits => "transmits",
299 }
300}
301
302categorical_term! {
303 Channel {
306 None => "none",
307 Filesystem => "filesystem",
308 StdoutToModel => "stdout-to-model",
309 Network => "network",
310 Clipboard => "clipboard", Ipc => "ipc",
312 CredentialStore => "credential-store", CrossProcess => "cross-process", Unknown => "unknown",
315 }
316 hazard = Unknown;
317}
318
319categorical_term! {
320 Principal {
323 Own => "own",
324 Cross => "cross",
325 }
326 hazard = Cross;
327}
328
329ordinal_term! {
332 NetDirection {
334 None => "none",
335 Loopback => "loopback",
336 Outbound => "outbound",
337 InboundListen => "inbound-listen",
338 }
339}
340
341ordinal_term! {
342 NetDestination {
344 Na => "n/a",
345 Fixed => "fixed",
346 Arbitrary => "arbitrary",
347 }
348}
349
350ordinal_term! {
351 NetPayload {
353 None => "none",
354 Fetches => "fetches",
355 SendsHostData => "sends-host-data",
356 }
357}
358
359ordinal_term! {
362 ExecutionTrust {
365 None => "none",
366 SelfCode => "self",
367 CallerInline => "caller-inline",
368 CallerFile => "caller-file",
369 AmbientConfig => "ambient-config", NetworkSourced => "network-sourced",
371 }
372}
373
374categorical_term! {
375 SupplySource {
378 UnverifiedUrl => "unverified-url",
379 PublicRegistry => "public-registry",
380 SignedRepo => "signed-repo",
381 PrivateRegistry => "private-registry",
382 Vendored => "vendored",
383 }
384 hazard = UnverifiedUrl;
385}
386
387ordinal_term! {
388 Pinning {
391 Floating => "floating",
392 Version => "version",
393 HashVerified => "hash-verified",
394 Digest => "digest",
395 }
396 hazard = Floating;
397}
398
399categorical_term! {
400 ExecSurface {
404 None => "none",
405 InstallHook => "install-hook", BuildScript => "build-script", CallTime => "call-time", RunArtifact => "run-artifact", }
410 hazard = InstallHook;
411}
412
413ordinal_term! {
416 Cost {
418 None => "none",
419 LocalResource => "local-resource",
420 Metered => "metered", Quota => "quota",
422 }
423}
424
425#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
429pub struct Locus {
430 pub local: LocalLocus,
431 pub remote: RemoteReach,
432 pub binding: RemoteBinding,
433 pub provenance: Provenance,
434}
435
436#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
438pub struct Trigger {
439 pub escape: TriggerEscape,
440 pub kind: TriggerKind,
441}
442
443#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
445pub struct Persistence {
446 pub level: PersistenceLevel,
447 pub trigger: Trigger,
448}
449
450#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
452pub struct Disclosure {
453 pub audience: DisclosureAudience,
454 pub channel: Channel,
455 pub principal: Principal,
456}
457
458#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
460pub struct Secret {
461 pub level: SecretLevel,
462 pub channel: Channel,
463 pub principal: Principal,
464}
465
466#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
468pub struct Network {
469 pub direction: NetDirection,
470 pub destination: NetDestination,
471 pub payload: NetPayload,
472}
473
474#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
476pub struct SupplyChain {
477 pub source: SupplySource,
478 pub pinning: Pinning,
479 pub exec_surface: ExecSurface,
480}
481
482#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
487pub struct Execution {
488 pub trust: ExecutionTrust,
489 pub supply_chain: Option<SupplyChain>,
490}
491
492#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
497pub struct Capability {
498 pub operation: Operation,
499 pub locus: Locus,
500 pub scale: Scale,
501 pub retrieval: RetrievalGranularity,
502 pub authority: Authority,
503 pub isolation: Isolation,
504 pub reversibility: Reversibility,
505 pub persistence: Persistence,
506 pub disclosure: Disclosure,
507 pub secret: Secret,
508 pub network: Network,
509 pub execution: Execution,
510 pub cost: Cost,
511 pub because: String,
512}
513
514impl Capability {
515 pub fn new(operation: Operation) -> Self {
517 Self { operation, ..Self::default() }
518 }
519
520 pub fn set_facets(&self) -> Vec<(&'static str, &'static str)> {
526 let d = Self::default();
527 let mut out: Vec<(&'static str, &'static str)> = Vec::new();
528 macro_rules! push {
529 ($name:literal, $field:expr, $dflt:expr) => {
530 if $field != $dflt {
531 out.push(($name, $field.as_str()));
532 }
533 };
534 }
535 out.push(("operation", self.operation.as_str()));
537 push!("locus.local", self.locus.local, d.locus.local);
538 push!("locus.remote", self.locus.remote, d.locus.remote);
539 push!("locus.binding", self.locus.binding, d.locus.binding);
540 push!("locus.provenance", self.locus.provenance, d.locus.provenance);
541 push!("scale", self.scale, d.scale);
542 push!("retrieval", self.retrieval, d.retrieval);
543 push!("authority", self.authority, d.authority);
544 push!("isolation", self.isolation, d.isolation);
545 push!("reversibility", self.reversibility, d.reversibility);
546 push!("persistence.level", self.persistence.level, d.persistence.level);
547 push!("persistence.trigger.escape", self.persistence.trigger.escape, d.persistence.trigger.escape);
548 push!("persistence.trigger.kind", self.persistence.trigger.kind, d.persistence.trigger.kind);
549 push!("disclosure.audience", self.disclosure.audience, d.disclosure.audience);
550 push!("disclosure.channel", self.disclosure.channel, d.disclosure.channel);
551 push!("disclosure.principal", self.disclosure.principal, d.disclosure.principal);
552 push!("secret.level", self.secret.level, d.secret.level);
553 push!("secret.channel", self.secret.channel, d.secret.channel);
554 push!("secret.principal", self.secret.principal, d.secret.principal);
555 push!("network.direction", self.network.direction, d.network.direction);
556 push!("network.destination", self.network.destination, d.network.destination);
557 push!("network.payload", self.network.payload, d.network.payload);
558 push!("execution.trust", self.execution.trust, d.execution.trust);
559 push!("cost", self.cost, d.cost);
560 if let Some(sc) = self.execution.supply_chain {
561 out.push(("supply_chain.source", sc.source.as_str()));
562 out.push(("supply_chain.pinning", sc.pinning.as_str()));
563 out.push(("supply_chain.exec_surface", sc.exec_surface.as_str()));
564 }
565 out
566 }
567
568 pub fn worst(because: impl Into<String>) -> Self {
583 Self {
584 operation: Operation::hazard(),
585 locus: Locus {
586 local: LocalLocus::hazard(),
587 remote: RemoteReach::hazard(),
588 binding: RemoteBinding::hazard(),
589 provenance: Provenance::hazard(),
590 },
591 scale: Scale::hazard(),
592 retrieval: RetrievalGranularity::hazard(),
593 authority: Authority::hazard(),
594 isolation: Isolation::hazard(),
595 reversibility: Reversibility::hazard(),
596 persistence: Persistence {
597 level: PersistenceLevel::hazard(),
598 trigger: Trigger { escape: TriggerEscape::hazard(), kind: TriggerKind::hazard() },
599 },
600 disclosure: Disclosure {
601 audience: DisclosureAudience::hazard(),
602 channel: Channel::hazard(),
603 principal: Principal::hazard(),
604 },
605 secret: Secret {
606 level: SecretLevel::hazard(),
607 channel: Channel::hazard(),
608 principal: Principal::hazard(),
609 },
610 network: Network {
611 direction: NetDirection::hazard(),
612 destination: NetDestination::hazard(),
613 payload: NetPayload::hazard(),
614 },
615 execution: Execution {
616 trust: ExecutionTrust::hazard(),
617 supply_chain: Some(SupplyChain {
621 source: SupplySource::hazard(),
622 pinning: Pinning::hazard(),
623 exec_surface: ExecSurface::hazard(),
624 }),
625 },
626 cost: Cost::hazard(),
627 because: because.into(),
628 }
629 }
630}
631
632#[derive(Clone, Debug, Default, PartialEq, Eq)]
635pub struct Profile {
636 pub capabilities: Vec<Capability>,
637}
638
639impl Profile {
640 pub fn of(capabilities: Vec<Capability>) -> Self {
642 Self { capabilities }
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649
650 fn assert_term_strings_roundtrip<T: FacetTerm + std::fmt::Debug>() {
651 for &term in T::all() {
652 assert_eq!(
653 T::from_term(term.as_str()),
654 Some(term),
655 "term {:?} did not round-trip through {:?}",
656 term,
657 term.as_str(),
658 );
659 }
660 for (i, &a) in T::all().iter().enumerate() {
661 for &b in &T::all()[i + 1..] {
662 assert_ne!(a.as_str(), b.as_str(), "two variants share a TOML spelling");
663 }
664 }
665 assert_eq!(T::from_term("definitely-not-a-term"), None);
666 }
667
668 fn assert_zero_is_minimum<T: FacetTerm + Ord + Default + std::fmt::Debug>() {
669 let zero = T::all()[0];
670 assert_eq!(T::default(), zero, "Default must be the zero term (first variant)");
671 for &term in T::all() {
672 assert!(zero <= term, "zero term {zero:?} is not <= {term:?}");
673 }
674 }
675
676 #[test]
677 fn every_term_roundtrips_and_is_uniquely_spelled() {
678 assert_term_strings_roundtrip::<Operation>();
679 assert_term_strings_roundtrip::<LocalLocus>();
680 assert_term_strings_roundtrip::<RemoteReach>();
681 assert_term_strings_roundtrip::<RemoteBinding>();
682 assert_term_strings_roundtrip::<Provenance>();
683 assert_term_strings_roundtrip::<Scale>();
684 assert_term_strings_roundtrip::<RetrievalGranularity>();
685 assert_term_strings_roundtrip::<Authority>();
686 assert_term_strings_roundtrip::<Isolation>();
687 assert_term_strings_roundtrip::<Reversibility>();
688 assert_term_strings_roundtrip::<PersistenceLevel>();
689 assert_term_strings_roundtrip::<TriggerEscape>();
690 assert_term_strings_roundtrip::<TriggerKind>();
691 assert_term_strings_roundtrip::<DisclosureAudience>();
692 assert_term_strings_roundtrip::<SecretLevel>();
693 assert_term_strings_roundtrip::<Channel>();
694 assert_term_strings_roundtrip::<Principal>();
695 assert_term_strings_roundtrip::<NetDirection>();
696 assert_term_strings_roundtrip::<NetDestination>();
697 assert_term_strings_roundtrip::<NetPayload>();
698 assert_term_strings_roundtrip::<ExecutionTrust>();
699 assert_term_strings_roundtrip::<SupplySource>();
700 assert_term_strings_roundtrip::<Pinning>();
701 assert_term_strings_roundtrip::<ExecSurface>();
702 assert_term_strings_roundtrip::<Cost>();
703 }
704
705 #[test]
706 fn ordinal_zero_terms_are_the_minimum() {
707 assert_zero_is_minimum::<LocalLocus>();
708 assert_zero_is_minimum::<RemoteReach>();
709 assert_zero_is_minimum::<Provenance>();
710 assert_zero_is_minimum::<Scale>();
711 assert_zero_is_minimum::<RetrievalGranularity>();
712 assert_zero_is_minimum::<Authority>();
713 assert_zero_is_minimum::<Isolation>();
714 assert_zero_is_minimum::<Reversibility>();
715 assert_zero_is_minimum::<PersistenceLevel>();
716 assert_zero_is_minimum::<TriggerEscape>();
717 assert_zero_is_minimum::<DisclosureAudience>();
718 assert_zero_is_minimum::<SecretLevel>();
719 assert_zero_is_minimum::<NetDirection>();
720 assert_zero_is_minimum::<NetDestination>();
721 assert_zero_is_minimum::<NetPayload>();
722 assert_zero_is_minimum::<ExecutionTrust>();
723 assert_zero_is_minimum::<Pinning>();
724 assert_zero_is_minimum::<Cost>();
725 }
726
727 #[test]
728 fn ordinal_ladders_match_the_spec() {
729 assert!(LocalLocus::Process < LocalLocus::Worktree);
730 assert!(LocalLocus::Worktree < LocalLocus::Adjacent);
731 assert!(LocalLocus::Adjacent < LocalLocus::WorktreeTrusted);
732 assert!(LocalLocus::WorktreeTrusted < LocalLocus::User);
733 assert!(LocalLocus::Worktree < LocalLocus::Machine);
734 assert!(LocalLocus::Machine < LocalLocus::SystemIntegrity);
735 assert!(LocalLocus::SystemIntegrity < LocalLocus::Device);
736 assert!(LocalLocus::Device < LocalLocus::Kernel);
737 assert!(Scale::Single < Scale::Bounded && Scale::Bounded < Scale::Unbounded);
738 assert!(RetrievalGranularity::Metadata < RetrievalGranularity::Record);
739 assert!(RetrievalGranularity::Record < RetrievalGranularity::BulkContent);
740 assert!(Authority::User < Authority::Root && Authority::Root < Authority::OtherUser);
741 assert!(Reversibility::Recoverable < Reversibility::Irreversible);
742 assert!(PersistenceLevel::Data < PersistenceLevel::Installing);
743 assert!(TriggerEscape::Immediate < TriggerEscape::Boot);
744 assert!(DisclosureAudience::LocalProcess < DisclosureAudience::Public);
745 assert!(Provenance::Na < Provenance::Established);
746 assert!(Provenance::Established < Provenance::Literal);
747 assert!(Provenance::Literal < Provenance::Opaque);
748 assert!(SecretLevel::Reads < SecretLevel::Transmits);
749 assert!(ExecutionTrust::SelfCode < ExecutionTrust::NetworkSourced);
750 assert!(Pinning::Floating < Pinning::HashVerified);
751 }
752
753 #[test]
754 fn capability_new_leaves_all_other_facets_at_zero() {
755 let cap = Capability::new(Operation::Destroy);
756 assert_eq!(cap.operation, Operation::Destroy);
757 assert_eq!(cap.locus, Locus::default());
758 assert_eq!(cap.locus.local, LocalLocus::Process);
759 assert_eq!(cap.scale, Scale::Single);
760 assert_eq!(cap.retrieval, RetrievalGranularity::Metadata);
761 assert_eq!(cap.authority, Authority::User);
762 assert_eq!(cap.reversibility, Reversibility::None);
763 assert_eq!(cap.secret.level, SecretLevel::None);
764 assert_eq!(cap.disclosure.audience, DisclosureAudience::None);
765 assert_eq!(cap.network.direction, NetDirection::None);
766 assert_eq!(cap.execution.trust, ExecutionTrust::None);
767 assert!(cap.execution.supply_chain.is_none());
768 assert_eq!(cap.cost, Cost::None);
769 assert!(cap.because.is_empty());
770 }
771
772 #[test]
773 fn default_capability_is_a_zero_observe() {
774 assert_eq!(Capability::default().operation, Operation::Observe);
775 assert_eq!(Capability::default(), Capability::new(Operation::Observe));
776 }
777}