Skip to main content

sim_lib_core/read_eval/
decision.rs

1//! Read-eval decision records and ledger projection.
2
3use std::sync::{Arc, Mutex, MutexGuard};
4
5use sim_kernel::{
6    CapabilityName, CapabilitySet, Cx, Datum, DatumStore, Error, Event, EventKind, EventLedger,
7    Expr, Ref, Result, Symbol,
8};
9
10use super::RequestOrigin;
11
12/// The outcome recorded for one explicit read-eval admission request.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub enum ReadEvalOutcome {
15    /// The request passed every gate and its shape-checked result was admitted.
16    Admitted,
17    /// The trusted read policy did not grant `read-eval`.
18    CapDenied,
19    /// The read policy was untrusted for `read-eval`.
20    TrustDenied,
21    /// The caller lacked a capability listed in `requires`.
22    MissingPower,
23    /// The request source could not be decoded through the named codec.
24    DecodeFailed,
25    /// The decoded or prebuilt expression failed during evaluation.
26    EvalFailed,
27    /// The evaluated result failed the expected shape.
28    ShapeDenied,
29    /// The shape check failed before it could report a match result.
30    ShapeError,
31}
32
33impl ReadEvalOutcome {
34    fn as_symbol(&self) -> Symbol {
35        match self {
36            Self::Admitted => Symbol::new("admitted"),
37            Self::CapDenied => Symbol::new("cap-denied"),
38            Self::TrustDenied => Symbol::new("trust-denied"),
39            Self::MissingPower => Symbol::new("missing-power"),
40            Self::DecodeFailed => Symbol::new("decode-failed"),
41            Self::EvalFailed => Symbol::new("eval-failed"),
42            Self::ShapeDenied => Symbol::new("shape-denied"),
43            Self::ShapeError => Symbol::new("shape-error"),
44        }
45    }
46
47    fn from_symbol(symbol: &Symbol) -> Result<Self> {
48        match symbol.name.as_ref() {
49            "admitted" => Ok(Self::Admitted),
50            "cap-denied" => Ok(Self::CapDenied),
51            "trust-denied" => Ok(Self::TrustDenied),
52            "missing-power" => Ok(Self::MissingPower),
53            "decode-failed" => Ok(Self::DecodeFailed),
54            "eval-failed" => Ok(Self::EvalFailed),
55            "shape-denied" => Ok(Self::ShapeDenied),
56            "shape-error" => Ok(Self::ShapeError),
57            other => Err(Error::Eval(format!(
58                "unknown read-eval decision outcome {other}"
59            ))),
60        }
61    }
62}
63
64/// Data recorded for one explicit read-eval admission decision.
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct ReadEvalDecision {
67    /// Open origin data describing who asked for eval.
68    pub origin: RequestOrigin,
69    /// Codec symbol used to decode the request source.
70    pub codec: Symbol,
71    /// Symbol naming the expected shape, when the shape exposes one.
72    pub expected_shape: Option<Symbol>,
73    /// Caller powers that were required before eval could run.
74    pub requires: Vec<CapabilityName>,
75    /// Maximum powers requested by the eval body (`allow`).
76    pub requested: Vec<CapabilityName>,
77    /// Powers that actually ran after diminishment.
78    pub active: Vec<CapabilityName>,
79    /// The gate outcome.
80    pub outcome: ReadEvalOutcome,
81}
82
83/// Returns the default run reference used by the read-eval decision ledger.
84pub fn read_eval_decision_run() -> Ref {
85    Ref::Symbol(Symbol::qualified("read-eval", "decisions"))
86}
87
88#[derive(Clone)]
89pub(super) struct ReadEvalLedger {
90    events: Arc<Mutex<EventLedger>>,
91    run: Ref,
92}
93
94impl Default for ReadEvalLedger {
95    fn default() -> Self {
96        Self {
97            events: Arc::new(Mutex::new(EventLedger::new())),
98            run: read_eval_decision_run(),
99        }
100    }
101}
102
103impl ReadEvalLedger {
104    pub(super) fn record(&self, cx: &mut Cx, decision: &ReadEvalDecision) -> Result<Event> {
105        let reference = decision_ref(cx, decision)?;
106        self.lock()?
107            .push(self.run.clone(), EventKind::Trace(reference))
108    }
109
110    pub(super) fn events_for_run(&self, run: &Ref) -> Result<Vec<Event>> {
111        Ok(self.lock()?.events_for_run(run).to_vec())
112    }
113
114    pub(super) fn decisions(&self, cx: &Cx) -> Result<Vec<ReadEvalDecision>> {
115        self.decisions_for_run(cx, &self.run)
116    }
117
118    pub(super) fn decisions_for_run(&self, cx: &Cx, run: &Ref) -> Result<Vec<ReadEvalDecision>> {
119        self.events_for_run(run)?
120            .iter()
121            .filter_map(|event| match &event.kind {
122                EventKind::Trace(reference) => Some(decision_from_ref(cx, reference)),
123                _ => None,
124            })
125            .collect()
126    }
127
128    fn lock(&self) -> Result<MutexGuard<'_, EventLedger>> {
129        self.events
130            .lock()
131            .map_err(|_| Error::PoisonedLock("read-eval decision ledger"))
132    }
133}
134
135fn decision_ref(cx: &mut Cx, decision: &ReadEvalDecision) -> Result<Ref> {
136    let id = cx.datum_store_mut().intern(decision_datum(decision))?;
137    Ok(Ref::Content(id))
138}
139
140fn decision_from_ref(cx: &Cx, reference: &Ref) -> Result<ReadEvalDecision> {
141    let Ref::Content(id) = reference else {
142        return Err(Error::Eval(
143            "read-eval decision trace does not reference content".to_owned(),
144        ));
145    };
146    let datum = cx
147        .datum_store()
148        .get(id)?
149        .ok_or_else(|| Error::Eval("read-eval decision content is missing".to_owned()))?;
150    decision_from_datum(datum)
151}
152
153fn decision_datum(decision: &ReadEvalDecision) -> Datum {
154    Datum::Node {
155        tag: decision_tag(),
156        fields: vec![
157            (Symbol::new("origin"), origin_datum(&decision.origin)),
158            (Symbol::new("codec"), Datum::Symbol(decision.codec.clone())),
159            (
160                Symbol::new("expected-shape"),
161                option_symbol_datum(decision.expected_shape.as_ref()),
162            ),
163            (
164                Symbol::new("requires"),
165                capabilities_datum(&decision.requires),
166            ),
167            (
168                Symbol::new("requested"),
169                capabilities_datum(&decision.requested),
170            ),
171            (Symbol::new("active"), capabilities_datum(&decision.active)),
172            (
173                Symbol::new("outcome"),
174                Datum::Symbol(decision.outcome.as_symbol()),
175            ),
176        ],
177    }
178}
179
180fn decision_from_datum(datum: &Datum) -> Result<ReadEvalDecision> {
181    let Datum::Node { tag, fields } = datum else {
182        return Err(Error::Eval(
183            "read-eval decision trace payload must be a datum node".to_owned(),
184        ));
185    };
186    if tag != &decision_tag() {
187        return Err(Error::Eval(
188            "trace payload is not a read-eval decision".to_owned(),
189        ));
190    }
191    Ok(ReadEvalDecision {
192        origin: origin_from_datum(field(fields, "origin")?)?,
193        codec: symbol_field(fields, "codec")?.clone(),
194        expected_shape: option_symbol_from_datum(field(fields, "expected-shape")?)?,
195        requires: capabilities_from_datum(field(fields, "requires")?)?,
196        requested: capabilities_from_datum(field(fields, "requested")?)?,
197        active: capabilities_from_datum(field(fields, "active")?)?,
198        outcome: ReadEvalOutcome::from_symbol(symbol_field(fields, "outcome")?)?,
199    })
200}
201
202fn origin_datum(origin: &RequestOrigin) -> Datum {
203    Datum::Node {
204        tag: Symbol::qualified("read-eval", "origin"),
205        fields: vec![
206            (Symbol::new("tag"), Datum::Symbol(origin.tag.clone())),
207            (
208                Symbol::new("detail"),
209                origin.detail.as_ref().map_or(Datum::Nil, expr_datum_lossy),
210            ),
211        ],
212    }
213}
214
215fn origin_from_datum(datum: &Datum) -> Result<RequestOrigin> {
216    let Datum::Node { tag, fields } = datum else {
217        return Err(Error::Eval(
218            "read-eval decision origin must be a datum node".to_owned(),
219        ));
220    };
221    if tag != &Symbol::qualified("read-eval", "origin") {
222        return Err(Error::Eval(
223            "read-eval decision origin has the wrong tag".to_owned(),
224        ));
225    }
226    let detail = match field(fields, "detail")? {
227        Datum::Nil => None,
228        other => Some(Expr::from(other.clone())),
229    };
230    Ok(RequestOrigin {
231        tag: symbol_field(fields, "tag")?.clone(),
232        detail,
233    })
234}
235
236fn expr_datum_lossy(expr: &Expr) -> Datum {
237    Datum::try_from(expr.clone()).unwrap_or_else(|_| Datum::String(format!("{expr:?}")))
238}
239
240fn capabilities_datum(capabilities: &[CapabilityName]) -> Datum {
241    Datum::Vector(
242        capabilities
243            .iter()
244            .map(|capability| Datum::String(capability.as_str().to_owned()))
245            .collect(),
246    )
247}
248
249fn capabilities_from_datum(datum: &Datum) -> Result<Vec<CapabilityName>> {
250    let Datum::Vector(items) = datum else {
251        return Err(Error::Eval(
252            "read-eval decision capabilities must be a vector".to_owned(),
253        ));
254    };
255    items
256        .iter()
257        .map(|item| match item {
258            Datum::String(name) => Ok(CapabilityName::new(name.clone())),
259            _ => Err(Error::Eval(
260                "read-eval decision capability must be a string".to_owned(),
261            )),
262        })
263        .collect()
264}
265
266fn option_symbol_datum(symbol: Option<&Symbol>) -> Datum {
267    symbol.cloned().map_or(Datum::Nil, Datum::Symbol)
268}
269
270fn option_symbol_from_datum(datum: &Datum) -> Result<Option<Symbol>> {
271    match datum {
272        Datum::Nil => Ok(None),
273        Datum::Symbol(symbol) => Ok(Some(symbol.clone())),
274        _ => Err(Error::Eval(
275            "read-eval decision optional symbol field is malformed".to_owned(),
276        )),
277    }
278}
279
280fn symbol_field<'a>(fields: &'a [(Symbol, Datum)], name: &str) -> Result<&'a Symbol> {
281    match field(fields, name)? {
282        Datum::Symbol(symbol) => Ok(symbol),
283        _ => Err(Error::Eval(format!(
284            "read-eval decision field {name} must be a symbol"
285        ))),
286    }
287}
288
289fn field<'a>(fields: &'a [(Symbol, Datum)], name: &str) -> Result<&'a Datum> {
290    fields
291        .iter()
292        .find_map(|(field, value)| (field.name.as_ref() == name).then_some(value))
293        .ok_or_else(|| Error::Eval(format!("read-eval decision missing {name} field")))
294}
295
296fn decision_tag() -> Symbol {
297    Symbol::qualified("read-eval", "decision")
298}
299
300pub(super) fn decision_from_request(
301    request: &super::ReadEvalRequest,
302    active: &CapabilitySet,
303    outcome: ReadEvalOutcome,
304) -> ReadEvalDecision {
305    ReadEvalDecision {
306        origin: request.origin.clone(),
307        codec: request.codec.clone(),
308        expected_shape: request.expected_shape.symbol(),
309        requires: request.requires.clone(),
310        requested: request.allow.iter().cloned().collect(),
311        active: active.iter().cloned().collect(),
312        outcome,
313    }
314}