Skip to main content

sim_lib_bridge/
rx.rs

1use std::collections::BTreeSet;
2use std::sync::Arc;
3
4use sim_codec_bridge::{
5    BridgeBook, BridgePacket, BridgePart, BridgePartSpec, content_id_string, decode_bridge_text,
6    expr_to_packet, packet_content_id,
7};
8use sim_kernel::{CapabilitySet, Cx, Diagnostic, Error, Expr, Result, Symbol, Value};
9use sim_lib_agent_runner_core::{ModelResponse, effective_ceiling, terminal_model_content};
10use sim_shape::{
11    AnyShape, ExactExprShape, ExprKind, ExprKindShape, FieldShape, FieldSpec, MatchScore,
12    OneOfShape, Shape, ShapeDoc, ShapeMatch, check_value_report, shape_value,
13};
14use sim_value::access::field;
15
16use crate::loom_validate::validate_packet_weaves;
17use crate::parent::parents_contain_cid;
18use crate::report::{BridgeObligation, BridgeReport};
19use crate::warrant::verify_warrant;
20
21/// Resolves a packet's declared capability ceiling against the current context.
22pub fn effective_caps(cx: &Cx, packet: &BridgePacket) -> Result<CapabilitySet> {
23    effective_ceiling(cx.capabilities(), &packet.header.ceiling)
24}
25
26/// Checks a BRIDGE packet against the local book and optional parent packet.
27///
28/// A packet checked as a reply must cite the parent content id, send from one
29/// of the parent's recipients, and address exactly the parent's sender. The
30/// same check also validates move legality, part shapes, warrant freshness,
31/// LOOM payloads, and the parent `Return` contract.
32pub fn rx_check(
33    cx: &mut Cx,
34    book: &BridgeBook,
35    packet: &BridgePacket,
36    reply_to: Option<&BridgePacket>,
37) -> Result<BridgeReport> {
38    let mut report = BridgeReport::new(packet_report_cid(packet));
39    check_header_linkage(&mut report, packet, reply_to);
40    check_move(book, &mut report, packet, reply_to);
41    check_parts(cx, book, &mut report, packet)?;
42    for obligation in verify_warrant(cx, book, packet)? {
43        report.obligate(obligation);
44    }
45    validate_packet_weaves(cx, packet, &mut report)?;
46    if let Some(parent) = reply_to {
47        check_parent_return(cx, &mut report, packet, parent)?;
48    }
49    Ok(report)
50}
51
52/// Decodes and checks the last content item of a model response.
53pub fn bridge_rx_response(
54    cx: &mut Cx,
55    book: &BridgeBook,
56    response: &ModelResponse,
57    reply_to: Option<&BridgePacket>,
58) -> Result<(BridgePacket, BridgeReport)> {
59    let text = terminal_bridge_text(response)?;
60    let packet = decode_bridge_text(text, book)?;
61    let report = rx_check(cx, book, &packet, reply_to)?;
62    if report.accepted() {
63        Ok((packet, report))
64    } else {
65        Err(Error::Eval(format!(
66            "bridge rx check failed: {:?}",
67            report.obligations
68        )))
69    }
70}
71
72/// Decodes and checks a model response expression.
73pub fn bridge_rx(
74    cx: &mut Cx,
75    book: &BridgeBook,
76    response: Expr,
77    reply_to: Option<&BridgePacket>,
78) -> Result<(BridgePacket, BridgeReport)> {
79    let response = ModelResponse::try_from(response)?;
80    bridge_rx_response(cx, book, &response, reply_to)
81}
82
83pub(crate) fn terminal_bridge_text(response: &ModelResponse) -> Result<&str> {
84    match terminal_model_content(response)? {
85        Expr::String(text) => Ok(text),
86        Expr::Map(_) => match field(terminal_model_content(response)?, "text") {
87            Some(Expr::String(text)) => Ok(text),
88            _ => Err(Error::Eval(
89                "terminal BRIDGE response content must be a string or text map".to_owned(),
90            )),
91        },
92        _ => Err(Error::Eval(
93            "terminal BRIDGE response content must be text".to_owned(),
94        )),
95    }
96}
97
98fn packet_report_cid(packet: &BridgePacket) -> String {
99    packet.header.cid.clone().unwrap_or_else(|| {
100        packet_content_id(packet)
101            .map(|id| content_id_string(&id))
102            .unwrap_or_else(|_| "unhashable".to_owned())
103    })
104}
105
106fn check_header_linkage(
107    report: &mut BridgeReport,
108    packet: &BridgePacket,
109    reply_to: Option<&BridgePacket>,
110) {
111    let mut seen = BTreeSet::new();
112    for part in &packet.body {
113        if !seen.insert(part.id.clone()) {
114            report.reject(&part.id);
115            report.obligate(BridgeObligation::repair_packet(
116                format!("body/{}", part.id.as_qualified_str()),
117                "duplicate part id",
118                "unique part id",
119                part.id.as_qualified_str(),
120            ));
121        }
122    }
123
124    require_part_id(report, packet, &packet.header.task, "header/task");
125    require_part_id(report, packet, &packet.header.output, "header/output");
126    for context in &packet.header.context {
127        require_part_id(report, packet, context, "header/context");
128    }
129
130    if let Some(parent) = reply_to {
131        let Some(parent_cid) = &parent.header.cid else {
132            report.obligate(BridgeObligation::repair_packet(
133                "header/parents",
134                "parent packet is missing a cid",
135                "stamped parent cid",
136                "none",
137            ));
138            return;
139        };
140        if !parents_contain_cid(&packet.header.parents, parent_cid) {
141            report.obligate(BridgeObligation::repair_packet(
142                "header/parents",
143                "reply does not cite parent packet",
144                parent_cid,
145                format!("{:?}", packet.header.parents),
146            ));
147        }
148        check_reply_actor_linkage(report, packet, parent);
149    }
150}
151
152fn check_reply_actor_linkage(
153    report: &mut BridgeReport,
154    packet: &BridgePacket,
155    parent: &BridgePacket,
156) {
157    if !parent
158        .header
159        .to
160        .iter()
161        .any(|actor| actor == &packet.header.from)
162    {
163        let expected = if parent.header.to.is_empty() {
164            "one parent header/to recipient".to_owned()
165        } else {
166            format!("{:?}", parent.header.to)
167        };
168        report.obligate(BridgeObligation::repair_packet(
169            "header/from",
170            "reply sender is not a parent recipient",
171            expected,
172            packet.header.from.clone(),
173        ));
174    }
175
176    let expected_to = vec![parent.header.from.clone()];
177    if packet.header.to != expected_to {
178        report.obligate(BridgeObligation::repair_packet(
179            "header/to",
180            "reply recipient does not mirror parent sender",
181            format!("{expected_to:?}"),
182            format!("{:?}", packet.header.to),
183        ));
184    }
185}
186
187fn require_part_id(report: &mut BridgeReport, packet: &BridgePacket, id: &Symbol, path: &str) {
188    if packet.body.iter().any(|part| &part.id == id) {
189        return;
190    }
191    report.obligate(BridgeObligation::repair_packet(
192        path,
193        "header references a missing part",
194        id.as_qualified_str(),
195        "missing",
196    ));
197}
198
199fn check_move(
200    book: &BridgeBook,
201    report: &mut BridgeReport,
202    packet: &BridgePacket,
203    reply_to: Option<&BridgePacket>,
204) {
205    let parent_moves = reply_to
206        .map(|parent| vec![parent.header.move_kind.clone()])
207        .unwrap_or_default();
208    let part_kinds = packet
209        .body
210        .iter()
211        .map(|part| part.kind.clone())
212        .collect::<Vec<_>>();
213    if let Err(err) = book
214        .moves
215        .check_move(&packet.header.move_kind, &parent_moves, &part_kinds)
216    {
217        report.obligate(BridgeObligation::repair_packet(
218            "header/move",
219            "illegal BRIDGE move",
220            "move book accepts intent, parent, and required parts",
221            err.to_string(),
222        ));
223    }
224}
225
226fn check_parts(
227    cx: &mut Cx,
228    book: &BridgeBook,
229    report: &mut BridgeReport,
230    packet: &BridgePacket,
231) -> Result<()> {
232    for part in &packet.body {
233        match book.parts.spec(&part.kind) {
234            Some(spec) => {
235                if check_part_shape(cx, spec, part, report)? {
236                    report.accept(&part.id);
237                } else {
238                    report.reject(&part.id);
239                }
240            }
241            None => {
242                report.reject(&part.id);
243                report.obligate(BridgeObligation::repair_packet(
244                    format!("body/{}", part.id.as_qualified_str()),
245                    "unknown BRIDGE part kind",
246                    "registered part kind",
247                    part.kind.as_qualified_str(),
248                ));
249            }
250        }
251    }
252    Ok(())
253}
254
255fn check_part_shape(
256    cx: &mut Cx,
257    spec: &BridgePartSpec,
258    part: &BridgePart,
259    report: &mut BridgeReport,
260) -> Result<bool> {
261    let expected = expected_part_payload_shape(spec);
262    check_payload_shape(
263        cx,
264        &format!("body/{}/payload", part.id.as_qualified_str()),
265        &format!("{} payload", spec.kind.as_qualified_str()),
266        expected,
267        &part.payload,
268        report,
269    )
270}
271
272fn expected_part_payload_shape(spec: &BridgePartSpec) -> Arc<dyn Shape> {
273    if spec.kind == Symbol::qualified("bridge", "Extension")
274        || spec.kind == Symbol::qualified("bridge", "Return")
275    {
276        Arc::new(AnyShape)
277    } else {
278        Arc::new(ExprKindShape::new(ExprKind::Map))
279    }
280}
281
282fn check_parent_return(
283    cx: &mut Cx,
284    report: &mut BridgeReport,
285    packet: &BridgePacket,
286    parent: &BridgePacket,
287) -> Result<()> {
288    let Some(contract) = part_by_id(parent, &parent.header.output) else {
289        report.obligate(BridgeObligation::repair_packet(
290            "reply-to/header/output",
291            "parent output part is missing",
292            parent.header.output.as_qualified_str(),
293            "missing",
294        ));
295        return Ok(());
296    };
297    if contract.kind != Symbol::qualified("bridge", "Return") {
298        report.obligate(BridgeObligation::repair_packet(
299            "reply-to/header/output",
300            "parent output part is not a Return contract",
301            "bridge/Return",
302            contract.kind.as_qualified_str(),
303        ));
304        return Ok(());
305    }
306
307    let Some(shape_expr) = field(&contract.payload, "shape") else {
308        return Ok(());
309    };
310    let Some(reply_output) = part_by_id(packet, &packet.header.output) else {
311        report.obligate(BridgeObligation::repair_packet(
312            "header/output",
313            "reply output part is missing",
314            packet.header.output.as_qualified_str(),
315            "missing",
316        ));
317        return Ok(());
318    };
319    let Some(shape) = shape_from_contract_expr(shape_expr) else {
320        report.obligate(BridgeObligation::repair_packet(
321            "reply-to/return/shape",
322            "unsupported Return shape expression",
323            "core primitives, bridge/Answer, bridge/Refusal, or shape/OneOf",
324            format!("{shape_expr:?}"),
325        ));
326        return Ok(());
327    };
328    check_payload_shape(
329        cx,
330        &format!("body/{}/payload", reply_output.id.as_qualified_str()),
331        "parent Return contract",
332        shape,
333        &reply_output.payload,
334        report,
335    )?;
336    Ok(())
337}
338
339fn part_by_id<'a>(packet: &'a BridgePacket, id: &Symbol) -> Option<&'a BridgePart> {
340    packet.body.iter().find(|part| &part.id == id)
341}
342
343pub(crate) fn shape_from_contract_expr(expr: &Expr) -> Option<Arc<dyn Shape>> {
344    match expr {
345        Expr::Symbol(symbol) => symbol_shape(symbol),
346        Expr::Map(_) => shape_descriptor(expr),
347        _ => None,
348    }
349}
350
351fn symbol_shape(symbol: &Symbol) -> Option<Arc<dyn Shape>> {
352    let shape: Arc<dyn Shape> = match (symbol.namespace.as_deref(), symbol.name.as_ref()) {
353        (Some("core"), "Any") => Arc::new(AnyShape),
354        (Some("core"), "String") | (Some("bridge"), "Answer") => {
355            Arc::new(ExprKindShape::new(ExprKind::String))
356        }
357        (Some("core"), "Bool") => Arc::new(ExprKindShape::new(ExprKind::Bool)),
358        (Some("core"), "Number") => Arc::new(ExprKindShape::new(ExprKind::Number)),
359        (Some("core"), "Symbol") => Arc::new(ExprKindShape::new(ExprKind::Symbol)),
360        (Some("core"), "List") => Arc::new(ExprKindShape::new(ExprKind::List)),
361        (Some("core"), "Map") => Arc::new(ExprKindShape::new(ExprKind::Map)),
362        (Some("bridge"), "Packet") => Arc::new(BridgePacketExprShape),
363        (Some("bridge"), "Refusal") => refusal_shape(),
364        _ => return None,
365    };
366    Some(shape)
367}
368
369struct BridgePacketExprShape;
370
371impl Shape for BridgePacketExprShape {
372    fn symbol(&self) -> Option<Symbol> {
373        Some(Symbol::qualified("bridge", "Packet"))
374    }
375
376    fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
377        let expr = value.object().as_expr(cx)?;
378        self.check_expr(cx, &expr)
379    }
380
381    fn check_expr(&self, _cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
382        match expr_to_packet(expr) {
383            Ok(_) => Ok(ShapeMatch::accept(MatchScore::exact(10))),
384            Err(err) => Ok(ShapeMatch::reject_with_diagnostic(Diagnostic::error(
385                format!("expected bridge/Packet expression: {err}"),
386            ))),
387        }
388    }
389
390    fn describe(&self, _cx: &mut Cx) -> Result<ShapeDoc> {
391        Ok(ShapeDoc::new("bridge/Packet"))
392    }
393}
394
395fn shape_descriptor(expr: &Expr) -> Option<Arc<dyn Shape>> {
396    let Some(Expr::Symbol(kind)) = field(expr, "shape") else {
397        return None;
398    };
399    if kind != &Symbol::qualified("shape", "OneOf") {
400        return None;
401    }
402    let choices = match field(expr, "choices")? {
403        Expr::Vector(items) | Expr::List(items) => items,
404        _ => return None,
405    };
406    let choices = choices
407        .iter()
408        .map(shape_from_contract_expr)
409        .collect::<Option<Vec<_>>>()?;
410    Some(Arc::new(OneOfShape::new(choices)))
411}
412
413fn refusal_shape() -> Arc<dyn Shape> {
414    Arc::new(FieldShape::anonymous(vec![
415        FieldSpec::required(
416            Symbol::new("kind"),
417            Arc::new(ExactExprShape::new(Expr::Symbol(Symbol::qualified(
418                "bridge", "Refusal",
419            )))),
420        ),
421        FieldSpec::required(
422            Symbol::new("reason"),
423            Arc::new(ExprKindShape::new(ExprKind::String)),
424        ),
425    ]))
426}
427
428fn check_payload_shape(
429    cx: &mut Cx,
430    path: &str,
431    expected: &str,
432    shape: Arc<dyn Shape>,
433    payload: &Expr,
434    report: &mut BridgeReport,
435) -> Result<bool> {
436    let shape_ref = shape_value(Symbol::qualified("bridge", expected), shape);
437    let value = cx.factory().expr(payload.clone())?;
438    let matched = check_value_report(cx, &shape_ref, value)?;
439    if matched.accepted {
440        return Ok(true);
441    }
442    report.obligate(BridgeObligation::repair_packet(
443        path,
444        "payload failed Shape check",
445        expected,
446        if matched.diagnostics.is_empty() {
447            format!("{payload:?}")
448        } else {
449            matched
450                .diagnostics
451                .iter()
452                .map(|diagnostic| diagnostic.message.clone())
453                .collect::<Vec<_>>()
454                .join("; ")
455        },
456    ));
457    Ok(false)
458}