Skip to main content

safe_chains/engine/
authoring.rs

1//! Compiling level TOML into [`Level`] values (v1.4 §4.1) — the analogue of
2//! `build_command` for the level language.
3//!
4//! A `[level.<name>]` table carries an optional `extends`, a list of `allow`
5//! clauses, and (for the loosest level only) `deny` clauses. Each clause maps a
6//! facet key to a constraint: an ordinal `"<= term"` / `">= term"` / `"term"`
7//! (exact), or a categorical term / list of terms. Compound facets are nested
8//! tables (`locus = { local = "<= worktree", remote = "none" }`).
9//!
10//! `extends` composes upward only (R27): an extending level inherits its base's
11//! allow *and* deny clauses and may add only allow clauses — declaring `deny` on an
12//! extending level is a compile error.
13
14use std::collections::BTreeMap;
15use std::sync::LazyLock;
16
17use serde::{Deserialize, Serialize};
18
19use super::facet::FacetTerm;
20use super::level::{Clause, Level, OrdBound};
21
22/// The default level set, compiled once from the embedded `levels/default.toml`.
23pub fn default_levels() -> &'static [Level] {
24    static LEVELS: LazyLock<Vec<Level>> =
25        LazyLock::new(|| build_level_set(include_str!("../../levels/default.toml")).expect("embedded levels/default.toml must compile"));
26    &LEVELS
27}
28
29/// Compile a TOML level set into levels, resolving `extends` in dependency order.
30pub fn build_level_set(source: &str) -> Result<Vec<Level>, String> {
31    let set: TomlLevelSet = toml::from_str(source).map_err(|e| e.to_string())?;
32    let mut pending: Vec<(String, TomlLevel)> = set.level.into_iter().collect();
33    let mut built: Vec<Level> = Vec::new();
34    let mut by_name: BTreeMap<String, usize> = BTreeMap::new();
35
36    while !pending.is_empty() {
37        let before = pending.len();
38        let mut still = Vec::new();
39        for (name, tl) in pending {
40            let ready = tl.extends.as_ref().is_none_or(|base| by_name.contains_key(base));
41            if ready {
42                let level = compile_level(name.clone(), tl, &built, &by_name)?;
43                by_name.insert(name, built.len());
44                built.push(level);
45            } else {
46                still.push((name, tl));
47            }
48        }
49        if still.len() == before {
50            let names: Vec<&String> = still.iter().map(|(n, _)| n).collect();
51            return Err(format!("unresolved `extends` (cycle or missing base) among {names:?}"));
52        }
53        pending = still;
54    }
55    Ok(built)
56}
57
58fn compile_level(name: String, tl: TomlLevel, built: &[Level], by_name: &BTreeMap<String, usize>) -> Result<Level, String> {
59    let allow = tl
60        .allow
61        .into_iter()
62        .map(build_clause)
63        .collect::<Result<Vec<_>, _>>()
64        .map_err(|e| format!("level `{name}`: {e}"))?;
65    let deny = tl
66        .deny
67        .into_iter()
68        .map(build_clause)
69        .collect::<Result<Vec<_>, _>>()
70        .map_err(|e| format!("level `{name}`: {e}"))?;
71
72    match tl.extends {
73        Some(base_name) => {
74            if !deny.is_empty() {
75                return Err(format!(
76                    "level `{name}` extends `{base_name}` and declares `deny` — extends only \
77                     loosens (R27); author a stricter level from a lower base instead"
78                ));
79            }
80            let idx = *by_name.get(&base_name).ok_or_else(|| format!("level `{name}`: unknown base `{base_name}`"))?;
81            let base = built.get(idx).ok_or_else(|| format!("level `{name}`: base index out of range"))?;
82            Ok(Level::extend(base, name, allow))
83        }
84        None => Ok(Level { name, allow, deny }),
85    }
86}
87
88fn build_clause(tc: TomlClause) -> Result<Clause, String> {
89    let mut c = Clause::default();
90    if let Some(v) = tc.operation {
91        c.operation = Some(parse_set(&v)?);
92    }
93    if let Some(l) = tc.locus {
94        c.local_locus = opt_bound(l.local.as_deref())?;
95        c.remote_reach = opt_bound(l.remote.as_deref())?;
96        c.provenance = opt_bound(l.provenance.as_deref())?;
97        if let Some(b) = l.binding {
98            c.remote_binding = Some(parse_set(&b)?);
99        }
100    }
101    c.scale = opt_bound(tc.scale.as_deref())?;
102    c.retrieval = opt_bound(tc.retrieval.as_deref())?;
103    c.authority = opt_bound(tc.authority.as_deref())?;
104    c.isolation = opt_bound(tc.isolation.as_deref())?;
105    c.reversibility = opt_bound(tc.reversibility.as_deref())?;
106    if let Some(p) = tc.persistence {
107        c.persistence_level = opt_bound(p.level.as_deref())?;
108        if let Some(t) = p.trigger {
109            c.trigger_escape = opt_bound(t.escape.as_deref())?;
110            if let Some(k) = t.kind {
111                c.trigger_kind = Some(parse_set(&k)?);
112            }
113        }
114    }
115    if let Some(d) = tc.disclosure {
116        c.disclosure_audience = opt_bound(d.audience.as_deref())?;
117        if let Some(ch) = d.channel {
118            c.disclosure_channel = Some(parse_set(&ch)?);
119        }
120        if let Some(pr) = d.principal {
121            c.disclosure_principal = Some(parse_set(&pr)?);
122        }
123    }
124    if let Some(s) = tc.secret {
125        c.secret_level = opt_bound(s.level.as_deref())?;
126        if let Some(ch) = s.channel {
127            c.secret_channel = Some(parse_set(&ch)?);
128        }
129        if let Some(pr) = s.principal {
130            c.secret_principal = Some(parse_set(&pr)?);
131        }
132    }
133    if let Some(n) = tc.network {
134        c.net_direction = opt_bound(n.direction.as_deref())?;
135        c.net_destination = opt_bound(n.destination.as_deref())?;
136        c.net_payload = opt_bound(n.payload.as_deref())?;
137    }
138    c.execution_trust = opt_bound(tc.execution.as_deref())?;
139    if let Some(sc) = tc.supply_chain {
140        if let Some(s) = sc.source {
141            c.supply_source = Some(parse_set(&s)?);
142        }
143        c.pinning = opt_bound(sc.pinning.as_deref())?;
144        if let Some(e) = sc.exec_surface {
145            c.exec_surface = Some(parse_set(&e)?);
146        }
147    }
148    c.cost = opt_bound(tc.cost.as_deref())?;
149    Ok(c)
150}
151
152fn opt_bound<T: FacetTerm + Ord>(s: Option<&str>) -> Result<Option<OrdBound<T>>, String> {
153    s.map(parse_bound).transpose()
154}
155
156/// Parse an ordinal constraint: `"<= term"`, `">= term"`, `"term"` (exact), or a
157/// two-sided range `">= lo, <= hi"` (a comma-separated floor and ceiling, order
158/// insensitive). A range is the only form that pins both ends — needed where an
159/// admit set is an interior band of the ladder (e.g. an executor locus that is
160/// worktree-local but neither below it, `temp`, nor above it, `user`).
161fn parse_bound<T: FacetTerm + Ord>(s: &str) -> Result<OrdBound<T>, String> {
162    let parts: Vec<&str> = s.split(',').map(str::trim).collect();
163    if parts.len() == 1 {
164        let p = parts[0];
165        return if let Some(rest) = p.strip_prefix("<=") {
166            Ok(OrdBound::at_most(parse_term(rest)?))
167        } else if let Some(rest) = p.strip_prefix(">=") {
168            Ok(OrdBound::at_least(parse_term(rest)?))
169        } else {
170            Ok(OrdBound::exactly(parse_term(p.strip_prefix('=').unwrap_or(p))?))
171        };
172    }
173    let (mut min, mut max) = (None, None);
174    for p in parts {
175        if let Some(rest) = p.strip_prefix("<=") {
176            if max.replace(parse_term(rest)?).is_some() {
177                return Err(format!("bound `{s}` sets `<=` more than once"));
178            }
179        } else if let Some(rest) = p.strip_prefix(">=") {
180            if min.replace(parse_term(rest)?).is_some() {
181                return Err(format!("bound `{s}` sets `>=` more than once"));
182            }
183        } else {
184            return Err(format!("bound `{s}`: each part of a range must be `<=`/`>=`"));
185        }
186    }
187    Ok(OrdBound { min, max })
188}
189
190fn parse_set<T: FacetTerm>(v: &StringOrVec) -> Result<Vec<T>, String> {
191    v.as_slice().iter().map(|s| parse_term(s)).collect()
192}
193
194fn parse_term<T: FacetTerm>(s: &str) -> Result<T, String> {
195    T::from_term(s.trim()).ok_or_else(|| format!("unknown term `{}`", s.trim()))
196}
197
198// ── the TOML schema ────────────────────────────────────────────────────────────
199
200// Serialization mirrors deserialization so a compiled level round-trips back to
201// equivalent TOML (`skip_serializing_if` keeps unset facets out of the output).
202
203#[derive(Deserialize, Serialize)]
204struct TomlLevelSet {
205    #[serde(default)]
206    level: BTreeMap<String, TomlLevel>,
207}
208
209#[derive(Deserialize, Serialize)]
210#[serde(deny_unknown_fields)]
211struct TomlLevel {
212    #[serde(skip_serializing_if = "Option::is_none")]
213    extends: Option<String>,
214    #[serde(default, skip_serializing_if = "Vec::is_empty")]
215    allow: Vec<TomlClause>,
216    #[serde(default, skip_serializing_if = "Vec::is_empty")]
217    deny: Vec<TomlClause>,
218}
219
220#[derive(Deserialize, Serialize, Default)]
221#[serde(deny_unknown_fields)]
222struct TomlClause {
223    #[serde(skip_serializing_if = "Option::is_none")]
224    operation: Option<StringOrVec>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    locus: Option<TomlLocus>,
227    #[serde(skip_serializing_if = "Option::is_none")]
228    scale: Option<String>,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    retrieval: Option<String>,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    authority: Option<String>,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    isolation: Option<String>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    reversibility: Option<String>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    persistence: Option<TomlPersistence>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    disclosure: Option<TomlDisclosure>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    secret: Option<TomlSecret>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    network: Option<TomlNetwork>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    execution: Option<String>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    supply_chain: Option<TomlSupplyChain>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    cost: Option<String>,
251}
252
253#[derive(Deserialize, Serialize)]
254#[serde(deny_unknown_fields)]
255struct TomlLocus {
256    #[serde(skip_serializing_if = "Option::is_none")]
257    local: Option<String>,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    remote: Option<String>,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    binding: Option<StringOrVec>,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    provenance: Option<String>,
264}
265
266#[derive(Deserialize, Serialize)]
267#[serde(deny_unknown_fields)]
268struct TomlPersistence {
269    #[serde(skip_serializing_if = "Option::is_none")]
270    level: Option<String>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    trigger: Option<TomlTrigger>,
273}
274
275#[derive(Deserialize, Serialize)]
276#[serde(deny_unknown_fields)]
277struct TomlTrigger {
278    #[serde(skip_serializing_if = "Option::is_none")]
279    escape: Option<String>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    kind: Option<StringOrVec>,
282}
283
284#[derive(Deserialize, Serialize)]
285#[serde(deny_unknown_fields)]
286struct TomlDisclosure {
287    #[serde(skip_serializing_if = "Option::is_none")]
288    audience: Option<String>,
289    #[serde(skip_serializing_if = "Option::is_none")]
290    channel: Option<StringOrVec>,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    principal: Option<StringOrVec>,
293}
294
295#[derive(Deserialize, Serialize)]
296#[serde(deny_unknown_fields)]
297struct TomlSecret {
298    #[serde(skip_serializing_if = "Option::is_none")]
299    level: Option<String>,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    channel: Option<StringOrVec>,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    principal: Option<StringOrVec>,
304}
305
306#[derive(Deserialize, Serialize)]
307#[serde(deny_unknown_fields)]
308struct TomlNetwork {
309    #[serde(skip_serializing_if = "Option::is_none")]
310    direction: Option<String>,
311    #[serde(skip_serializing_if = "Option::is_none")]
312    destination: Option<String>,
313    #[serde(skip_serializing_if = "Option::is_none")]
314    payload: Option<String>,
315}
316
317#[derive(Deserialize, Serialize)]
318#[serde(deny_unknown_fields)]
319struct TomlSupplyChain {
320    #[serde(skip_serializing_if = "Option::is_none")]
321    source: Option<StringOrVec>,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pinning: Option<String>,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    exec_surface: Option<StringOrVec>,
326}
327
328#[derive(Deserialize, Serialize)]
329#[serde(untagged)]
330enum StringOrVec {
331    One(String),
332    Many(Vec<String>),
333}
334
335impl StringOrVec {
336    fn as_slice(&self) -> &[String] {
337        match self {
338            StringOrVec::One(s) => std::slice::from_ref(s),
339            StringOrVec::Many(v) => v,
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use crate::engine::facet::*;
348
349    fn level<'a>(levels: &'a [Level], name: &str) -> &'a Level {
350        levels.iter().find(|l| l.name == name).expect("level exists")
351    }
352
353    fn observe_at(local: LocalLocus) -> Profile {
354        let mut c = Capability::new(Operation::Observe);
355        c.locus.local = local;
356        Profile::of(vec![c])
357    }
358
359    #[test]
360    fn the_default_ladder_compiles() {
361        let levels = default_levels();
362        let mut names: Vec<&str> = levels.iter().map(|l| l.name.as_str()).collect();
363        names.sort_unstable();
364        assert_eq!(names, ["developer", "editor", "local-admin", "network-admin", "paranoid", "reader", "yolo"],);
365        // yolo is a base level (carries the catastrophe `deny`), so build order isn't the ladder
366        // order — but the mapped auto-approve band MUST stay ascending, since `bridge::project`
367        // returns the first admitting mapped level as the minimum.
368        let raw: Vec<&str> = levels.iter().map(|l| l.name.as_str()).collect();
369        let pos = |n| raw.iter().position(|&x| x == n).expect("level present");
370        assert!(
371            pos("paranoid") < pos("reader") && pos("reader") < pos("editor") && pos("editor") < pos("developer"),
372            "mapped band out of order: {raw:?}",
373        );
374    }
375
376    #[test]
377    fn inert_admits_a_version_probe_but_not_reading_the_worktree() {
378        let levels = default_levels();
379        let inert = level(levels, "paranoid");
380        assert!(inert.admits(&observe_at(LocalLocus::Process)), "node --version");
381        assert!(!inert.admits(&observe_at(LocalLocus::Worktree)), "cat ./notes is above paranoid");
382    }
383
384    #[test]
385    fn read_local_reads_the_worktree_but_refuses_home_extraction_and_writes() {
386        let levels = default_levels();
387        let read_local = level(levels, "reader");
388        assert!(read_local.admits(&observe_at(LocalLocus::Worktree)), "cat ./notes");
389        assert!(read_local.admits(&observe_at(LocalLocus::WorktreeTrusted)), "git status reads .git");
390
391        // Home content READS, and this is the assertion that used to say otherwise. The old
392        // comment here read "cat ~/.ssh/id_rsa: locus=user, secret=none" — which is exactly the
393        // bug: the refusal came from the rung, so it took `~/notes.txt` down with the key and
394        // would have evaporated the moment the rung opened. The rung is open now, and what
395        // refuses the key is the secret claim `reads_path` attaches to it (asserted below).
396        assert!(read_local.admits(&observe_at(LocalLocus::User)), "cat ~/notes.txt");
397
398        // a credential-extraction command — denied by the positive secret claim
399        // (security find-generic-password -w: secret=reads, regardless of locus)
400        let extraction = {
401            let mut c = Capability::new(Operation::Observe);
402            c.secret.level = SecretLevel::Reads;
403            Profile::of(vec![c])
404        };
405        assert!(!read_local.admits(&extraction), "keychain extraction");
406
407        assert!(!read_local.admits(&Profile::of(vec![Capability::new(Operation::Create)])), "a write");
408    }
409
410    /// reader reads LOCAL and REMOTE alike (a pure fetch is a read), but the network read is a
411    /// pure fetch, never an egress: `sends-host-data` (exfil) and any remote WRITE stay above it.
412    #[test]
413    fn reader_admits_a_pure_remote_fetch_but_not_exfil_or_remote_writes() {
414        let reader = level(default_levels(), "reader");
415
416        let fetch = {
417            let mut c = Capability::new(Operation::Observe);
418            c.locus.remote = RemoteReach::Arbitrary;
419            c.network.direction = NetDirection::Outbound;
420            c.network.payload = NetPayload::Fetches;
421            c.disclosure.audience = DisclosureAudience::LocalProcess;
422            Profile::of(vec![c])
423        };
424        assert!(reader.admits(&fetch), "curl GET / koyeb list — a pure remote fetch");
425
426        // exfil: the request carries host data OUT — above reader
427        let exfil = {
428            let mut c = Capability::new(Operation::Observe);
429            c.locus.remote = RemoteReach::Arbitrary;
430            c.network.direction = NetDirection::Outbound;
431            c.network.payload = NetPayload::SendsHostData;
432            Profile::of(vec![c])
433        };
434        assert!(!reader.admits(&exfil), "sends-host-data (curl -d @secret) is not a read");
435
436        // a remote WRITE — above reader (this is the nuance that lives on the write side)
437        let remote_write = {
438            let mut c = Capability::new(Operation::Mutate);
439            c.locus.remote = RemoteReach::Fixed;
440            c.network.direction = NetDirection::Outbound;
441            Profile::of(vec![c])
442        };
443        assert!(!reader.admits(&remote_write), "a remote write is network-admin, not reader");
444
445        // paranoid still blocks the network entirely
446        assert!(!level(default_levels(), "paranoid").admits(&fetch), "paranoid blocks all network");
447    }
448
449    #[test]
450    fn write_local_writes_the_worktree_but_not_installs_or_mass_ops() {
451        let levels = default_levels();
452        let write_local = level(levels, "editor");
453
454        let touch = {
455            let mut c = Capability::new(Operation::Create);
456            c.locus.local = LocalLocus::Worktree;
457            Profile::of(vec![c])
458        };
459        assert!(write_local.admits(&touch), "touch build/out");
460        // still reads (inherited)
461        assert!(write_local.admits(&observe_at(LocalLocus::Worktree)));
462
463        let install = {
464            let mut c = Capability::new(Operation::Create);
465            c.locus.local = LocalLocus::Worktree;
466            c.persistence.level = PersistenceLevel::Installing;
467            Profile::of(vec![c])
468        };
469        assert!(!write_local.admits(&install), "installing is above write-local");
470    }
471
472    #[test]
473    fn developer_deletes_within_the_worktree_but_not_beyond_it() {
474        let levels = default_levels();
475        let (write_local, developer) = (level(levels, "editor"), level(levels, "developer"));
476
477        let destroy_at = |local| {
478            let mut c = Capability::new(Operation::Destroy);
479            c.locus.local = local;
480            c.scale = Scale::Unbounded; // rm -rf
481            c.reversibility = Reversibility::Effortful;
482            Profile::of(vec![c])
483        };
484        // recursive/effortful worktree delete admits at developer, but not at write-local
485        assert!(!write_local.admits(&destroy_at(LocalLocus::Worktree)), "rm waits for developer");
486        assert!(developer.admits(&destroy_at(LocalLocus::Worktree)), "rm -rf ./node_modules");
487        // .git/ (worktree-trusted), home, and system deletion stay above developer
488        assert!(!developer.admits(&destroy_at(LocalLocus::WorktreeTrusted)), "rm -rf .git");
489        assert!(!developer.admits(&destroy_at(LocalLocus::User)), "rm -rf ~");
490        assert!(!developer.admits(&destroy_at(LocalLocus::Machine)), "rm -rf /");
491
492        // the boundary is destroy vs create/overwrite: overwriting your own worktree file
493        // (a recoverable create — echo > f, cp ./a ./b) stays at write-local, NOT developer.
494        let overwrite = {
495            let mut c = Capability::new(Operation::Create);
496            c.locus.local = LocalLocus::Worktree;
497            c.reversibility = Reversibility::Recoverable;
498            c.persistence.level = PersistenceLevel::Data;
499            Profile::of(vec![c])
500        };
501        assert!(write_local.admits(&overwrite), "cp ./a ./b is write-local (create), not developer");
502        // developer still inherits every write-local grant
503        let touch = {
504            let mut c = Capability::new(Operation::Create);
505            c.locus.local = LocalLocus::Worktree;
506            Profile::of(vec![c])
507        };
508        assert!(developer.admits(&touch), "developer ⊇ write-local");
509    }
510
511    // Running code: the discriminator is the EXECUTOR-ORIGIN band, not blast radius. developer runs
512    // code that LIVES in the worktree (bash ./x.sh) but refuses FOREIGN code below the band
513    // (/tmp/x.sh, inline `python -c`) and SYSTEM code above it (~/x.sh, /usr/local/bin/x). The band's
514    // FLOOR (`>= sandbox-scope`) makes locus.local non-monotone for execute, so the coherence
515    // generator skips lowering it there (testgen::lowered_variants); this pins the band's exact edges
516    // so a future mis-authoring can't drop the floor or slide the ceiling undetected — the coverage
517    // gap that let the monotonicity break hide from the deterministic ceiling test.
518    #[test]
519    fn developer_runs_worktree_code_but_not_foreign_or_system() {
520        let levels = default_levels();
521        let developer = level(levels, "developer");
522        let exec_at = |local| {
523            let mut c = Capability::new(Operation::Execute);
524            c.locus.local = local;
525            c.execution.trust = ExecutionTrust::CallerFile; // bash ./x.sh — code from a named file
526            Profile::of(vec![c])
527        };
528        // In the band [sandbox-scope, worktree-trusted]: worktree-local (and sibling) code runs.
529        for local in [LocalLocus::SandboxScope, LocalLocus::Worktree, LocalLocus::Adjacent, LocalLocus::WorktreeTrusted] {
530            assert!(developer.admits(&exec_at(local)), "developer runs worktree-scope code: {local:?}");
531        }
532        // Below the band: foreign/downloaded (temp) or inline (process) code is denied.
533        assert!(!developer.admits(&exec_at(LocalLocus::Temp)), "bash /tmp/x.sh is foreign");
534        assert!(!developer.admits(&exec_at(LocalLocus::Process)), "inline `python -c` is below the band");
535        // Above the band: home/system executables are denied.
536        assert!(!developer.admits(&exec_at(LocalLocus::User)), "~/x.sh waits for a higher level");
537        assert!(!developer.admits(&exec_at(LocalLocus::Machine)), "/usr/local/bin/x waits for a higher level");
538    }
539
540    #[test]
541    fn the_ladder_nests() {
542        let levels = default_levels();
543        let (inert, read, write) = (level(levels, "paranoid"), level(levels, "reader"), level(levels, "editor"));
544        // everything inert admits, read-local and write-local admit too
545        for local in [LocalLocus::Process, LocalLocus::Temp] {
546            let p = observe_at(local);
547            assert!(inert.admits(&p) && read.admits(&p) && write.admits(&p));
548        }
549    }
550
551    /// The two admin flavors are INCOMPARABLE siblings above developer — each flexes a
552    /// disjoint facet region (local-admin down into the machine, network-admin out to the
553    /// network), and BOTH keep developer's `reversibility <= effortful` cap. Only yolo lifts
554    /// it. This is the partial-order the old linear `SafetyLevel` enum could not express.
555    #[test]
556    fn the_admin_flavors_flex_disjoint_regions_and_only_yolo_is_irreversible() {
557        let levels = default_levels();
558        let developer = level(levels, "developer");
559        let local_admin = level(levels, "local-admin");
560        let network_admin = level(levels, "network-admin");
561        let yolo = level(levels, "yolo");
562
563        // sudo: elevated authority on the machine — local-admin admits, network-admin refuses
564        let sudo = {
565            let mut c = Capability::new(Operation::Mutate);
566            c.locus.local = LocalLocus::Machine;
567            c.authority = Authority::Root;
568            Profile::of(vec![c])
569        };
570        assert!(!developer.admits(&sudo), "sudo is above developer");
571        assert!(local_admin.admits(&sudo), "local-admin runs this machine");
572        assert!(!network_admin.admits(&sudo), "network-admin never sudo's the box");
573
574        // remote mutate over the network — network-admin admits, local-admin refuses
575        let remote = {
576            let mut c = Capability::new(Operation::Mutate);
577            c.locus.remote = RemoteReach::Arbitrary;
578            c.network.direction = NetDirection::Outbound;
579            Profile::of(vec![c])
580        };
581        assert!(!developer.admits(&remote), "remote reach is above developer");
582        assert!(network_admin.admits(&remote), "network-admin operates remotes");
583        assert!(!local_admin.admits(&remote), "local-admin never reaches the network");
584
585        // the reversibility spine: irreversible destroy is reserved for yolo, on ANY locus
586        let irreversible = |local, remote| {
587            let mut c = Capability::new(Operation::Destroy);
588            c.locus.local = local;
589            c.locus.remote = remote;
590            c.reversibility = Reversibility::Irreversible;
591            Profile::of(vec![c])
592        };
593        let mkfs = irreversible(LocalLocus::Device, RemoteReach::None); // disk wipe
594        let tf_destroy = irreversible(LocalLocus::Process, RemoteReach::Fixed); // terraform destroy
595        assert!(!local_admin.admits(&mkfs), "mkfs (irreversible) is above local-admin");
596        assert!(!network_admin.admits(&tf_destroy), "terraform destroy (irreversible) is above network-admin");
597        assert!(yolo.admits(&mkfs) && yolo.admits(&tf_destroy), "irreversible destroy is reserved for yolo");
598
599        // but recoverable/effortful destruction in each direction stays at the flavor
600        let effortful_machine = {
601            let mut c = Capability::new(Operation::Destroy);
602            c.locus.local = LocalLocus::Machine;
603            c.scale = Scale::Unbounded;
604            c.reversibility = Reversibility::Effortful;
605            Profile::of(vec![c])
606        };
607        assert!(local_admin.admits(&effortful_machine), "sudo rm -rf /var (recoverable) is local-admin");
608    }
609
610    /// yolo lifts every cap EXCEPT the one catastrophe corner, carved purely by facets:
611    /// `destroy · irreversible · unbounded` (rm -rf /). Everything adjacent — bounded or
612    /// single-target irreversible destroy, or recoverable mass destroy — stays admitted,
613    /// distinguished by facet alone, never by command name.
614    #[test]
615    fn yolo_denies_only_unbounded_irreversible_destroy() {
616        let levels = default_levels();
617        let yolo = level(levels, "yolo");
618
619        let destroy = |scale, rev| {
620            let mut c = Capability::new(Operation::Destroy);
621            c.scale = scale;
622            c.reversibility = rev;
623            c.locus.local = LocalLocus::Machine;
624            Profile::of(vec![c])
625        };
626        // the one refusal: rm -rf / — destroy the world, no recovery, no bound
627        assert!(!yolo.admits(&destroy(Scale::Unbounded, Reversibility::Irreversible)), "rm -rf / is denied even at yolo",);
628        // everything one facet away stays yolo-allowed, by facet:
629        assert!(yolo.admits(&destroy(Scale::Bounded, Reversibility::Irreversible)), "terraform destroy (bounded)");
630        assert!(yolo.admits(&destroy(Scale::Single, Reversibility::Irreversible)), "mkfs (single device)");
631        assert!(yolo.admits(&destroy(Scale::Unbounded, Reversibility::Effortful)), "rm -rf ./x (recoverable)");
632        // and yolo still admits the non-destroy extremes it exists for
633        let wild = {
634            let mut c = Capability::new(Operation::Execute);
635            c.execution.trust = ExecutionTrust::NetworkSourced;
636            c.locus.local = LocalLocus::Kernel;
637            Profile::of(vec![c])
638        };
639        assert!(yolo.admits(&wild), "yolo still admits everything but the catastrophe corner");
640    }
641
642    #[test]
643    fn unknown_term_is_a_compile_error() {
644        let src = r#"
645            [level.x]
646            [[level.x.allow]]
647            scale = "<= enormous"
648        "#;
649        let err = build_level_set(src).unwrap_err();
650        assert!(err.contains("enormous"), "{err}");
651    }
652
653    #[test]
654    fn unknown_facet_key_is_a_compile_error() {
655        let src = r#"
656            [level.x]
657            [[level.x.allow]]
658            operashun = ["observe"]
659        "#;
660        assert!(build_level_set(src).is_err());
661    }
662
663    #[test]
664    fn deny_on_an_extending_level_is_rejected() {
665        let src = r#"
666            [level.base]
667            [[level.base.allow]]
668            operation = ["observe"]
669
670            [level.child]
671            extends = "base"
672            [[level.child.deny]]
673            operation = ["destroy"]
674        "#;
675        let err = build_level_set(src).unwrap_err();
676        assert!(err.contains("R27"), "{err}");
677    }
678
679    #[test]
680    fn scalar_facet_values_parse() {
681        // a set-valued facet given as a scalar (StringOrVec::One), not an array
682        let src = r#"
683            [level.x]
684            [[level.x.allow]]
685            operation = "observe"
686            locus = { binding = "pinned" }
687        "#;
688        let levels = build_level_set(src).expect("compiles");
689        let c = &level(&levels, "x").allow[0];
690        assert_eq!(c.operation, Some(vec![Operation::Observe]));
691        assert_eq!(c.remote_binding, Some(vec![RemoteBinding::Pinned]));
692    }
693
694    #[test]
695    fn a_mutual_extends_cycle_is_a_compile_error() {
696        let src = r#"
697            [level.a]
698            extends = "b"
699            [level.b]
700            extends = "a"
701        "#;
702        assert!(build_level_set(src).is_err());
703    }
704
705    #[test]
706    fn missing_base_is_a_compile_error() {
707        let src = r#"
708            [level.child]
709            extends = "ghost"
710            [[level.child.allow]]
711            operation = ["observe"]
712        "#;
713        assert!(build_level_set(src).is_err());
714    }
715
716    #[test]
717    fn ordinal_operators_parse() {
718        let src = r#"
719            [level.x]
720            [[level.x.allow]]
721            scale = ">= bounded"
722            reversibility = "<= recoverable"
723            authority = "root"
724        "#;
725        let levels = build_level_set(src).expect("compiles");
726        let clause = &level(&levels, "x").allow[0];
727        assert_eq!(clause.scale, Some(OrdBound::at_least(Scale::Bounded)));
728        assert_eq!(clause.reversibility, Some(OrdBound::at_most(Reversibility::Recoverable)));
729        assert_eq!(clause.authority, Some(OrdBound::exactly(Authority::Root)));
730    }
731
732    // ── facet-monotonicity: the coherence check on the authored levels ──────────────
733    //
734    // A level is coherent iff making any command *less* severe never flips it from
735    // admitted to denied. An allow clause with an ordinal *floor* (or an exact bound
736    // on a non-minimum term) would break this — the check exists to catch that in
737    // hand-authored TOML.
738
739    use crate::engine::testgen::{arb_capability, arb_profile, lowered_variants, predecessor};
740    use proptest::prelude::*;
741
742    fn assert_monotone_from(lvl: &Level, boundary: Capability) {
743        assert!(lvl.admits(&Profile::of(vec![boundary.clone()])), "{}: boundary capability should be admitted", lvl.name,);
744        for lowered in lowered_variants(&boundary) {
745            assert!(
746                lvl.admits(&Profile::of(vec![lowered.clone()])),
747                "{}: admitted a boundary cap but denied it after lowering one facet:\n  {:?}\n  {:?}",
748                lvl.name,
749                boundary,
750                lowered,
751            );
752        }
753    }
754
755    #[test]
756    fn authored_levels_are_monotone_at_their_ceilings() {
757        let levels = default_levels();
758
759        let mut inert_cap = Capability::new(Operation::Observe);
760        inert_cap.locus.local = LocalLocus::Temp;
761        inert_cap.disclosure.audience = DisclosureAudience::LocalProcess;
762        inert_cap.execution.trust = ExecutionTrust::SelfCode;
763        assert_monotone_from(level(levels, "paranoid"), inert_cap);
764
765        let mut read_cap = Capability::new(Operation::Observe);
766        read_cap.locus.local = LocalLocus::WorktreeTrusted;
767        read_cap.secret.level = SecretLevel::UsesAmbient;
768        read_cap.network.direction = NetDirection::Loopback;
769        read_cap.disclosure.audience = DisclosureAudience::LocalProcess;
770        read_cap.execution.trust = ExecutionTrust::SelfCode;
771        assert_monotone_from(level(levels, "reader"), read_cap);
772
773        let mut write_cap = Capability::new(Operation::Mutate);
774        write_cap.locus.local = LocalLocus::Worktree;
775        write_cap.scale = Scale::Bounded;
776        write_cap.reversibility = Reversibility::Recoverable;
777        write_cap.persistence.level = PersistenceLevel::Data;
778        write_cap.secret.level = SecretLevel::UsesAmbient;
779        write_cap.disclosure.audience = DisclosureAudience::LocalProcess;
780        write_cap.execution.trust = ExecutionTrust::CallerInline;
781        assert_monotone_from(level(levels, "editor"), write_cap);
782    }
783
784    /// A union level's name paired with the predicate matching the ONE capability it withholds.
785    type LevelGap = (&'static str, fn(&Capability) -> bool);
786
787    // ── union-level completeness: flat DNF's failure mode is a silent gap ────────────
788    //
789    // A level authored as a UNION of allow clauses to mean "allow almost everything" must admit a
790    // capability IFF it is not in that level's ONE intended hole. A missing clause leaves an
791    // accidental gap (a benign capability nothing admits → over-deny); a too-wide clause leaks the
792    // hole (the corner slips in → fail-open). This proves the union has EXACTLY its declared gap —
793    // the guard flat DNF needs before we lean on union constructions. Table-driven: add a row when
794    // a new union-level is authored, and the whole class stays covered.
795    proptest! {
796        #[test]
797        fn union_levels_admit_everything_but_their_declared_gap(cap in arb_capability()) {
798            let gaps: &[LevelGap] = &[
799                // yolo withholds only `destroy · irreversible · unbounded` (rm -rf /), carved by
800                // the union of its allow clauses — never by a deny.
801                ("yolo", |c: &Capability| {
802                    c.operation == Operation::Destroy
803                        && c.reversibility == Reversibility::Irreversible
804                        && c.scale == Scale::Unbounded
805                }),
806            ];
807            let levels = default_levels();
808            for (name, gap) in gaps {
809                let lvl = levels.iter().find(|l| &l.name == name).expect("level present");
810                let admitted = lvl.admits(&Profile::of(vec![cap.clone()]));
811                prop_assert_eq!(
812                    admitted, !gap(&cap),
813                    "level `{}`: capability {:?} admitted={} but intended_admit={}",
814                    name, cap, admitted, !gap(&cap),
815                );
816            }
817        }
818    }
819
820    proptest! {
821        /// For any profile an authored level admits, lowering any single ordinal facet
822        /// of any capability keeps the profile admitted.
823        #[test]
824        fn authored_levels_are_facet_monotone(profile in arb_profile()) {
825            for lvl in default_levels() {
826                if !lvl.admits(&profile) {
827                    continue;
828                }
829                for (i, cap) in profile.capabilities.iter().enumerate() {
830                    for lowered in lowered_variants(cap) {
831                        let mut lowered_profile = profile.clone();
832                        lowered_profile.capabilities[i] = lowered;
833                        prop_assert!(
834                            lvl.admits(&lowered_profile),
835                            "{} broke facet-monotonicity",
836                            lvl.name,
837                        );
838                    }
839                }
840            }
841        }
842    }
843
844    // execute·locus is the single facet `lowered_variants` skips (the executor-origin band), so the
845    // proptest above cannot see a non-monotone execute band added to another level. This guard closes
846    // that gap DIRECTLY at the level: for every level, admitting execute at a locus must admit it one
847    // rung lower — checking the level as a whole, so a floored clause that a WIDER clause covers (a
848    // level that `extend`s developer and re-admits below the band) is correctly monotone. Only the
849    // levels whose executor-origin band is intentionally floored are exempt; a new non-monotone
850    // execute band on any other level fails CLOSED here.
851    #[test]
852    fn execute_locus_is_monotone_except_the_intended_origin_bands() {
853        // Only developer AUTHORS an execute·locus floor (`>= sandbox-scope`: temp/process below are
854        // foreign code). network-admin `extend`s developer and inherits that clause verbatim without
855        // re-admitting below it, so it shares developer's exact band. That is safe by construction:
856        // `extend` only ADDS allow clauses, which only WIDEN the admit set — an extender can fill the
857        // band's floor (local-admin does, via `<= machine`, and is monotone) but can never introduce a
858        // floor worse than the one developer authored. So the shared band is fully pinned by
859        // developer's edge test (`developer_runs_worktree_code_but_not_foreign_or_system`); any level
860        // NOT listed here must be fully monotone in execute·locus, and a new base-level floor fails
861        // closed until it is declared here with its own edge test.
862        let intended: &[&str] = &["developer", "network-admin"];
863        for lvl in default_levels() {
864            if intended.contains(&lvl.name.as_str()) {
865                continue;
866            }
867            let exec_at = |local| {
868                let mut c = Capability::new(Operation::Execute);
869                c.locus.local = local;
870                c.execution.trust = ExecutionTrust::CallerFile;
871                Profile::of(vec![c])
872            };
873            for local in LocalLocus::all() {
874                let Some(lower) = predecessor(*local) else { continue };
875                if lvl.admits(&exec_at(*local)) {
876                    assert!(
877                        lvl.admits(&exec_at(lower)),
878                        "level `{}`: admits execute at {:?} but denies it one rung lower at {:?} — a \
879                         non-monotone execute band. If deliberate, add `{}` to `intended` WITH an edge \
880                         test; otherwise widen or remove the floor.",
881                        lvl.name,
882                        local,
883                        lower,
884                        lvl.name,
885                    );
886                }
887            }
888        }
889    }
890
891    // ── round-trip: Level -> TOML -> Level is identity ──────────────────────────────
892    //
893    // The reverse of build_clause: a compiled clause serializes back to equivalent
894    // TOML that recompiles to the same clause. Mirrors every operator the parser
895    // produces (<=, >=, exact, and the two-sided range).
896
897    fn bound_str<T: FacetTerm>(b: OrdBound<T>) -> String {
898        match (b.min, b.max) {
899            (Some(lo), Some(hi)) if lo == hi => lo.as_str().to_string(),
900            (Some(lo), Some(hi)) => format!(">= {}, <= {}", lo.as_str(), hi.as_str()),
901            (None, Some(hi)) => format!("<= {}", hi.as_str()),
902            (Some(lo), None) => format!(">= {}", lo.as_str()),
903            (None, None) => panic!("empty bound has no representation"),
904        }
905    }
906
907    fn opt_bound_str<T: FacetTerm>(b: Option<OrdBound<T>>) -> Option<String> {
908        b.map(bound_str)
909    }
910
911    fn set_str<T: FacetTerm>(v: &[T]) -> StringOrVec {
912        StringOrVec::Many(v.iter().map(|t| t.as_str().to_string()).collect())
913    }
914
915    fn clause_to_toml(c: &Clause) -> TomlClause {
916        let locus =
917            (c.local_locus.is_some() || c.remote_reach.is_some() || c.remote_binding.is_some() || c.provenance.is_some()).then(|| {
918                TomlLocus {
919                    local: opt_bound_str(c.local_locus),
920                    remote: opt_bound_str(c.remote_reach),
921                    binding: c.remote_binding.as_deref().map(set_str),
922                    provenance: opt_bound_str(c.provenance),
923                }
924            });
925        let persistence =
926            (c.persistence_level.is_some() || c.trigger_escape.is_some() || c.trigger_kind.is_some()).then(|| TomlPersistence {
927                level: opt_bound_str(c.persistence_level),
928                trigger: (c.trigger_escape.is_some() || c.trigger_kind.is_some())
929                    .then(|| TomlTrigger { escape: opt_bound_str(c.trigger_escape), kind: c.trigger_kind.as_deref().map(set_str) }),
930            });
931        let disclosure =
932            (c.disclosure_audience.is_some() || c.disclosure_channel.is_some() || c.disclosure_principal.is_some()).then(|| {
933                TomlDisclosure {
934                    audience: opt_bound_str(c.disclosure_audience),
935                    channel: c.disclosure_channel.as_deref().map(set_str),
936                    principal: c.disclosure_principal.as_deref().map(set_str),
937                }
938            });
939        let secret = (c.secret_level.is_some() || c.secret_channel.is_some() || c.secret_principal.is_some()).then(|| TomlSecret {
940            level: opt_bound_str(c.secret_level),
941            channel: c.secret_channel.as_deref().map(set_str),
942            principal: c.secret_principal.as_deref().map(set_str),
943        });
944        let network = (c.net_direction.is_some() || c.net_destination.is_some() || c.net_payload.is_some()).then(|| TomlNetwork {
945            direction: opt_bound_str(c.net_direction),
946            destination: opt_bound_str(c.net_destination),
947            payload: opt_bound_str(c.net_payload),
948        });
949        let supply_chain = (c.supply_source.is_some() || c.pinning.is_some() || c.exec_surface.is_some()).then(|| TomlSupplyChain {
950            source: c.supply_source.as_deref().map(set_str),
951            pinning: opt_bound_str(c.pinning),
952            exec_surface: c.exec_surface.as_deref().map(set_str),
953        });
954        TomlClause {
955            operation: c.operation.as_deref().map(set_str),
956            locus,
957            scale: opt_bound_str(c.scale),
958            retrieval: opt_bound_str(c.retrieval),
959            authority: opt_bound_str(c.authority),
960            isolation: opt_bound_str(c.isolation),
961            reversibility: opt_bound_str(c.reversibility),
962            persistence,
963            disclosure,
964            secret,
965            network,
966            execution: opt_bound_str(c.execution_trust),
967            supply_chain,
968            cost: opt_bound_str(c.cost),
969        }
970    }
971
972    fn round_trip(levels: &[Level]) -> Vec<Level> {
973        let level = levels
974            .iter()
975            .map(|l| {
976                let tl = TomlLevel {
977                    extends: None,
978                    allow: l.allow.iter().map(clause_to_toml).collect(),
979                    deny: l.deny.iter().map(clause_to_toml).collect(),
980                };
981                (l.name.clone(), tl)
982            })
983            .collect();
984        let source = toml::to_string(&TomlLevelSet { level }).expect("serialize");
985        build_level_set(&source).expect("re-parse serialized levels")
986    }
987
988    fn assert_round_trips(levels: &[Level]) {
989        let round = round_trip(levels);
990        for original in levels {
991            let back = round.iter().find(|l| l.name == original.name).expect("level survives");
992            assert_eq!(original.allow, back.allow, "{} allow clauses", original.name);
993            assert_eq!(original.deny, back.deny, "{} deny clauses", original.name);
994        }
995    }
996
997    #[test]
998    fn authored_levels_round_trip() {
999        assert_round_trips(default_levels());
1000    }
1001
1002    #[test]
1003    fn every_facet_round_trips() {
1004        // a kitchen-sink level exercising every reverse-conversion branch
1005        let src = r#"
1006            [level.sink]
1007            [[level.sink.allow]]
1008            operation = ["observe", "create", "destroy"]
1009            locus = { local = "<= machine", remote = "<= fixed", binding = ["pinned", "ambient"] }
1010            scale = "<= bounded"
1011            authority = "<= root"
1012            isolation = "<= vm"
1013            reversibility = "<= effortful"
1014            persistence = { level = "<= installing", trigger = { escape = "<= boot", kind = ["clock", "event"] } }
1015            disclosure = { audience = "<= public", channel = ["filesystem", "network"], principal = ["own"] }
1016            secret = { level = ">= reads", channel = ["credential-store"], principal = ["cross"] }
1017            network = { direction = "<= outbound", destination = "<= arbitrary", payload = "<= sends-host-data" }
1018            execution = "<= network-sourced"
1019            supply_chain = { source = ["public-registry", "signed-repo"], pinning = ">= version", exec_surface = ["build-script", "install-hook"] }
1020            cost = "<= quota"
1021            [[level.sink.deny]]
1022            operation = ["destroy"]
1023            reversibility = ">= irreversible"
1024        "#;
1025        let levels = build_level_set(src).expect("compiles");
1026        assert_round_trips(&levels);
1027    }
1028}