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
21pub fn effective_caps(cx: &Cx, packet: &BridgePacket) -> Result<CapabilitySet> {
23 effective_ceiling(cx.capabilities(), &packet.header.ceiling)
24}
25
26pub 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
52pub 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
72pub 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}