1use std::collections::BTreeMap;
13use std::sync::LazyLock;
14
15use serde::Deserialize;
16
17use super::facet::{Capability, FacetTerm, Operation};
18
19pub fn archetype(name: &str) -> Option<&'static Capability> {
22 ARCHETYPES.get(name)
23}
24
25pub fn names() -> impl Iterator<Item = &'static str> {
27 ARCHETYPES.keys().map(String::as_str)
28}
29
30static ARCHETYPES: LazyLock<BTreeMap<String, Capability>> = LazyLock::new(|| {
31 build_archetypes(include_str!("../../archetypes.toml")).expect("embedded archetypes.toml must compile")
32});
33
34fn build_archetypes(src: &str) -> Result<BTreeMap<String, Capability>, String> {
35 let set: TomlArchetypeSet = toml::from_str(src).map_err(|e| e.to_string())?;
36 set.archetype
37 .into_iter()
38 .map(|(name, tc)| build_capability(&name, tc).map(|c| (name, c)))
39 .collect()
40}
41
42fn build_capability(name: &str, tc: TomlCapability) -> Result<Capability, String> {
43 let operation = Operation::from_term(&tc.operation)
44 .ok_or_else(|| format!("archetype `{name}`: unknown operation `{}`", tc.operation))?;
45 let mut c = Capability::new(operation);
46
47 if let Some(l) = &tc.locus {
48 set_term(name, "locus.local", l.local.as_deref(), &mut c.locus.local)?;
49 set_term(name, "locus.remote", l.remote.as_deref(), &mut c.locus.remote)?;
50 set_term(name, "locus.binding", l.binding.as_deref(), &mut c.locus.binding)?;
51 set_term(name, "locus.provenance", l.provenance.as_deref(), &mut c.locus.provenance)?;
52 }
53 set_term(name, "scale", tc.scale.as_deref(), &mut c.scale)?;
54 set_term(name, "retrieval", tc.retrieval.as_deref(), &mut c.retrieval)?;
55 set_term(name, "authority", tc.authority.as_deref(), &mut c.authority)?;
56 set_term(name, "reversibility", tc.reversibility.as_deref(), &mut c.reversibility)?;
57 if let Some(p) = &tc.persistence {
58 set_term(name, "persistence.level", p.level.as_deref(), &mut c.persistence.level)?;
59 }
60 if let Some(d) = &tc.disclosure {
61 set_term(name, "disclosure.audience", d.audience.as_deref(), &mut c.disclosure.audience)?;
62 }
63 if let Some(s) = &tc.secret {
64 set_term(name, "secret.level", s.level.as_deref(), &mut c.secret.level)?;
65 }
66 if let Some(net) = &tc.network {
67 set_term(name, "network.direction", net.direction.as_deref(), &mut c.network.direction)?;
68 set_term(name, "network.destination", net.destination.as_deref(), &mut c.network.destination)?;
69 set_term(name, "network.payload", net.payload.as_deref(), &mut c.network.payload)?;
70 }
71 set_term(name, "execution", tc.execution.as_deref(), &mut c.execution.trust)?;
72 set_term(name, "cost", tc.cost.as_deref(), &mut c.cost)?;
73
74 if tc.because.trim().is_empty() {
75 return Err(format!("archetype `{name}`: `because` is required"));
76 }
77 c.because = tc.because;
78 Ok(c)
79}
80
81fn set_term<T: FacetTerm>(name: &str, field: &str, s: Option<&str>, slot: &mut T) -> Result<(), String> {
84 if let Some(v) = s {
85 *slot = T::from_term(v).ok_or_else(|| format!("archetype `{name}`: unknown {field} term `{v}`"))?;
86 }
87 Ok(())
88}
89
90#[derive(Deserialize)]
91struct TomlArchetypeSet {
92 #[serde(default)]
93 archetype: BTreeMap<String, TomlCapability>,
94}
95
96#[derive(Deserialize)]
97#[serde(deny_unknown_fields)]
98struct TomlCapability {
99 operation: String,
100 because: String,
101 #[serde(default)]
102 locus: Option<TomlLocus>,
103 #[serde(default)]
104 scale: Option<String>,
105 #[serde(default)]
106 retrieval: Option<String>,
107 #[serde(default)]
108 authority: Option<String>,
109 #[serde(default)]
110 reversibility: Option<String>,
111 #[serde(default)]
112 persistence: Option<TomlPersistence>,
113 #[serde(default)]
114 disclosure: Option<TomlDisclosure>,
115 #[serde(default)]
116 secret: Option<TomlSecret>,
117 #[serde(default)]
118 network: Option<TomlNetwork>,
119 #[serde(default)]
120 execution: Option<String>,
121 #[serde(default)]
122 cost: Option<String>,
123}
124
125#[derive(Deserialize)]
126#[serde(deny_unknown_fields)]
127struct TomlLocus {
128 local: Option<String>,
129 remote: Option<String>,
130 binding: Option<String>,
131 provenance: Option<String>,
132}
133
134#[derive(Deserialize)]
135#[serde(deny_unknown_fields)]
136struct TomlPersistence {
137 level: Option<String>,
138}
139
140#[derive(Deserialize)]
141#[serde(deny_unknown_fields)]
142struct TomlDisclosure {
143 audience: Option<String>,
144}
145
146#[derive(Deserialize)]
147#[serde(deny_unknown_fields)]
148struct TomlSecret {
149 level: Option<String>,
150}
151
152#[derive(Deserialize)]
153#[serde(deny_unknown_fields)]
154struct TomlNetwork {
155 direction: Option<String>,
156 destination: Option<String>,
157 payload: Option<String>,
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::engine::authoring::default_levels;
164 use crate::engine::bridge::project;
165 use crate::engine::facet::Profile;
166 use crate::engine::level::Level;
167 use crate::verdict::Verdict;
168
169 fn level(name: &str) -> &'static Level {
170 default_levels().iter().find(|l| l.name == name).expect("level exists")
171 }
172
173 #[test]
179 fn sibling_adjacent_locus_reads_at_reader_writes_at_developer_not_editor() {
180 use crate::engine::facet::{Capability, LocalLocus, Operation, PersistenceLevel, Reversibility};
181 let at_adjacent = |op: Operation| {
182 let mut c = Capability::new(op);
183 c.locus.local = LocalLocus::Adjacent;
184 c
185 };
186 let read = Profile::of(vec![at_adjacent(Operation::Observe)]);
187 let patch = Profile::of(vec![{
188 let mut c = at_adjacent(Operation::Mutate);
189 c.reversibility = Reversibility::Recoverable;
190 c.persistence.level = PersistenceLevel::Data;
191 c
192 }]);
193 let destroy = Profile::of(vec![at_adjacent(Operation::Destroy)]);
194
195 assert!(level("reader").admits(&read), "a sibling READ auto-approves from reader");
196 assert!(level("developer").admits(&read), "…and higher");
197 assert!(!level("editor").admits(&patch), "editor does NOT write a sibling (writes stay worktree)");
198 assert!(level("developer").admits(&patch), "developer patches a sibling (create/mutate)");
199 assert!(!level("editor").admits(&destroy), "editor does NOT destroy a sibling");
200 assert!(!level("developer").admits(&destroy), "developer does NOT destroy a sibling (conservative — its destroy clause stays `<= worktree`)");
201 }
202
203 #[test]
204 fn archetypes_toml_compiles_and_every_capability_is_justified() {
205 let mut count = 0;
207 for n in names() {
208 let c = archetype(n).expect("listed archetype resolves");
209 assert!(!c.because.is_empty(), "archetype `{n}` has no because");
210 count += 1;
211 }
212 assert!(count >= 10, "expected the full catalog, got {count}");
213 assert!(archetype("does-not-exist").is_none(), "unknown profile fails closed");
214 }
215
216 #[test]
220 fn archetypes_land_where_the_catalog_says() {
221 let cases: &[(&str, &str, &str)] = &[
223 ("remote-read", "reader", "paranoid"),
225 ("data-export", "reader", "paranoid"),
228 ("remote-mutate", "network-admin", "developer"),
229 ("remote-create", "network-admin", "developer"),
230 ("remote-destroy-recoverable", "network-admin", "developer"),
231 ("remote-destroy-irreversible", "yolo", "network-admin"),
232 ("remote-authorize", "network-admin", "developer"),
233 ("remote-control", "network-admin", "developer"),
234 ("vcs-sync", "network-admin", "developer"),
235 ("blockchain-txn", "yolo", "network-admin"),
236 ("local-privileged", "local-admin", "developer"),
237 ("privileged-control", "local-admin", "developer"),
239 ("local-install-pinned", "developer", "editor"),
241 ("supply-chain-build", "yolo", "developer"),
243 ("remote-exec", "yolo", "network-admin"),
245 ("credential-read", "yolo", "network-admin"),
247 ("credential-mint", "yolo", "network-admin"),
248 ("decrypt-read", "yolo", "network-admin"),
251 ("bulk-object-read", "network-admin", "developer"),
256 ];
257 for (name, admitted_by, refused_by) in cases {
258 let p = Profile::of(vec![archetype(name).expect("archetype exists").clone()]);
259 assert!(level(admitted_by).admits(&p), "{name} should be admitted by {admitted_by}");
260 assert!(!level(refused_by).admits(&p), "{name} should be refused by {refused_by}");
261 }
262 }
263
264 #[test]
269 fn every_remote_write_archetype_is_not_auto_approved() {
270 let write_remotes = names()
271 .filter(|n| (n.starts_with("remote-") && *n != "remote-read") || *n == "vcs-sync" || *n == "blockchain-txn");
272 for name in write_remotes {
273 let p = Profile::of(vec![archetype(name).expect("archetype").clone()]);
274 assert_eq!(project(&p), Verdict::Denied, "{name} must not auto-approve in the 3-value projection");
275 }
276 assert_eq!(
278 project(&Profile::of(vec![archetype("remote-read").unwrap().clone()])),
279 Verdict::Allowed(crate::verdict::SafetyLevel::SafeRead),
280 "a pure remote fetch is reader-level",
281 );
282 }
283
284 #[test]
292 fn public_disclosure_is_recorded_not_gated_secret_transmission_is() {
293 use crate::engine::facet::{
294 DisclosureAudience, NetDestination, NetDirection, NetPayload, Network, RemoteReach,
295 Reversibility, SecretLevel,
296 };
297
298 let publish_to_public = || {
299 let mut c = Capability::new(Operation::Communicate);
300 c.locus.remote = RemoteReach::Arbitrary;
301 c.reversibility = Reversibility::Effortful;
302 c.disclosure.audience = DisclosureAudience::Public;
303 c.network = Network {
304 direction: NetDirection::Outbound,
305 destination: NetDestination::Arbitrary,
306 payload: NetPayload::SendsHostData,
307 };
308 c
309 };
310
311 let mut publish = publish_to_public();
313 publish.because = "publish authored content to a public destination".into();
314 let publish = Profile::of(vec![publish]);
315 assert!(level("network-admin").admits(&publish), "public non-secret publish is network-admin");
316 assert!(!level("developer").admits(&publish), "outbound remote egress is above developer");
317
318 let mut exfil = publish_to_public();
320 exfil.secret.level = SecretLevel::Transmits;
321 exfil.because = "transmit a secret off-box".into();
322 let exfil = Profile::of(vec![exfil]);
323 assert!(!level("network-admin").admits(&exfil), "secret transmission is the gate, above network-admin");
324 assert!(level("yolo").admits(&exfil), "yolo admits secret exfil (non-destroy clause)");
325 }
326
327 #[test]
333 fn system_integrity_is_above_local_admin_ordinary_machine_is_not() {
334 use crate::engine::facet::{Authority, LocalLocus};
335 let (local, yolo) = (level("local-admin"), level("yolo"));
336
337 let root_write_at = |loc| {
338 let mut c = Capability::new(Operation::Mutate);
339 c.locus.local = loc;
340 c.authority = Authority::Root;
341 c.because = "root machine write".into();
342 Profile::of(vec![c])
343 };
344
345 assert!(local.admits(&root_write_at(LocalLocus::Machine)), "ordinary machine write is local-admin");
347
348 let integrity = root_write_at(LocalLocus::SystemIntegrity);
350 assert!(!local.admits(&integrity), "the system-integrity substrate is above local-admin");
351 assert!(yolo.admits(&integrity), "yolo owns the machine's trust root");
352 }
353
354 #[test]
362 fn pinned_scripts_off_install_is_developer_the_supply_chain_surface_is_yolo() {
363 use crate::engine::facet::{
364 ExecutionTrust, LocalLocus, NetDirection, NetPayload, PersistenceLevel, Reversibility,
365 };
366 let (dev, yolo) = (level("developer"), level("yolo"));
367
368 let safe_install = {
370 let mut c = Capability::new(Operation::Create);
371 c.locus.local = LocalLocus::Worktree;
372 c.persistence.level = PersistenceLevel::Installing;
373 c.reversibility = Reversibility::Effortful;
374 c.network.direction = NetDirection::Outbound;
375 c.network.payload = NetPayload::Fetches;
376 c.execution.trust = ExecutionTrust::SelfCode;
377 c.because = "pinned, scripts-off install".into();
378 Profile::of(vec![c])
379 };
380 assert!(dev.admits(&safe_install), "a pinned, scripts-off install is developer");
381 assert!(yolo.admits(&safe_install), "and of course yolo");
382
383 let supply_chain = Profile::of(vec![archetype("supply-chain-build").unwrap().clone()]);
385 assert!(!dev.admits(&supply_chain), "network-sourced install (scripts on / unpinned) is above developer");
386 assert!(yolo.admits(&supply_chain), "the supply-chain surface lands at yolo");
387 }
388
389 #[test]
396 fn a_literal_send_target_is_network_admin_an_opaque_one_is_yolo() {
397 use crate::engine::facet::{NetDirection, NetPayload, Provenance, RemoteReach};
398
399 let send_to = |prov| {
400 let mut c = Capability::new(Operation::Communicate);
401 c.locus.remote = RemoteReach::Fixed;
402 c.locus.provenance = prov;
403 c.network.direction = NetDirection::Outbound;
404 c.network.payload = NetPayload::SendsHostData;
405 c.because = "send host data to a designated target".into();
406 c
407 };
408
409 let literal = Profile::of(vec![send_to(Provenance::Literal)]);
410 assert!(level("network-admin").admits(&literal), "a visible literal URL is a network-admin send");
411 assert!(!level("developer").admits(&literal), "sends-host-data is above the local developer band");
412
413 let opaque = Profile::of(vec![send_to(Provenance::Opaque)]);
414 assert!(!level("network-admin").admits(&opaque), "an opaque (variable) destination is held above network-admin");
415 assert!(level("yolo").admits(&opaque), "yolo leaves provenance unconstrained");
416 }
417}