Skip to main content

sim_lib_core/
read_eval.rs

1//! Explicit read-eval admission through one diminished gate.
2
3mod config;
4mod decision;
5
6use std::sync::Arc;
7
8use sim_codec::{Input, decode_with_codec};
9use sim_kernel::{
10    AbiVersion, CapabilityName, CapabilitySet, Cx, Diagnostic, Error, Event, Export, Expr, Lib,
11    LibManifest, LibTarget, Linker, LoadCx, Object, ReadPolicy, Ref, Result, Shape, ShapeId,
12    Symbol, Value, Version, read_eval_capability,
13};
14use sim_shape::expected_shape_diagnostic;
15
16pub use config::{
17    ConfigEvalNode, HostConfigEvalOptIn, config_eval_node_symbol, config_eval_origin_tag,
18    parse_config_eval_node, realize_config_expr,
19};
20pub use decision::{ReadEvalDecision, ReadEvalOutcome, read_eval_decision_run};
21
22#[cfg(test)]
23trait GrantOutcome {
24    fn expect_granted(self);
25}
26
27#[cfg(test)]
28impl GrantOutcome for () {
29    fn expect_granted(self) {}
30}
31
32#[cfg(test)]
33impl GrantOutcome for Result<()> {
34    fn expect_granted(self) {
35        self.unwrap();
36    }
37}
38
39#[cfg(test)]
40macro_rules! expect_granted {
41    ($grant:expr) => {{
42        #[allow(clippy::let_unit_value)]
43        let grant_result = $grant;
44        #[allow(clippy::unit_arg)]
45        grant_result.expect_granted();
46    }};
47}
48
49/// Open origin data for an explicit read-eval request.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct RequestOrigin {
52    /// Open tag for the request origin, such as `config/node` or `repl`.
53    pub tag: Symbol,
54    /// Optional origin detail carried as data for callers and ledger records.
55    pub detail: Option<Expr>,
56}
57
58impl RequestOrigin {
59    /// Builds origin data from a tag with no detail.
60    pub fn new(tag: Symbol) -> Self {
61        Self { tag, detail: None }
62    }
63
64    /// Builds origin data from a tag and detail expression.
65    pub fn with_detail(tag: Symbol, detail: Expr) -> Self {
66        Self {
67            tag,
68            detail: Some(detail),
69        }
70    }
71}
72
73/// Source accepted by the read-eval broker.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub enum ReadEvalSource {
76    /// Decode this text through the request codec before evaluation.
77    Text(String),
78    /// Decode these bytes through the request codec before evaluation.
79    Bytes(Vec<u8>),
80    /// Evaluate an already-decoded expression.
81    Expr(Expr),
82}
83
84/// A single explicit, host-authorized read-eval admission request.
85pub struct ReadEvalRequest {
86    /// Open origin data describing who asked for eval.
87    pub origin: RequestOrigin,
88    /// Codec symbol used to decode text or bytes sources.
89    pub codec: Symbol,
90    /// Source to decode and evaluate, or an already-decoded expression.
91    pub source: ReadEvalSource,
92    /// Trusted host-built read policy; never derive this from request text.
93    pub read_policy: ReadPolicy,
94    /// Capabilities the caller must already hold before eval can run.
95    pub requires: Vec<CapabilityName>,
96    /// Maximum powers the request allows the eval body to run with.
97    pub allow: CapabilitySet,
98    /// Shape the evaluated result must satisfy before it is admitted.
99    pub expected_shape: Arc<dyn Shape>,
100}
101
102// sim-non-citizen(reason = "host admission gate object; explicit request data is not a read-constructor surface", kind = "runtime", descriptor = "")
103/// The one runtime admission gate for explicit diminished read-eval.
104#[derive(Clone, Default)]
105pub struct ReadEvalBroker {
106    ledger: decision::ReadEvalLedger,
107}
108
109impl ReadEvalBroker {
110    /// Creates a broker with an empty decision ledger.
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    /// Admits one explicit read-eval request or fails closed.
116    pub fn admit(&self, cx: &mut Cx, request: ReadEvalRequest) -> Result<Value> {
117        if let Err(err) = request.read_policy.require(&read_eval_capability()) {
118            let outcome = match err {
119                Error::TrustDenied { .. } => ReadEvalOutcome::TrustDenied,
120                _ => ReadEvalOutcome::CapDenied,
121            };
122            self.record(cx, &request, &CapabilitySet::new(), outcome)?;
123            return Err(err);
124        }
125        if let Err(err) = cx.require_all(&request.requires) {
126            self.record(
127                cx,
128                &request,
129                &CapabilitySet::new(),
130                ReadEvalOutcome::MissingPower,
131            )?;
132            return Err(err);
133        }
134
135        let active = diminish_capabilities(cx.capabilities(), &request.allow);
136        let expr = match cx.with_capabilities(active.clone(), |cx| {
137            decode_source(
138                cx,
139                &request.codec,
140                request.source.clone(),
141                request.read_policy.clone(),
142            )
143        }) {
144            Ok(expr) => expr,
145            Err(err) => {
146                self.record(cx, &request, &active, ReadEvalOutcome::DecodeFailed)?;
147                return Err(err);
148            }
149        };
150        let value = match cx.with_capabilities(active.clone(), |cx| cx.eval_expr(expr)) {
151            Ok(value) => value,
152            Err(err) => {
153                self.record(cx, &request, &active, ReadEvalOutcome::EvalFailed)?;
154                return Err(err);
155            }
156        };
157
158        let matched = match request.expected_shape.check_value(cx, value.clone()) {
159            Ok(matched) => matched,
160            Err(err) => {
161                self.record(cx, &request, &active, ReadEvalOutcome::ShapeError)?;
162                return Err(err);
163            }
164        };
165        if matched.accepted {
166            self.record(cx, &request, &active, ReadEvalOutcome::Admitted)?;
167            return Ok(value);
168        }
169
170        let diagnostics =
171            match shape_diagnostics(cx, request.expected_shape.as_ref(), matched.diagnostics) {
172                Ok(diagnostics) => diagnostics,
173                Err(err) => {
174                    self.record(cx, &request, &active, ReadEvalOutcome::ShapeError)?;
175                    return Err(err);
176                }
177            };
178        self.record(cx, &request, &active, ReadEvalOutcome::ShapeDenied)?;
179        Err(Error::WrongShape {
180            expected: request.expected_shape.id().unwrap_or(ShapeId(0)),
181            diagnostics,
182        })
183    }
184
185    /// Returns read-eval decisions recorded in the broker's default run.
186    pub fn decisions(&self, cx: &Cx) -> Result<Vec<ReadEvalDecision>> {
187        self.ledger.decisions(cx)
188    }
189
190    /// Returns read-eval decisions recorded for `run`.
191    pub fn decisions_for_run(&self, cx: &Cx, run: &Ref) -> Result<Vec<ReadEvalDecision>> {
192        self.ledger.decisions_for_run(cx, run)
193    }
194
195    /// Returns raw ledger events recorded for `run`.
196    pub fn events_for_run(&self, run: &Ref) -> Result<Vec<Event>> {
197        self.ledger.events_for_run(run)
198    }
199
200    fn record(
201        &self,
202        cx: &mut Cx,
203        request: &ReadEvalRequest,
204        active: &CapabilitySet,
205        outcome: ReadEvalOutcome,
206    ) -> Result<Event> {
207        let decision = decision::decision_from_request(request, active, outcome);
208        self.ledger.record(cx, &decision)
209    }
210}
211
212impl Object for ReadEvalBroker {
213    fn display(&self, _cx: &mut Cx) -> Result<String> {
214        Ok("#<read-eval-broker>".to_owned())
215    }
216
217    fn as_any(&self) -> &dyn std::any::Any {
218        self
219    }
220}
221
222impl sim_kernel::ObjectCompat for ReadEvalBroker {
223    fn class(&self, cx: &mut Cx) -> Result<sim_kernel::ClassRef> {
224        cx.factory().class_stub(
225            sim_kernel::ClassId(0),
226            Symbol::qualified("read-eval", "Broker"),
227        )
228    }
229}
230
231/// Returns the broker value symbol exported by [`ReadEvalBrokerLib`].
232pub fn read_eval_broker_symbol() -> Symbol {
233    Symbol::qualified("read-eval", "broker")
234}
235
236/// Returns the manifest id for the read-eval broker library.
237pub fn read_eval_broker_lib_id() -> Symbol {
238    Symbol::qualified("sim", "read-eval-broker")
239}
240
241/// Loadable library that registers the read-eval broker value.
242pub struct ReadEvalBrokerLib;
243
244impl Lib for ReadEvalBrokerLib {
245    fn manifest(&self) -> LibManifest {
246        LibManifest {
247            id: read_eval_broker_lib_id(),
248            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
249            abi: AbiVersion { major: 0, minor: 1 },
250            target: LibTarget::HostRegistered,
251            requires: Vec::new(),
252            capabilities: Vec::new(),
253            exports: vec![Export::Value {
254                symbol: read_eval_broker_symbol(),
255            }],
256        }
257    }
258
259    fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
260        linker.value(
261            read_eval_broker_symbol(),
262            cx.factory().opaque(Arc::new(ReadEvalBroker::new()))?,
263        )?;
264        Ok(())
265    }
266}
267
268/// Installs the read-eval broker library if it is not already loaded.
269pub fn install_read_eval_broker(cx: &mut Cx) -> Result<bool> {
270    crate::install_once(cx, &ReadEvalBrokerLib)
271}
272
273fn decode_source(
274    cx: &mut Cx,
275    codec: &Symbol,
276    source: ReadEvalSource,
277    read_policy: ReadPolicy,
278) -> Result<Expr> {
279    match source {
280        ReadEvalSource::Text(text) => decode_with_codec(cx, codec, Input::Text(text), read_policy),
281        ReadEvalSource::Bytes(bytes) => {
282            decode_with_codec(cx, codec, Input::Bytes(bytes), read_policy)
283        }
284        ReadEvalSource::Expr(expr) => Ok(expr),
285    }
286}
287
288fn diminish_capabilities(current: &CapabilitySet, allowed: &CapabilitySet) -> CapabilitySet {
289    current
290        .iter()
291        .filter(|capability| allowed.contains(capability))
292        .cloned()
293        .fold(CapabilitySet::new(), CapabilitySet::grant)
294}
295
296fn shape_diagnostics(
297    cx: &mut Cx,
298    shape: &dyn Shape,
299    diagnostics: Vec<Diagnostic>,
300) -> Result<Vec<Diagnostic>> {
301    if !diagnostics.is_empty() {
302        return Ok(diagnostics);
303    }
304    let expected = match shape.symbol() {
305        Some(symbol) => symbol.to_string(),
306        None => shape.describe(cx)?.name,
307    };
308    Ok(vec![expected_shape_diagnostic(
309        expected,
310        "read-eval result",
311    )])
312}
313
314#[cfg(test)]
315mod config_tests;
316
317#[cfg(test)]
318mod ledger_tests;
319
320#[cfg(test)]
321mod tests;