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::{fmt, sync::Arc};
7
8use sim_codec::{Input, decode_with_codec};
9use sim_kernel::{
10    AbiVersion, CapabilityName, CapabilitySet, Cx, Datum, Diagnostic, Error, Event, Export, Expr,
11    Lib, LibManifest, LibTarget, Linker, LoadCx, Object, ReadPolicy, Ref, Result, Shape, ShapeId,
12    Symbol, Value, Version, diminish, 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/// Immutable host authority shared by requests that load or evaluate source.
85///
86/// Construction is deliberately available only from trusted Rust code. Source
87/// data has no decoder or read-constructor for this value, and its data
88/// projection omits the trusted read policy.
89#[derive(Clone, PartialEq, Eq)]
90pub struct SourceAuthority {
91    read_policy: ReadPolicy,
92    requires: Vec<CapabilityName>,
93    allow: CapabilitySet,
94}
95
96impl fmt::Debug for SourceAuthority {
97    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98        formatter
99            .debug_struct("SourceAuthority")
100            .field("read_policy", &"<redacted>")
101            .field("requires", &self.requires)
102            .field("allow", &self.allow)
103            .finish()
104    }
105}
106
107impl SourceAuthority {
108    /// Builds authority after checking that its read policy admits explicit
109    /// evaluation. Required powers retain caller order; allowed powers retain
110    /// set semantics.
111    pub fn new(
112        read_policy: ReadPolicy,
113        requires: Vec<CapabilityName>,
114        allow: CapabilitySet,
115    ) -> Result<Self> {
116        read_policy.require(&read_eval_capability())?;
117        Ok(Self {
118            read_policy,
119            requires,
120            allow,
121        })
122    }
123
124    /// Returns the trusted policy governing source decoding.
125    pub fn read_policy(&self) -> &ReadPolicy {
126        &self.read_policy
127    }
128
129    /// Returns the caller powers required before source evaluation.
130    pub fn requires(&self) -> &[CapabilityName] {
131        &self.requires
132    }
133
134    /// Returns the maximum powers allowed during source evaluation.
135    pub fn allow(&self) -> &CapabilitySet {
136        &self.allow
137    }
138
139    /// Projects authority for decision data without exposing read-policy
140    /// trust or capability internals.
141    pub fn decision_datum(&self) -> Datum {
142        Datum::Node {
143            tag: Symbol::qualified("source", "authority"),
144            fields: vec![
145                (
146                    Symbol::new("requires"),
147                    capability_names_datum(self.requires()),
148                ),
149                (
150                    Symbol::new("allow"),
151                    capability_names_datum(self.allow().iter()),
152                ),
153                (
154                    Symbol::new("read-policy"),
155                    Datum::Symbol(Symbol::new("redacted")),
156                ),
157            ],
158        }
159    }
160}
161
162/// A single explicit, host-authorized read-eval admission request.
163pub struct ReadEvalRequest {
164    /// Open origin data describing who asked for eval.
165    pub origin: RequestOrigin,
166    /// Codec symbol used to decode text or bytes sources.
167    pub codec: Symbol,
168    /// Source to decode and evaluate, or an already-decoded expression.
169    pub source: ReadEvalSource,
170    /// Trusted host authority governing source admission and evaluation.
171    pub authority: SourceAuthority,
172    /// Shape the evaluated result must satisfy before it is admitted.
173    pub expected_shape: Arc<dyn Shape>,
174}
175
176impl ReadEvalRequest {
177    /// Builds a request whose source authority is explicit and indivisible.
178    pub fn new(
179        origin: RequestOrigin,
180        codec: Symbol,
181        source: ReadEvalSource,
182        authority: SourceAuthority,
183        expected_shape: Arc<dyn Shape>,
184    ) -> Self {
185        Self {
186            origin,
187            codec,
188            source,
189            authority,
190            expected_shape,
191        }
192    }
193}
194
195// sim-non-citizen(reason = "host admission gate object; explicit request data is not a read-constructor surface", kind = "runtime", descriptor = "")
196/// The one runtime admission gate for explicit diminished read-eval.
197#[derive(Clone, Default)]
198pub struct ReadEvalBroker {
199    ledger: decision::ReadEvalLedger,
200}
201
202/// The value-or-error and the single ledger event produced by one admission.
203pub struct ReadEvalAdmission {
204    /// Evaluation result returned to the caller.
205    pub result: Result<Value>,
206    /// Decision recorded for this admission.
207    pub decision: ReadEvalDecision,
208    /// Exact event carrying `decision` in the broker ledger.
209    pub event: Event,
210}
211
212impl ReadEvalBroker {
213    /// Creates a broker with an empty decision ledger.
214    pub fn new() -> Self {
215        Self::default()
216    }
217
218    /// Admits one explicit read-eval request or fails closed.
219    pub fn admit(&self, cx: &mut Cx, request: ReadEvalRequest) -> Result<Value> {
220        self.admit_with_event(cx, request)?.result
221    }
222
223    /// Admits one request and returns the exact decision event alongside its result.
224    pub fn admit_with_event(
225        &self,
226        cx: &mut Cx,
227        request: ReadEvalRequest,
228    ) -> Result<ReadEvalAdmission> {
229        if let Err(err) = request
230            .authority
231            .read_policy()
232            .require(&read_eval_capability())
233        {
234            let outcome = match err {
235                Error::TrustDenied { .. } => ReadEvalOutcome::TrustDenied,
236                _ => ReadEvalOutcome::CapDenied,
237            };
238            return self.admission(cx, &request, &CapabilitySet::new(), outcome, Err(err));
239        }
240        if let Err(err) = cx.require_all(request.authority.requires()) {
241            return self.admission(
242                cx,
243                &request,
244                &CapabilitySet::new(),
245                ReadEvalOutcome::MissingPower,
246                Err(err),
247            );
248        }
249
250        let active = diminish(cx.capabilities(), request.authority.allow());
251        let expr = match cx.with_capabilities(active.clone(), |cx| {
252            decode_source(
253                cx,
254                &request.codec,
255                request.source.clone(),
256                request.authority.read_policy().clone(),
257            )
258        }) {
259            Ok(expr) => expr,
260            Err(err) => {
261                return self.admission(
262                    cx,
263                    &request,
264                    &active,
265                    ReadEvalOutcome::DecodeFailed,
266                    Err(err),
267                );
268            }
269        };
270        let value = match cx.with_capabilities(active.clone(), |cx| cx.eval_expr(expr)) {
271            Ok(value) => value,
272            Err(err) => {
273                return self.admission(
274                    cx,
275                    &request,
276                    &active,
277                    ReadEvalOutcome::EvalFailed,
278                    Err(err),
279                );
280            }
281        };
282
283        let matched = match request.expected_shape.check_value(cx, value.clone()) {
284            Ok(matched) => matched,
285            Err(err) => {
286                return self.admission(
287                    cx,
288                    &request,
289                    &active,
290                    ReadEvalOutcome::ShapeError,
291                    Err(err),
292                );
293            }
294        };
295        if matched.accepted {
296            return self.admission(cx, &request, &active, ReadEvalOutcome::Admitted, Ok(value));
297        }
298
299        let diagnostics =
300            match shape_diagnostics(cx, request.expected_shape.as_ref(), matched.diagnostics) {
301                Ok(diagnostics) => diagnostics,
302                Err(err) => {
303                    return self.admission(
304                        cx,
305                        &request,
306                        &active,
307                        ReadEvalOutcome::ShapeError,
308                        Err(err),
309                    );
310                }
311            };
312        self.admission(
313            cx,
314            &request,
315            &active,
316            ReadEvalOutcome::ShapeDenied,
317            Err(Error::WrongShape {
318                expected: request.expected_shape.id().unwrap_or(ShapeId(0)),
319                diagnostics,
320            }),
321        )
322    }
323
324    /// Returns read-eval decisions recorded in the broker's default run.
325    pub fn decisions(&self, cx: &Cx) -> Result<Vec<ReadEvalDecision>> {
326        self.ledger.decisions(cx)
327    }
328
329    /// Returns read-eval decisions recorded for `run`.
330    pub fn decisions_for_run(&self, cx: &Cx, run: &Ref) -> Result<Vec<ReadEvalDecision>> {
331        self.ledger.decisions_for_run(cx, run)
332    }
333
334    /// Returns raw ledger events recorded for `run`.
335    pub fn events_for_run(&self, run: &Ref) -> Result<Vec<Event>> {
336        self.ledger.events_for_run(run)
337    }
338
339    fn admission(
340        &self,
341        cx: &mut Cx,
342        request: &ReadEvalRequest,
343        active: &CapabilitySet,
344        outcome: ReadEvalOutcome,
345        result: Result<Value>,
346    ) -> Result<ReadEvalAdmission> {
347        let decision = decision::decision_from_request(request, active, outcome);
348        let event = self.ledger.record(cx, &decision)?;
349        Ok(ReadEvalAdmission {
350            result,
351            decision,
352            event,
353        })
354    }
355}
356
357/// Reusable policy for dynamic source evaluated through a named codec.
358///
359/// The policy fixes only the source provenance and codec. Authority and the
360/// expected result shape remain explicit inputs to every evaluation, so a
361/// guest-language wrapper cannot accidentally retain or widen either one.
362/// Callers that need origin detail must provide it in [`RequestOrigin`] when
363/// constructing the policy.
364#[derive(Clone)]
365pub struct DynamicSourcePolicy {
366    broker: ReadEvalBroker,
367    codec: Symbol,
368    origin: RequestOrigin,
369}
370
371impl DynamicSourcePolicy {
372    /// Builds a policy with its own decision ledger.
373    pub fn new(codec: Symbol, origin: RequestOrigin) -> Self {
374        Self::with_broker(ReadEvalBroker::new(), codec, origin)
375    }
376
377    /// Builds a policy over an existing broker.
378    ///
379    /// Cloned brokers share their ledger, allowing several origin/codec
380    /// policies to expose one ordered decision stream.
381    pub fn with_broker(broker: ReadEvalBroker, codec: Symbol, origin: RequestOrigin) -> Self {
382        Self {
383            broker,
384            codec,
385            origin,
386        }
387    }
388
389    /// Evaluates text decoded through this policy's codec.
390    pub fn evaluate_text(
391        &self,
392        cx: &mut Cx,
393        text: impl Into<String>,
394        authority: SourceAuthority,
395        expected_shape: Arc<dyn Shape>,
396    ) -> Result<Value> {
397        self.evaluate(
398            cx,
399            ReadEvalSource::Text(text.into()),
400            authority,
401            expected_shape,
402        )
403    }
404
405    /// Evaluates bytes decoded through this policy's codec.
406    pub fn evaluate_bytes(
407        &self,
408        cx: &mut Cx,
409        bytes: impl Into<Vec<u8>>,
410        authority: SourceAuthority,
411        expected_shape: Arc<dyn Shape>,
412    ) -> Result<Value> {
413        self.evaluate(
414            cx,
415            ReadEvalSource::Bytes(bytes.into()),
416            authority,
417            expected_shape,
418        )
419    }
420
421    /// Evaluates an already-decoded expression through the same admission gate.
422    pub fn evaluate_expr(
423        &self,
424        cx: &mut Cx,
425        expr: Expr,
426        authority: SourceAuthority,
427        expected_shape: Arc<dyn Shape>,
428    ) -> Result<Value> {
429        self.evaluate(cx, ReadEvalSource::Expr(expr), authority, expected_shape)
430    }
431
432    /// Evaluates any supported source form through the shared broker law.
433    pub fn evaluate(
434        &self,
435        cx: &mut Cx,
436        source: ReadEvalSource,
437        authority: SourceAuthority,
438        expected_shape: Arc<dyn Shape>,
439    ) -> Result<Value> {
440        self.broker.admit(
441            cx,
442            ReadEvalRequest::new(
443                self.origin.clone(),
444                self.codec.clone(),
445                source,
446                authority,
447                expected_shape,
448            ),
449        )
450    }
451
452    /// Returns decisions recorded in the policy's default run.
453    pub fn decisions(&self, cx: &Cx) -> Result<Vec<ReadEvalDecision>> {
454        self.broker.decisions(cx)
455    }
456
457    /// Returns decisions recorded for `run`.
458    pub fn decisions_for_run(&self, cx: &Cx, run: &Ref) -> Result<Vec<ReadEvalDecision>> {
459        self.broker.decisions_for_run(cx, run)
460    }
461
462    /// Returns raw ledger events recorded for `run`.
463    pub fn events_for_run(&self, run: &Ref) -> Result<Vec<Event>> {
464        self.broker.events_for_run(run)
465    }
466}
467
468impl Object for ReadEvalBroker {
469    fn display(&self, _cx: &mut Cx) -> Result<String> {
470        Ok("#<read-eval-broker>".to_owned())
471    }
472
473    fn as_any(&self) -> &dyn std::any::Any {
474        self
475    }
476}
477
478impl sim_kernel::ObjectCompat for ReadEvalBroker {
479    fn class(&self, cx: &mut Cx) -> Result<sim_kernel::ClassRef> {
480        cx.factory().class_stub(
481            sim_kernel::ClassId(0),
482            Symbol::qualified("read-eval", "Broker"),
483        )
484    }
485}
486
487/// Returns the broker value symbol exported by [`ReadEvalBrokerLib`].
488pub fn read_eval_broker_symbol() -> Symbol {
489    Symbol::qualified("read-eval", "broker")
490}
491
492/// Returns the manifest id for the read-eval broker library.
493pub fn read_eval_broker_lib_id() -> Symbol {
494    Symbol::qualified("sim", "read-eval-broker")
495}
496
497/// Loadable library that registers the read-eval broker value.
498pub struct ReadEvalBrokerLib;
499
500impl Lib for ReadEvalBrokerLib {
501    fn manifest(&self) -> LibManifest {
502        LibManifest {
503            id: read_eval_broker_lib_id(),
504            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
505            abi: AbiVersion { major: 0, minor: 1 },
506            target: LibTarget::HostRegistered,
507            requires: Vec::new(),
508            capabilities: Vec::new(),
509            exports: vec![Export::Value {
510                symbol: read_eval_broker_symbol(),
511            }],
512        }
513    }
514
515    fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
516        linker.value(
517            read_eval_broker_symbol(),
518            cx.factory().opaque(Arc::new(ReadEvalBroker::new()))?,
519        )?;
520        Ok(())
521    }
522}
523
524/// Installs the read-eval broker library if it is not already loaded.
525pub fn install_read_eval_broker(cx: &mut Cx) -> Result<bool> {
526    crate::install_once(cx, &ReadEvalBrokerLib)
527}
528
529fn decode_source(
530    cx: &mut Cx,
531    codec: &Symbol,
532    source: ReadEvalSource,
533    read_policy: ReadPolicy,
534) -> Result<Expr> {
535    match source {
536        ReadEvalSource::Text(text) => decode_with_codec(cx, codec, Input::Text(text), read_policy),
537        ReadEvalSource::Bytes(bytes) => {
538            decode_with_codec(cx, codec, Input::Bytes(bytes), read_policy)
539        }
540        ReadEvalSource::Expr(expr) => Ok(expr),
541    }
542}
543
544fn capability_names_datum<'a>(capabilities: impl IntoIterator<Item = &'a CapabilityName>) -> Datum {
545    Datum::Vector(
546        capabilities
547            .into_iter()
548            .map(|capability| Datum::String(capability.as_str().to_owned()))
549            .collect(),
550    )
551}
552
553fn shape_diagnostics(
554    cx: &mut Cx,
555    shape: &dyn Shape,
556    diagnostics: Vec<Diagnostic>,
557) -> Result<Vec<Diagnostic>> {
558    if !diagnostics.is_empty() {
559        return Ok(diagnostics);
560    }
561    let expected = match shape.symbol() {
562        Some(symbol) => symbol.to_string(),
563        None => shape.describe(cx)?.name,
564    };
565    Ok(vec![expected_shape_diagnostic(
566        expected,
567        "read-eval result",
568    )])
569}
570
571#[cfg(test)]
572mod config_tests;
573
574#[cfg(test)]
575mod ledger_tests;
576
577#[cfg(test)]
578mod tests;