Skip to main content

sim_lib_core/read_eval/
config.rs

1//! Config data-node adapter for explicit read-eval.
2
3use std::{collections::BTreeMap, sync::Arc};
4
5use sim_kernel::{
6    CapabilityName, CapabilitySet, Cx, Error, Expr, ReadPolicy, Result, Shape, ShapeId, Symbol,
7    TrustLevel, read_eval_capability,
8};
9use sim_shape::{expected_shape_diagnostic, parse_shape_expr};
10
11use super::{ReadEvalBroker, ReadEvalRequest, ReadEvalSource, RequestOrigin};
12
13/// Host-owned opt-in for realizing explicit config eval nodes.
14///
15/// The policy is constructed by the host, not by config text. A config node can
16/// request a diminished capability set for the eval body, but it cannot create
17/// trust or grant capabilities that the caller does not already hold.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct HostConfigEvalOptIn {
20    read_policy: ReadPolicy,
21}
22
23impl HostConfigEvalOptIn {
24    /// Creates an opt-in from a host-built read policy.
25    pub fn new(read_policy: ReadPolicy) -> Self {
26        Self { read_policy }
27    }
28
29    /// Creates a trusted-source opt-in and grants the read-eval capability to
30    /// the read policy.
31    pub fn trusted(capabilities: CapabilitySet) -> Self {
32        Self::new(ReadPolicy {
33            trust: TrustLevel::TrustedSource,
34            capabilities: capabilities.grant(read_eval_capability()),
35        })
36    }
37
38    /// Returns the host-owned read policy used for broker admission.
39    pub fn read_policy(&self) -> &ReadPolicy {
40        &self.read_policy
41    }
42}
43
44/// Parsed data carried by an explicit `config/eval` node.
45#[derive(Clone)]
46pub struct ConfigEvalNode {
47    /// Codec used when the source is text or bytes.
48    pub codec: Symbol,
49    /// Source expression, text, or bytes admitted through the broker.
50    pub source: ReadEvalSource,
51    /// Capabilities the caller must already hold before eval can run.
52    pub requires: Vec<CapabilityName>,
53    /// Maximum capabilities the eval body may run with.
54    pub allow: CapabilitySet,
55    /// Shape the evaluated result must match before it is merged.
56    pub expected_shape: Arc<dyn Shape>,
57}
58
59impl ConfigEvalNode {
60    fn into_request(self, read_policy: ReadPolicy, detail: Expr) -> ReadEvalRequest {
61        ReadEvalRequest {
62            origin: RequestOrigin::with_detail(config_eval_origin_tag(), detail),
63            codec: self.codec,
64            source: self.source,
65            read_policy,
66            requires: self.requires,
67            allow: self.allow,
68            expected_shape: self.expected_shape,
69        }
70    }
71}
72
73/// Returns the explicit config eval node symbol.
74pub fn config_eval_node_symbol() -> Symbol {
75    Symbol::qualified("config", "eval")
76}
77
78/// Returns the origin tag used for config eval broker ledger entries.
79pub fn config_eval_origin_tag() -> Symbol {
80    Symbol::qualified("config", "node")
81}
82
83/// Parses an explicit config eval node from expression data.
84///
85/// The adapter accepts two inert data shapes: the field map stored under a
86/// config table's `config/eval` entry, and a list headed by the `config/eval`
87/// symbol with field pairs. The required fields are `codec`, exactly one of
88/// `source` or `expr`, `requires`, `allow`, and `shape`.
89pub fn parse_config_eval_node(expr: &Expr) -> Result<ConfigEvalNode> {
90    let fields = match expr {
91        Expr::Map(entries) => collect_map_fields(entries)?,
92        Expr::List(items) => collect_list_fields(items)?,
93        _ => {
94            return Err(config_eval_error(
95                "config/eval node must be a field map or headed list",
96            ));
97        }
98    };
99    build_node(fields)
100}
101
102/// Realizes explicit config eval nodes in a decoded config expression.
103///
104/// With no opt-in, the expression is returned unchanged and no node is parsed or
105/// evaluated. With opt-in, `config/eval` entries in maps are admitted through
106/// the broker and their map result is merged into that map. A list headed by
107/// `config/eval` in value position is replaced by its admitted result.
108pub fn realize_config_expr(
109    cx: &mut Cx,
110    broker: &ReadEvalBroker,
111    opt_in: Option<&HostConfigEvalOptIn>,
112    expr: Expr,
113) -> Result<Expr> {
114    let Some(opt_in) = opt_in else {
115        return Ok(expr);
116    };
117    realize_expr_inner(cx, broker, opt_in, expr)
118}
119
120fn realize_expr_inner(
121    cx: &mut Cx,
122    broker: &ReadEvalBroker,
123    opt_in: &HostConfigEvalOptIn,
124    expr: Expr,
125) -> Result<Expr> {
126    match expr {
127        Expr::Map(entries) => realize_map(cx, broker, opt_in, entries),
128        Expr::List(items) if list_head_is_config_eval(&items) => {
129            admit_node_expr(cx, broker, opt_in, Expr::List(items))
130        }
131        Expr::List(items) => items
132            .into_iter()
133            .map(|item| realize_expr_inner(cx, broker, opt_in, item))
134            .collect::<Result<Vec<_>>>()
135            .map(Expr::List),
136        Expr::Vector(items) => items
137            .into_iter()
138            .map(|item| realize_expr_inner(cx, broker, opt_in, item))
139            .collect::<Result<Vec<_>>>()
140            .map(Expr::Vector),
141        Expr::Set(items) => items
142            .into_iter()
143            .map(|item| realize_expr_inner(cx, broker, opt_in, item))
144            .collect::<Result<Vec<_>>>()
145            .map(Expr::Set),
146        other => Ok(other),
147    }
148}
149
150fn realize_map(
151    cx: &mut Cx,
152    broker: &ReadEvalBroker,
153    opt_in: &HostConfigEvalOptIn,
154    entries: Vec<(Expr, Expr)>,
155) -> Result<Expr> {
156    let mut realized = Vec::<(Expr, Expr)>::new();
157    for (key, value) in entries {
158        if key_is_config_eval(&key) {
159            let merged = admit_node_expr(cx, broker, opt_in, value)?;
160            let Expr::Map(entries) = merged else {
161                return Err(config_eval_error(
162                    "config/eval map entry must produce a map result",
163                ));
164            };
165            for entry in entries {
166                push_unique_entry(&mut realized, entry)?;
167            }
168        } else {
169            let value = realize_expr_inner(cx, broker, opt_in, value)?;
170            push_unique_entry(&mut realized, (key, value))?;
171        }
172    }
173    Ok(Expr::Map(realized))
174}
175
176fn admit_node_expr(
177    cx: &mut Cx,
178    broker: &ReadEvalBroker,
179    opt_in: &HostConfigEvalOptIn,
180    node_expr: Expr,
181) -> Result<Expr> {
182    let node = parse_config_eval_node(&node_expr)?;
183    let value = broker.admit(
184        cx,
185        node.into_request(opt_in.read_policy().clone(), node_expr),
186    )?;
187    value.object().as_expr(cx)
188}
189
190fn push_unique_entry(target: &mut Vec<(Expr, Expr)>, entry: (Expr, Expr)) -> Result<()> {
191    if target.iter().any(|(key, _)| key == &entry.0) {
192        return Err(config_eval_error(
193            "config/eval merge produced a duplicate key",
194        ));
195    }
196    target.push(entry);
197    Ok(())
198}
199
200fn build_node(fields: BTreeMap<String, &Expr>) -> Result<ConfigEvalNode> {
201    let codec = parse_symbol(required_field(&fields, "codec")?, "codec")?;
202    let requires = parse_capability_list(required_field(&fields, "requires")?, "requires")?;
203    let allow = parse_capability_set(required_field(&fields, "allow")?, "allow")?;
204    let expected_shape = parse_shape_field(required_field(&fields, "shape")?)?;
205    let source = match (fields.get("source"), fields.get("expr")) {
206        (Some(_), Some(_)) => {
207            return Err(config_eval_error(
208                "config/eval node must carry only one of source or expr",
209            ));
210        }
211        (Some(source), None) => parse_source_field(source)?,
212        (None, Some(expr)) => ReadEvalSource::Expr((*expr).clone()),
213        (None, None) => {
214            return Err(config_eval_error(
215                "config/eval node requires source or expr",
216            ));
217        }
218    };
219
220    Ok(ConfigEvalNode {
221        codec,
222        source,
223        requires,
224        allow,
225        expected_shape,
226    })
227}
228
229fn collect_map_fields(entries: &[(Expr, Expr)]) -> Result<BTreeMap<String, &Expr>> {
230    let mut fields = BTreeMap::new();
231    for (key, value) in entries {
232        let name = field_name(key)?;
233        if fields.insert(name.clone(), value).is_some() {
234            return Err(config_eval_error(format!(
235                "config/eval node repeats field {name:?}"
236            )));
237        }
238    }
239    Ok(fields)
240}
241
242fn collect_list_fields(items: &[Expr]) -> Result<BTreeMap<String, &Expr>> {
243    let Some((head, tail)) = items.split_first() else {
244        return Err(config_eval_error("config/eval list cannot be empty"));
245    };
246    if !expr_is_config_eval(head) {
247        return Err(config_eval_error("config/eval list has the wrong head"));
248    }
249    let tail = if matches!(tail.first(), Some(version) if expr_text(version) == Some("v1")) {
250        &tail[1..]
251    } else {
252        tail
253    };
254    if tail.len() % 2 != 0 {
255        return Err(config_eval_error(
256            "config/eval list fields must be key/value pairs",
257        ));
258    }
259    let mut fields = BTreeMap::new();
260    for pair in tail.chunks_exact(2) {
261        let name = field_name(&pair[0])?;
262        if fields.insert(name.clone(), &pair[1]).is_some() {
263            return Err(config_eval_error(format!(
264                "config/eval node repeats field {name:?}"
265            )));
266        }
267    }
268    Ok(fields)
269}
270
271fn required_field<'a>(fields: &'a BTreeMap<String, &Expr>, name: &str) -> Result<&'a Expr> {
272    fields
273        .get(name)
274        .copied()
275        .ok_or_else(|| config_eval_error(format!("config/eval node requires {name:?}")))
276}
277
278fn parse_source_field(expr: &Expr) -> Result<ReadEvalSource> {
279    match expr {
280        Expr::String(text) => Ok(ReadEvalSource::Text(text.clone())),
281        Expr::Bytes(bytes) => Ok(ReadEvalSource::Bytes(bytes.clone())),
282        _ => Err(config_eval_error("source must be a string or bytes value")),
283    }
284}
285
286fn parse_shape_field(expr: &Expr) -> Result<Arc<dyn Shape>> {
287    match expr {
288        Expr::String(text) => parse_shape_expr(&Expr::Symbol(parse_symbol_text(text))),
289        Expr::Symbol(_) | Expr::List(_) => parse_shape_expr(expr),
290        _ => Err(Error::WrongShape {
291            expected: ShapeId(0),
292            diagnostics: vec![expected_shape_diagnostic(
293                "shape expression",
294                "config/eval shape",
295            )],
296        }),
297    }
298}
299
300fn parse_capability_list(expr: &Expr, field: &str) -> Result<Vec<CapabilityName>> {
301    capability_items(expr, field)?
302        .iter()
303        .map(parse_capability)
304        .collect()
305}
306
307fn parse_capability_set(expr: &Expr, field: &str) -> Result<CapabilitySet> {
308    parse_capability_list(expr, field).map(|capabilities| {
309        capabilities
310            .into_iter()
311            .fold(CapabilitySet::new(), CapabilitySet::grant)
312    })
313}
314
315fn capability_items<'a>(expr: &'a Expr, field: &str) -> Result<&'a [Expr]> {
316    match expr {
317        Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => Ok(items),
318        _ => Err(config_eval_error(format!(
319            "{field} must be a list of capability names"
320        ))),
321    }
322}
323
324fn parse_capability(expr: &Expr) -> Result<CapabilityName> {
325    let text = match expr {
326        Expr::String(text) => text.clone(),
327        Expr::Symbol(symbol) => symbol_text(symbol),
328        _ => {
329            return Err(config_eval_error(
330                "capability entries must be strings or symbols",
331            ));
332        }
333    };
334    let text = strip_field_prefix(&text);
335    let text = text.strip_prefix("capability/").unwrap_or(text).to_owned();
336    Ok(CapabilityName::new(text))
337}
338
339fn parse_symbol(expr: &Expr, field: &str) -> Result<Symbol> {
340    match expr {
341        Expr::String(text) => Ok(parse_symbol_text(text)),
342        Expr::Symbol(symbol) => Ok(symbol.clone()),
343        _ => Err(config_eval_error(format!(
344            "{field} must be a string or symbol"
345        ))),
346    }
347}
348
349fn parse_symbol_text(text: &str) -> Symbol {
350    let text = strip_field_prefix(text);
351    match text.split_once('/') {
352        Some((namespace, name)) if !namespace.is_empty() && !name.is_empty() => {
353            Symbol::qualified(namespace.to_owned(), name.to_owned())
354        }
355        _ => Symbol::new(text.to_owned()),
356    }
357}
358
359fn field_name(expr: &Expr) -> Result<String> {
360    match expr {
361        Expr::String(text) => Ok(strip_field_prefix(text).to_owned()),
362        Expr::Symbol(symbol) => Ok(strip_field_prefix(&symbol_text(symbol)).to_owned()),
363        _ => Err(config_eval_error(
364            "config/eval field names must be strings or symbols",
365        )),
366    }
367}
368
369fn list_head_is_config_eval(items: &[Expr]) -> bool {
370    items.first().is_some_and(expr_is_config_eval)
371}
372
373fn key_is_config_eval(expr: &Expr) -> bool {
374    expr_is_config_eval(expr)
375}
376
377fn expr_is_config_eval(expr: &Expr) -> bool {
378    match expr {
379        Expr::Symbol(symbol) => symbol_is_config_eval(symbol),
380        Expr::String(text) => text == "config/eval",
381        _ => false,
382    }
383}
384
385fn symbol_is_config_eval(symbol: &Symbol) -> bool {
386    symbol == &config_eval_node_symbol()
387        || (symbol.namespace.is_none() && symbol.name.as_ref() == "config/eval")
388}
389
390fn expr_text(expr: &Expr) -> Option<&str> {
391    match expr {
392        Expr::String(text) => Some(text),
393        Expr::Symbol(symbol) if symbol.namespace.is_none() => Some(symbol.name.as_ref()),
394        _ => None,
395    }
396}
397
398fn symbol_text(symbol: &Symbol) -> String {
399    match &symbol.namespace {
400        Some(namespace) => format!("{namespace}/{}", symbol.name),
401        None => symbol.name.to_string(),
402    }
403}
404
405fn strip_field_prefix(text: &str) -> &str {
406    text.strip_prefix(':').unwrap_or(text)
407}
408
409fn config_eval_error(message: impl Into<String>) -> Error {
410    Error::domain_error(
411        config_eval_node_symbol(),
412        Symbol::qualified("config", "eval-node"),
413        message,
414    )
415}