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