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