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