1#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
24
25pub mod authority_bindings;
26pub mod avc_bindings;
27pub mod catapult_bindings;
28pub mod consent_bindings;
29pub mod core_bindings;
30pub mod decision_forum_bindings;
31pub mod economy_bindings;
32pub mod escalation_bindings;
33pub mod gatekeeper_bindings;
34pub mod governance_bindings;
35pub mod identity_bindings;
36pub mod legal_bindings;
37pub mod messaging_bindings;
38mod serde_bridge;
39
40pub use authority_bindings::*;
43pub use avc_bindings::*;
44pub use catapult_bindings::*;
45pub use consent_bindings::*;
46pub use core_bindings::*;
47pub use decision_forum_bindings::*;
48pub use economy_bindings::*;
49pub use escalation_bindings::*;
50pub use gatekeeper_bindings::*;
51pub use governance_bindings::*;
52pub use identity_bindings::*;
53pub use legal_bindings::*;
54pub use messaging_bindings::*;
55
56#[cfg(test)]
57mod source_guard_tests {
58 #[test]
59 fn wasm_bridge_uses_deterministic_collections() {
60 let binding_sources = [
61 (
62 "authority_bindings.rs",
63 include_str!("authority_bindings.rs"),
64 ),
65 (
66 "governance_bindings.rs",
67 include_str!("governance_bindings.rs"),
68 ),
69 ("economy_bindings.rs", include_str!("economy_bindings.rs")),
70 ("avc_bindings.rs", include_str!("avc_bindings.rs")),
71 ];
72
73 for (path, source) in binding_sources {
74 assert!(
75 !source.contains("HashMap"),
76 "{path} must use deterministic BTreeMap-style collections at the WASM boundary"
77 );
78 assert!(
79 !source.contains("HashSet"),
80 "{path} must use deterministic BTreeSet-style collections at the WASM boundary"
81 );
82 }
83 }
84
85 #[test]
86 fn wasm_consent_bridge_requires_caller_supplied_time() {
87 let source = include_str!("consent_bindings.rs");
88 let forbidden = [
89 format!("{}{}", "Timestamp::", "now_utc()"),
90 format!("{}{}", "Uuid::", "new_v4()"),
91 "HybridClock::new()".to_string(),
92 ];
93
94 for pattern in forbidden {
95 assert!(
96 !source.contains(&pattern),
97 "consent WASM bindings must receive caller-supplied IDs and HLC timestamps"
98 );
99 }
100 }
101
102 #[test]
103 fn wasm_consent_termination_refuses_unsigned_actor_did_bridge() {
104 let source = include_str!("consent_bindings.rs");
105 let termination = source
106 .split("pub fn wasm_terminate_bailment(")
107 .nth(1)
108 .and_then(|section| {
109 section
110 .split("pub fn wasm_terminate_bailment_signed(")
111 .next()
112 })
113 .expect("wasm_terminate_bailment source");
114 let signed_termination = source
115 .split("pub fn wasm_terminate_bailment_signed(")
116 .nth(1)
117 .expect("wasm_terminate_bailment_signed source");
118
119 assert!(
120 termination.contains("unsigned bailment termination is disabled"),
121 "legacy WASM bailment termination must fail closed instead of trusting actor_did"
122 );
123 assert!(
124 !termination.contains("exo_consent::bailment::terminate(&mut bailment, &actor)"),
125 "legacy WASM bailment termination must not reach the core state transition"
126 );
127 assert!(
128 source.contains("pub fn wasm_bailment_termination_payload("),
129 "WASM consent bridge must expose the canonical termination payload for external signing"
130 );
131 assert!(
132 source.contains("pub fn wasm_terminate_bailment_signed("),
133 "WASM consent bridge must keep the signed termination entrypoint fail-closed"
134 );
135 assert!(
136 signed_termination.contains("untrusted_wasm_bailment_termination_error"),
137 "WASM signed termination must fail closed instead of trusting caller-supplied key material"
138 );
139 assert!(
140 !signed_termination.contains("terminate_verified"),
141 "WASM signed termination must not call core termination without trusted DID resolution"
142 );
143 }
144
145 #[test]
146 fn wasm_consent_acceptance_refuses_caller_supplied_bailee_keys() {
147 let source = include_str!("consent_bindings.rs");
148 let acceptance = source
149 .split("pub fn wasm_accept_bailment(")
150 .nth(1)
151 .and_then(|section| {
152 section
153 .split("pub fn wasm_bailment_signing_payload(")
154 .next()
155 })
156 .expect("wasm_accept_bailment source");
157
158 assert!(
159 acceptance.contains("untrusted_wasm_bailment_acceptance_error"),
160 "WASM bailment acceptance must fail closed without trusted DID resolution"
161 );
162 assert!(
163 !acceptance.contains("exo_consent::bailment::accept("),
164 "WASM bailment acceptance must not call core accept without trusted DID resolution"
165 );
166 }
167
168 #[test]
169 fn wasm_governance_bridge_requires_caller_supplied_metadata() {
170 let source = include_str!("governance_bindings.rs");
171 let forbidden = [
172 format!("{}{}", "Timestamp::", "now_utc()"),
173 format!("{}{}", "Uuid::", "new_v4()"),
174 "HybridClock::new()".to_string(),
175 ];
176
177 for pattern in forbidden {
178 assert!(
179 !source.contains(&pattern),
180 "governance WASM bindings must receive caller-supplied IDs and HLC timestamps"
181 );
182 }
183 }
184
185 #[test]
186 fn wasm_governance_close_requires_trusted_runtime_adapter() {
187 let source = include_str!("governance_bindings.rs");
188 let production = source
189 .split("#[cfg(test)]")
190 .next()
191 .expect("production section");
192 assert!(
193 production
194 .contains("verified deliberation closure requires a trusted core runtime adapter"),
195 "public WASM deliberation close must fail closed without a trusted runtime adapter"
196 );
197 assert!(
198 !production.contains("deliberation::close(&mut delib"),
199 "WASM deliberation close must not call the structural-only close path"
200 );
201 assert!(
202 !production.contains("close_verified(&mut delib, &policy, &resolver)"),
203 "public WASM deliberation close must not trust caller-supplied signer keys and roles"
204 );
205 }
206
207 #[test]
208 fn wasm_governance_clearance_requires_caller_supplied_registry() {
209 let source = include_str!("governance_bindings.rs");
210 assert!(
211 source.contains("registry_json"),
212 "WASM clearance checks must accept caller-supplied clearance registry data"
213 );
214 assert!(
215 !source.contains("ClearanceLevel::Governor"),
216 "WASM clearance checks must not fabricate Governor clearance"
217 );
218 }
219
220 #[test]
221 fn wasm_governance_bridge_bounds_untrusted_collection_inputs() {
222 let source = include_str!("governance_bindings.rs");
223 for required in [
224 "MAX_WASM_CLEARANCE_REGISTRY_ENTRIES",
225 "MAX_WASM_CONFLICT_DECLARATIONS",
226 "MAX_WASM_AUDIT_ENTRIES",
227 "MAX_WASM_DELIBERATION_PARTICIPANTS",
228 "MAX_WASM_INDEPENDENCE_ACTORS",
229 "MAX_WASM_REGISTRY_RELATIONSHIPS",
230 "MAX_WASM_COORDINATION_ACTIONS",
231 "MAX_WASM_PROPOSAL_BYTES",
232 "MAX_WASM_CHALLENGE_EVIDENCE_BYTES",
233 "parse_bounded_vec",
234 ] {
235 assert!(
236 source.contains(required),
237 "governance WASM boundary must define and use {required}"
238 );
239 }
240
241 for forbidden in [
242 "let key_pairs: Vec<(String, String)> = from_json_str(public_keys_json)?;",
243 "let entries: Vec<WasmClearanceRegistryEntry> = from_json_str(registry_json)?;",
244 "let approvals: Vec<exo_governance::quorum::Approval> = from_json_str(approvals_json)?;",
245 "let entries: Vec<exo_governance::audit::AuditEntry> = from_json_str(entries_json)?;",
246 "let did_strs: Vec<String> = from_json_str(participants_json)?;",
247 "let did_strs: Vec<String> = from_json_str(actors_json)?;",
248 "let actions: Vec<exo_governance::crosscheck::TimestampedAction> = from_json_str(actions_json)?;",
249 ] {
250 assert!(
251 !source.contains(forbidden),
252 "governance WASM boundary must not deserialize untrusted arrays without a count bound: {forbidden}"
253 );
254 }
255 }
256
257 #[test]
258 fn wasm_non_governance_vec_inputs_use_explicit_count_bounds() {
259 let required = [
260 (
261 "authority_bindings.rs",
262 include_str!("authority_bindings.rs"),
263 "MAX_WASM_AUTHORITY_LINKS",
264 ),
265 (
266 "authority_bindings.rs",
267 include_str!("authority_bindings.rs"),
268 "AUTHORITY_CHAIN_TRUSTED_ADAPTER_REQUIRED",
269 ),
270 (
271 "authority_bindings.rs",
272 include_str!("authority_bindings.rs"),
273 "AUTHORITY_CHAIN_CALLER_KEYS_REJECTED",
274 ),
275 (
276 "identity_bindings.rs",
277 include_str!("identity_bindings.rs"),
278 "MAX_WASM_SHAMIR_SHARES",
279 ),
280 (
281 "escalation_bindings.rs",
282 include_str!("escalation_bindings.rs"),
283 "MAX_WASM_DETECTION_SIGNALS",
284 ),
285 (
286 "escalation_bindings.rs",
287 include_str!("escalation_bindings.rs"),
288 "MAX_WASM_FEEDBACK_ENTRIES",
289 ),
290 (
291 "escalation_bindings.rs",
292 include_str!("escalation_bindings.rs"),
293 "MAX_WASM_ESCALATION_CASES",
294 ),
295 (
296 "messaging_bindings.rs",
297 include_str!("messaging_bindings.rs"),
298 "MAX_WASM_AUTHORIZED_TRUSTEES",
299 ),
300 (
301 "legal_bindings.rs",
302 include_str!("legal_bindings.rs"),
303 "MAX_WASM_LEGAL_AUDIT_ACTIONS",
304 ),
305 (
306 "legal_bindings.rs",
307 include_str!("legal_bindings.rs"),
308 "MAX_WASM_EDISCOVERY_CORPUS_ITEMS",
309 ),
310 (
311 "legal_bindings.rs",
312 include_str!("legal_bindings.rs"),
313 "MAX_WASM_RETENTION_RECORDS",
314 ),
315 ];
316
317 for (path, source, required_name) in required {
318 assert!(
319 source.contains(required_name),
320 "{path} must define and use {required_name}"
321 );
322 }
323
324 let forbidden = [
325 (
326 "authority_bindings.rs",
327 include_str!("authority_bindings.rs"),
328 "let links: Vec<exo_authority::AuthorityLink> = from_json_str(links_json)?;",
329 ),
330 (
331 "authority_bindings.rs",
332 include_str!("authority_bindings.rs"),
333 "let key_pairs: Vec<(String, String)> = from_json_str(keys_json)?;",
334 ),
335 (
336 "identity_bindings.rs",
337 include_str!("identity_bindings.rs"),
338 "let shares: Vec<exo_identity::shamir::Share> = from_json_str(shares_json)?;",
339 ),
340 (
341 "escalation_bindings.rs",
342 include_str!("escalation_bindings.rs"),
343 "let signals: Vec<exo_escalation::detector::DetectionSignal> = from_json_str(signals_json)?;",
344 ),
345 (
346 "escalation_bindings.rs",
347 include_str!("escalation_bindings.rs"),
348 "from_json_str(entries_json)?;",
349 ),
350 (
351 "escalation_bindings.rs",
352 include_str!("escalation_bindings.rs"),
353 "let feedbacks: Vec<exo_escalation::feedback::FeedbackEntry> = from_json_str(feedbacks_json)?;",
354 ),
355 (
356 "escalation_bindings.rs",
357 include_str!("escalation_bindings.rs"),
358 "let mut cases: Vec<exo_escalation::escalation::EscalationCase> = from_json_str(cases_json)?;",
359 ),
360 (
361 "messaging_bindings.rs",
362 include_str!("messaging_bindings.rs"),
363 "let trustees: Vec<WasmAuthorizedTrustee> = from_json_str(authorized_trustees_json)?;",
364 ),
365 (
366 "legal_bindings.rs",
367 include_str!("legal_bindings.rs"),
368 "let actions: Vec<exo_legal::fiduciary::AuditEntry> = from_json_str(actions_json)?;",
369 ),
370 (
371 "legal_bindings.rs",
372 include_str!("legal_bindings.rs"),
373 "let corpus: Vec<exo_legal::evidence::Evidence> = from_json_str(corpus_json)?;",
374 ),
375 (
376 "legal_bindings.rs",
377 include_str!("legal_bindings.rs"),
378 "let mut records: Vec<exo_legal::records::Record> = from_json_str(records_json)?;",
379 ),
380 ];
381
382 for (path, source, forbidden_pattern) in forbidden {
383 assert!(
384 !source.contains(forbidden_pattern),
385 "{path} must not deserialize untrusted JSON arrays without explicit count bounds: {forbidden_pattern}"
386 );
387 }
388 }
389
390 #[test]
391 fn wasm_decision_forum_vec_inputs_use_explicit_count_bounds() {
392 let source = include_str!("decision_forum_bindings.rs");
393
394 for required in [
395 "MAX_WASM_FORUM_EMERGENCY_ACTIONS",
396 "MAX_WASM_FORUM_CHALLENGES",
397 ] {
398 assert!(
399 source.contains(required),
400 "decision forum WASM boundary must define and use {required}"
401 );
402 }
403
404 for forbidden in [
405 "let actions: Vec<decision_forum::emergency::EmergencyAction> = from_json_str(actions_json)?;",
406 "from_json_str(challenges_json)?;",
407 "let sig_pairs: Vec<(String, String)> = from_json_str(signatures_json)?;",
408 "let key_pairs: Vec<(String, String)> = from_json_str(public_keys_json)?;",
409 ] {
410 assert!(
411 !source.contains(forbidden),
412 "decision forum WASM boundary must not deserialize untrusted arrays without a count bound: {forbidden}"
413 );
414 }
415 }
416
417 #[test]
418 fn wasm_decision_forum_human_vote_exports_fail_closed_without_verified_registry() {
419 let source = include_str!("decision_forum_bindings.rs");
420
421 assert!(
422 source.contains("decision_forum::human_gate::enforce_human_gate(&policy, &decision)"),
423 "legacy WASM human-gate export must use the fail-closed core helper when no trusted human registry is available"
424 );
425 assert!(
426 source.contains("decision_forum::human_gate::is_human_vote(&vote)"),
427 "legacy WASM human-vote export must use the fail-closed core helper, not declared actor metadata"
428 );
429 assert!(
430 source.contains("decision_forum::quorum::check_quorum(®istry, &decision)"),
431 "legacy WASM quorum export must use the fail-closed core helper when no trusted human registry is available"
432 );
433
434 for forbidden in [
435 "is_declared_human_vote",
436 "check_quorum_with_verified_humans",
437 "verified_human_voters",
438 ] {
439 assert!(
440 !source.contains(forbidden),
441 "legacy WASM decision-forum exports must not accept or synthesize verified human status via {forbidden}"
442 );
443 }
444 }
445
446 #[test]
447 fn wasm_decision_transition_requires_kernel_adjudication() {
448 let source = include_str!("decision_forum_bindings.rs");
449 let legacy_transition = source
450 .split("pub fn wasm_transition_decision(")
451 .nth(1)
452 .and_then(|section| {
453 section
454 .split("pub fn wasm_transition_decision_adjudicated(")
455 .next()
456 })
457 .expect("legacy decision transition export source");
458
459 assert!(
460 legacy_transition.contains("unadjudicated decision transitions are disabled"),
461 "legacy WASM decision transition must fail closed without a kernel verdict"
462 );
463 assert!(
464 !legacy_transition.contains(".transition_at("),
465 "legacy WASM decision transition must not reach the raw BCTS transition"
466 );
467 assert!(
468 source.contains("Kernel::new") && source.contains(".transition_adjudicated_at("),
469 "WASM decision bridge must expose a kernel-adjudicated transition entrypoint"
470 );
471 assert!(
472 source.contains("WasmDecisionTransitionAdjudicatedRequest")
473 && source.contains("request_json"),
474 "WASM adjudicated decision transition must use a typed bounded request JSON instead of a wide argument list"
475 );
476 let adjudicated_transition = source
477 .split("pub fn wasm_transition_decision_adjudicated(")
478 .nth(1)
479 .and_then(|section| section.split("/// Add a vote").next())
480 .expect("adjudicated decision transition export source");
481 assert!(
482 adjudicated_transition.contains("InvariantSet::all()"),
483 "WASM adjudicated decision transition must enforce the canonical complete invariant set"
484 );
485 assert!(
486 !adjudicated_transition.contains("Kernel::new(constitution, invariant_set)"),
487 "WASM adjudicated decision transition must not build a kernel from caller-supplied invariants"
488 );
489 assert!(
490 source
491 .split("struct WasmDecisionTransitionAdjudicatedRequest")
492 .nth(1)
493 .and_then(|section| section.split("/// Create a new DecisionObject").next())
494 .is_some_and(|section| !section.contains("invariant_set:")),
495 "WASM adjudicated decision transition request must not deserialize caller-supplied invariants"
496 );
497 assert!(
498 !source.contains(
499 "timestamp_logical: u32,\n constitution: &[u8],\n invariant_set_json: &str,"
500 ),
501 "WASM adjudicated decision transition must not expose a clippy-wide argument list"
502 );
503 }
504
505 #[test]
506 fn wasm_messaging_bridge_requires_caller_supplied_envelope_metadata() {
507 let source = include_str!("messaging_bindings.rs");
508 assert!(
509 source.contains("message_id") && source.contains("created_physical_ms"),
510 "messaging WASM encryption must expose caller-supplied envelope metadata"
511 );
512 assert!(
513 source.contains("ComposeMetadata::new"),
514 "messaging WASM encryption must validate caller-supplied envelope metadata"
515 );
516 assert!(
517 source.contains("DeathVerificationCreationMetadata::new")
518 && source.contains("DeathConfirmationMetadata::new"),
519 "messaging WASM death verification must validate caller-supplied state metadata"
520 );
521 let initial_payload = source
522 .split("pub fn wasm_death_verification_initial_signing_payload")
523 .nth(1)
524 .and_then(|section| {
525 section
526 .split("/// Create a new death verification request")
527 .next()
528 })
529 .expect("initial death-verification signing payload section");
530 assert!(
531 initial_payload.contains("created_physical_ms")
532 && initial_payload.contains("&metadata.created_at"),
533 "initial death-verification signatures must bind the submitted creation timestamp"
534 );
535 let confirmation_payload = source
536 .split("pub fn wasm_death_verification_confirmation_signing_payload")
537 .nth(1)
538 .and_then(|section| section.split("/// Add a trustee confirmation").next())
539 .expect("confirmation death-verification signing payload section");
540 assert!(
541 confirmation_payload.contains("confirmed_physical_ms")
542 && confirmation_payload.contains("&metadata.confirmed_at"),
543 "death-verification confirmation signatures must bind the submitted confirmation timestamp"
544 );
545
546 let forbidden = [
547 format!("{}{}", "Timestamp::", "now_utc()"),
548 format!("{}{}", "Uuid::", "new_v4()"),
549 "HybridClock::new()".to_string(),
550 ];
551
552 for pattern in forbidden {
553 assert!(
554 !source.contains(&pattern),
555 "messaging WASM bindings must receive caller-supplied IDs and HLC timestamps"
556 );
557 }
558 }
559
560 #[test]
561 fn wasm_messaging_bridge_does_not_export_x25519_secret_material() {
562 let source = include_str!("messaging_bindings.rs");
563 let forbidden = [
564 ["secret", "_key_hex"].concat(),
565 [".secret", ".to_hex()"].concat(),
566 [".secret", ".0"].concat(),
567 ];
568 for pattern in forbidden {
569 assert!(
570 !source.contains(&pattern),
571 "messaging WASM bindings must not export or tuple-access X25519 secret material via {pattern}"
572 );
573 }
574 }
575
576 #[test]
577 fn wasm_messaging_bridge_does_not_generate_x25519_keypairs() {
578 let source = include_str!("messaging_bindings.rs");
579 let production = source
580 .split("// ===========================================================================")
581 .next()
582 .unwrap_or(source);
583
584 assert!(
585 !production.contains("X25519KeyPair::generate"),
586 "messaging WASM must not generate X25519 key material inside the bridge"
587 );
588 assert!(
589 production.contains("ephemeral_x25519_secret_hex"),
590 "messaging WASM encryption must receive caller-supplied ephemeral X25519 material"
591 );
592 }
593
594 #[test]
595 fn wasm_messaging_bridge_does_not_decode_ed25519_signing_secrets() {
596 let source = include_str!("messaging_bindings.rs");
597 let production = source
598 .split("// ===========================================================================")
599 .next()
600 .unwrap_or(source);
601
602 assert!(
603 production.contains("wasm_prepare_encrypted_message"),
604 "messaging WASM must expose unsigned encrypted envelopes plus signing bytes"
605 );
606 assert!(
607 production.contains("wasm_attach_message_signature"),
608 "messaging WASM must attach caller-produced signatures without importing sender secrets"
609 );
610
611 for pattern in [
612 "parse_ed25519_signing_seed_hex",
613 "sender_signing_key_hex",
614 "SecretKey::from_bytes",
615 "lock_and_send(",
616 ] {
617 assert!(
618 !production.contains(pattern),
619 "messaging WASM bindings must not decode or use Ed25519 signing secrets via {pattern}"
620 );
621 }
622 }
623
624 #[test]
625 fn wasm_messaging_legacy_raw_secret_entrypoints_fail_closed() {
626 let source = include_str!("messaging_bindings.rs");
627 let production = source
628 .split("// ===========================================================================")
629 .next()
630 .unwrap_or(source);
631
632 assert!(
633 production
634 .contains("raw X25519 secret public derivation is disabled at the WASM boundary"),
635 "legacy X25519 raw-secret public derivation must fail closed"
636 );
637 assert!(
638 production.contains("raw Ed25519 sender signing is disabled at the WASM boundary"),
639 "legacy raw-secret message encryption must fail closed"
640 );
641 }
642
643 #[test]
644 fn wasm_identity_risk_bridge_requires_caller_supplied_signer_and_time() {
645 let source = include_str!("identity_bindings.rs");
646 assert!(
647 source.contains("attester_secret_hex")
648 && source.contains("now_physical_ms")
649 && source.contains("now_logical"),
650 "risk assessment must expose caller-supplied signer and HLC metadata"
651 );
652
653 let forbidden = [
654 "HybridClock::new()".to_string(),
655 "generate_keypair()".to_string(),
656 ["Timestamp::", "now_utc()"].concat(),
657 ];
658
659 for pattern in &forbidden {
660 assert!(
661 !source.contains(pattern),
662 "identity WASM risk binding must not fabricate signer or time with {pattern}"
663 );
664 }
665 }
666
667 #[test]
668 fn wasm_shamir_split_exposes_caller_supplied_entropy_boundary() {
669 let source = include_str!("identity_bindings.rs");
670 let production = source
671 .split("// ===========================================================================")
672 .next()
673 .unwrap_or(source);
674
675 assert!(
676 production.contains("wasm_shamir_split_with_entropy"),
677 "WASM Shamir splitting must expose a caller-supplied entropy entrypoint"
678 );
679 assert!(
680 production.contains("exo_identity::shamir::split_with_entropy"),
681 "WASM Shamir splitting must call the entropy-explicit core split API"
682 );
683 assert!(
684 production.contains("entropy"),
685 "WASM Shamir split boundary must keep entropy explicit in the public API"
686 );
687 }
688
689 #[test]
690 fn wasm_identity_secret_metadata_has_no_debug_surface() {
691 let source = include_str!("identity_bindings.rs");
692 let production = source
693 .split("// ===========================================================================")
694 .next()
695 .unwrap_or(source);
696 let metadata_def = production
697 .split("struct RiskAssessmentMetadata")
698 .next()
699 .expect("risk metadata definition must exist");
700
701 assert!(
702 production.contains("attester_secret_hex"),
703 "risk assessment metadata must keep the caller-supplied attester secret explicit"
704 );
705 assert!(
706 !metadata_def.contains("Debug"),
707 "secret-bearing risk metadata must not derive or expose Debug"
708 );
709 }
710
711 #[test]
712 fn wasm_core_event_bridge_requires_caller_supplied_metadata() {
713 let source = include_str!("core_bindings.rs");
714 assert!(
715 source.contains("event_id")
716 && source.contains("timestamp_physical_ms")
717 && source.contains("timestamp_logical"),
718 "signed event creation must expose caller-supplied event ID and HLC metadata"
719 );
720
721 let forbidden = [
722 "CorrelationId::new()".to_string(),
723 "HybridClock::new()".to_string(),
724 ["Timestamp::", "now_utc()"].concat(),
725 ];
726
727 for pattern in &forbidden {
728 assert!(
729 !source.contains(pattern),
730 "core WASM event bindings must not fabricate event metadata with {pattern}"
731 );
732 }
733 }
734
735 #[test]
736 fn wasm_core_bridge_does_not_decode_raw_secret_keys() {
737 let source = include_str!("core_bindings.rs");
738 let production = source
739 .split("// ===========================================================================")
740 .next()
741 .unwrap_or(source);
742
743 assert!(
744 production.contains("wasm_event_signing_payload"),
745 "core WASM events must expose canonical signing bytes for external signers"
746 );
747 assert!(
748 production.contains("wasm_create_event_with_signature"),
749 "core WASM events must accept caller-produced signatures without importing secrets"
750 );
751
752 for pattern in [
753 "parse_ed25519_secret_array_hex",
754 "parse_ed25519_signing_seed_hex",
755 "SecretKey::from_bytes",
756 "KeyPair::from_secret_bytes",
757 "exo_core::events::create_signed_event",
758 ] {
759 assert!(
760 !production.contains(pattern),
761 "core WASM bindings must not decode or use raw secret keys via {pattern}"
762 );
763 }
764 }
765
766 #[test]
767 fn wasm_core_legacy_raw_secret_entrypoints_fail_closed() {
768 let source = include_str!("core_bindings.rs");
769 let production = source
770 .split("// ===========================================================================")
771 .next()
772 .unwrap_or(source);
773
774 assert!(
775 production.contains("raw secret-key signing is disabled at the WASM boundary"),
776 "legacy raw-secret signing entrypoint must fail closed"
777 );
778 assert!(
779 production
780 .contains("raw secret-key public derivation is disabled at the WASM boundary"),
781 "legacy raw-secret public-key derivation entrypoint must fail closed"
782 );
783 assert!(
784 production.contains("raw secret-key event signing is disabled at the WASM boundary"),
785 "legacy raw-secret event creation entrypoint must fail closed"
786 );
787 }
788
789 #[test]
790 fn wasm_avc_bindings_externalize_subject_signing() {
791 let source = include_str!("avc_bindings.rs");
792 let production = source.split("\n#[cfg(test)]").next().unwrap_or(source);
793
794 assert!(
795 production.contains("from_json_str::<AutonomousVolitionCredential>"),
796 "AVC WASM credential inputs must use the bounded JSON bridge"
797 );
798 assert!(
799 production.contains("from_json_str::<AvcActionRequest>"),
800 "AVC WASM action inputs must use the bounded JSON bridge"
801 );
802 assert!(
803 production.contains("from_json_str::<Signature>"),
804 "AVC WASM signature inputs must use the bounded JSON bridge"
805 );
806 for pattern in [
807 "serde_json::from_str(credential_json)",
808 "serde_json::from_str(action_json)",
809 "serde_json::from_str(subject_signature_json)",
810 "hex::decode(value)",
811 ] {
812 assert!(
813 !production.contains(pattern),
814 "AVC WASM production bindings must not bypass input bounds via {pattern}"
815 );
816 }
817
818 assert!(
819 production.contains("wasm_avc_action_signing_payload"),
820 "AVC WASM bindings must expose canonical action bytes for external signers"
821 );
822 assert!(
823 production.contains("wasm_avc_build_emit_request_from_signature"),
824 "AVC WASM bindings must accept externally produced signatures"
825 );
826 assert!(
827 production.contains("raw AVC subject-key signing is disabled at the WASM boundary"),
828 "legacy AVC raw subject-key signing must fail closed"
829 );
830 assert!(
831 production.contains(
832 "raw AVC subject-key emit request building is disabled at the WASM boundary"
833 ),
834 "legacy AVC raw subject-key request building must fail closed"
835 );
836
837 for pattern in [
838 "subject_secret_hex",
839 "raw_subject_key_material",
840 "SecretKey::from_bytes",
841 "KeyPair::from_secret_bytes",
842 "crypto::sign(",
843 ] {
844 assert!(
845 !production.contains(pattern),
846 "AVC WASM production bindings must not decode, derive, or use raw subject keys via {pattern}"
847 );
848 }
849 }
850
851 #[test]
852 fn wasm_core_merkle_bindings_bound_untrusted_arrays() {
853 let source = include_str!("core_bindings.rs");
854 assert!(
855 source.contains("MAX_WASM_MERKLE_LEAVES"),
856 "WASM Merkle root/proof bindings must cap caller-supplied leaf arrays"
857 );
858 assert!(
859 source.contains("MAX_WASM_MERKLE_PROOF_HASHES"),
860 "WASM Merkle verification must cap caller-supplied proof arrays"
861 );
862 }
863
864 #[test]
865 fn wasm_secret_key_decoding_zeroizes_rust_owned_buffers() {
866 let sources = [
867 ("identity_bindings.rs", include_str!("identity_bindings.rs")),
868 (
869 "messaging_bindings.rs",
870 include_str!("messaging_bindings.rs"),
871 ),
872 ];
873
874 for (path, source) in sources {
875 assert!(
876 source.contains("Zeroizing"),
877 "{path} must wrap decoded secret-key buffers in zeroize::Zeroizing"
878 );
879 }
880
881 let messaging_source = include_str!("messaging_bindings.rs");
882 let x25519_helper = messaging_source
883 .split("fn parse_x25519_keypair_hex")
884 .nth(1)
885 .and_then(|rest| {
886 rest.split("/// Attach a caller-produced Ed25519 signature")
887 .next()
888 })
889 .expect("messaging X25519 secret parser must be present");
890 assert!(
891 x25519_helper.contains("Zeroizing::new("),
892 "messaging X25519 secret parser must zeroize Rust-owned decoded buffers"
893 );
894 }
895
896 #[test]
897 fn wasm_gatekeeper_boundary_redacts_internal_errors_and_state() {
898 let source = include_str!("gatekeeper_bindings.rs");
899 assert!(
900 source.contains("gatekeeper_boundary_error"),
901 "gatekeeper WASM bindings must centralize sanitized boundary errors"
902 );
903 assert!(
904 source.contains("holon_state_label"),
905 "gatekeeper WASM bindings must expose explicit lifecycle labels"
906 );
907
908 let forbidden = [
909 "format!(\"Reduction error: {e}\")",
910 "format!(\"Step error: {e}\")",
911 "format!(\"{:?}\", holon.state)",
912 ];
913
914 for pattern in forbidden {
915 assert!(
916 !source.contains(pattern),
917 "gatekeeper WASM boundary must not expose internal debug/error details via {pattern}"
918 );
919 }
920 }
921
922 #[test]
923 fn wasm_escalation_kanban_validator_uses_bounded_json_bridge() {
924 let source = include_str!("escalation_bindings.rs");
925 assert!(
926 source.contains("from_json_str(column_json)"),
927 "Kanban column validation must use the bounded JSON bridge"
928 );
929 assert!(
930 !source.contains("serde_json::from_str(column_json)"),
931 "Kanban column validation must not bypass the bounded JSON bridge"
932 );
933 assert!(
934 !source.contains("\"error\": e.to_string()"),
935 "Kanban column validation must not return raw serde errors to WASM callers"
936 );
937 }
938}