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