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