Skip to main content

telltale_vm/
composition.rs

1//! Protocol composition API for running many protocols in one VM instance.
2//!
3//! This layer provides:
4//! - proof-carrying admission (`LinkOKFull`-style certificate flag),
5//! - shared immutable protocol artifacts (`Arc<CodeImage>`),
6//! - memory-budget accounting for composed workloads.
7
8use std::mem::size_of;
9use std::sync::Arc;
10
11use crate::determinism::DeterminismMode;
12use crate::effect::EffectHandler;
13use crate::loader::CodeImage;
14use crate::output_condition::OutputConditionPolicy;
15use crate::runtime_contracts::{enforce_vm_runtime_gates, RuntimeContracts, RuntimeGateResult};
16use crate::scheduler::SchedPolicy;
17use crate::vm::{VMConfig, VMError, VM};
18
19/// Determinism capability required to admit a protocol bundle.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum DeterminismCapability {
22    /// Supports full determinism for fixed initial state/policy/effect trace.
23    Full,
24    /// Supports determinism modulo effect traces.
25    ModuloEffects,
26    /// Supports determinism modulo admissible commutativity.
27    ModuloCommutativity,
28    /// Supports replay determinism profile.
29    Replay,
30}
31
32/// Scheduler profile capability required to admit a protocol bundle.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum SchedulerCapability {
35    /// Cooperative scheduler profile.
36    Cooperative,
37    /// Round-robin scheduler profile.
38    RoundRobin,
39    /// Priority scheduler profile.
40    Priority,
41    /// Progress-aware scheduler profile.
42    ProgressAware,
43}
44
45/// Theorem-pack capabilities surfaced by a proof artifact.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct TheoremPackCapabilities {
48    /// Determinism profiles certified by the proof artifact.
49    pub determinism: Vec<DeterminismCapability>,
50    /// Scheduler profiles certified by the proof artifact.
51    pub schedulers: Vec<SchedulerCapability>,
52    /// Whether output-condition commit-gating is certified.
53    pub output_condition_gating: bool,
54}
55
56impl TheoremPackCapabilities {
57    /// Capability set that allows all currently supported advanced runtime modes.
58    #[must_use]
59    pub fn full() -> Self {
60        Self {
61            determinism: vec![
62                DeterminismCapability::Full,
63                DeterminismCapability::ModuloEffects,
64                DeterminismCapability::ModuloCommutativity,
65                DeterminismCapability::Replay,
66            ],
67            schedulers: vec![
68                SchedulerCapability::Cooperative,
69                SchedulerCapability::RoundRobin,
70                SchedulerCapability::Priority,
71                SchedulerCapability::ProgressAware,
72            ],
73            output_condition_gating: true,
74        }
75    }
76}
77
78/// Proof/certificate artifact required for composed protocol admission.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct CompositionCertificate {
81    /// Stable identifier for the certificate/proof artifact.
82    pub artifact_id: String,
83    /// Whether this artifact witnesses `LinkOKFull`-style compatibility.
84    pub link_ok_full: bool,
85    /// Theorem-pack capabilities proven by this certificate.
86    pub theorem_pack: TheoremPackCapabilities,
87    /// Optional runtime contract bundle required for advanced runtime modes.
88    pub runtime_contracts: Option<RuntimeContracts>,
89}
90
91/// Immutable protocol bundle loaded by the composition API.
92#[derive(Debug, Clone)]
93pub struct ProtocolBundle {
94    /// Shared immutable code image.
95    pub code: Arc<CodeImage>,
96    /// Certificate checked at admission time.
97    pub certificate: CompositionCertificate,
98}
99
100impl ProtocolBundle {
101    /// Construct a bundle from a code image and certificate.
102    #[must_use]
103    pub fn new(code: Arc<CodeImage>, certificate: CompositionCertificate) -> Self {
104        Self { code, certificate }
105    }
106}
107
108/// Memory budget policy for composed execution.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct MemoryBudget {
111    /// Max bytes allowed per coroutine.
112    pub max_bytes_per_coroutine: usize,
113    /// Max bytes allowed per session.
114    pub max_bytes_per_session: usize,
115    /// Max incremental bytes allowed per additional protocol.
116    pub max_incremental_bytes_per_protocol: usize,
117}
118
119impl Default for MemoryBudget {
120    fn default() -> Self {
121        Self {
122            max_bytes_per_coroutine: 64 * 1024,
123            max_bytes_per_session: 256 * 1024,
124            max_incremental_bytes_per_protocol: 128 * 1024,
125        }
126    }
127}
128
129/// Composition memory usage snapshot.
130#[derive(Debug, Clone, Default, PartialEq, Eq)]
131pub struct MemoryUsage {
132    /// Estimated bytes per coroutine.
133    pub bytes_per_coroutine: usize,
134    /// Estimated bytes per session.
135    pub bytes_per_session: usize,
136    /// Estimated incremental overhead per protocol.
137    pub incremental_bytes_per_protocol: usize,
138    /// Number of loaded protocol bundles.
139    pub protocol_count: usize,
140    /// Number of live sessions.
141    pub session_count: usize,
142    /// Number of live coroutines.
143    pub coroutine_count: usize,
144}
145
146/// Errors emitted by the composition API.
147#[derive(Debug, thiserror::Error)]
148pub enum CompositionError {
149    /// Missing/invalid proof artifact.
150    #[error("bundle `{artifact_id}` rejected: missing LinkOKFull compatibility evidence")]
151    MissingCompatibilityProof {
152        /// Certificate artifact id that failed admission.
153        artifact_id: String,
154    },
155    /// The certificate does not expose required theorem/capability evidence.
156    #[error("bundle `{artifact_id}` rejected: missing required capability `{capability}`")]
157    MissingCapability {
158        /// Certificate artifact id that failed admission.
159        artifact_id: String,
160        /// Human-readable capability key.
161        capability: String,
162    },
163    /// Advanced runtime mode requires runtime contract evidence.
164    #[error("bundle `{artifact_id}` rejected: missing VM runtime contracts for advanced mode")]
165    MissingRuntimeContracts {
166        /// Certificate artifact id that failed admission.
167        artifact_id: String,
168    },
169    /// Admission would violate memory budget.
170    #[error("bundle `{artifact_id}` rejected: memory budget exceeded ({reason})")]
171    BudgetExceeded {
172        /// Certificate artifact id associated with the rejected bundle.
173        artifact_id: String,
174        /// Human-readable budget reason.
175        reason: String,
176    },
177    /// VM-layer load/run failure.
178    #[error(transparent)]
179    Vm(#[from] VMError),
180}
181
182/// Runtime wrapper for composed protocol execution.
183#[derive(Debug)]
184pub struct ComposedRuntime {
185    vm: VM,
186    bundles: Vec<ProtocolBundle>,
187    budget: MemoryBudget,
188    usage: MemoryUsage,
189}
190
191impl ComposedRuntime {
192    /// Create an empty composed runtime.
193    #[must_use]
194    pub fn new(config: VMConfig, budget: MemoryBudget) -> Self {
195        let vm = VM::new(config);
196        Self {
197            vm,
198            bundles: Vec::new(),
199            budget,
200            usage: MemoryUsage {
201                bytes_per_coroutine: size_of::<crate::coroutine::Coroutine>(),
202                bytes_per_session: size_of::<crate::session::SessionState>(),
203                incremental_bytes_per_protocol: size_of::<Arc<CodeImage>>(),
204                ..MemoryUsage::default()
205            },
206        }
207    }
208
209    /// Admit a protocol bundle after certificate + budget checks.
210    ///
211    /// # Errors
212    ///
213    /// Returns a `CompositionError` when proof or budget checks fail.
214    pub fn admit_bundle(&mut self, bundle: ProtocolBundle) -> Result<(), CompositionError> {
215        if !bundle.certificate.link_ok_full {
216            return Err(CompositionError::MissingCompatibilityProof {
217                artifact_id: bundle.certificate.artifact_id,
218            });
219        }
220        self.require_capabilities(&bundle)?;
221
222        if self.usage.incremental_bytes_per_protocol
223            > self.budget.max_incremental_bytes_per_protocol
224        {
225            return Err(CompositionError::BudgetExceeded {
226                artifact_id: bundle.certificate.artifact_id,
227                reason: "incremental protocol overhead".to_string(),
228            });
229        }
230
231        self.bundles.push(bundle);
232        self.usage.protocol_count = self.bundles.len();
233        Ok(())
234    }
235
236    /// Load one session instance from an admitted bundle index.
237    ///
238    /// # Errors
239    ///
240    /// Returns a `CompositionError` when index is invalid or VM loading fails.
241    pub fn load_bundle_session(&mut self, bundle_idx: usize) -> Result<usize, CompositionError> {
242        let bundle =
243            self.bundles
244                .get(bundle_idx)
245                .ok_or_else(|| CompositionError::BudgetExceeded {
246                    artifact_id: format!("bundle/{bundle_idx}"),
247                    reason: "bundle index out of range".to_string(),
248                })?;
249
250        let sid = self.vm.load_choreography(&bundle.code)?;
251        self.refresh_usage();
252        self.assert_budget(bundle_idx)?;
253        Ok(sid)
254    }
255
256    /// Load `sessions` instances from a bundle index.
257    ///
258    /// # Errors
259    ///
260    /// Returns a `CompositionError` when any session load fails.
261    pub fn load_bundle_sessions(
262        &mut self,
263        bundle_idx: usize,
264        sessions: usize,
265    ) -> Result<Vec<usize>, CompositionError> {
266        let mut out = Vec::with_capacity(sessions);
267        for _ in 0..sessions {
268            out.push(self.load_bundle_session(bundle_idx)?);
269        }
270        Ok(out)
271    }
272
273    /// Run the composed runtime.
274    ///
275    /// # Errors
276    ///
277    /// Returns VM-layer errors as composition errors.
278    pub fn run(
279        &mut self,
280        handler: &dyn EffectHandler,
281        max_steps: usize,
282    ) -> Result<(), CompositionError> {
283        self.vm.run(handler, max_steps)?;
284        self.refresh_usage();
285        Ok(())
286    }
287
288    /// Access current memory usage snapshot.
289    #[must_use]
290    pub fn memory_usage(&self) -> &MemoryUsage {
291        &self.usage
292    }
293
294    /// Access admitted bundles.
295    #[must_use]
296    pub fn bundles(&self) -> &[ProtocolBundle] {
297        &self.bundles
298    }
299
300    /// Access underlying VM.
301    #[must_use]
302    pub fn vm(&self) -> &VM {
303        &self.vm
304    }
305
306    fn refresh_usage(&mut self) {
307        self.usage.session_count = self.vm.session_count();
308        self.usage.coroutine_count = self.vm.coroutine_count();
309        self.usage.protocol_count = self.bundles.len();
310    }
311
312    fn assert_budget(&self, bundle_idx: usize) -> Result<(), CompositionError> {
313        let per_coro = self.usage.bytes_per_coroutine;
314        let per_sess = self.usage.bytes_per_session;
315        let artifact_id = self
316            .bundles
317            .get(bundle_idx)
318            .map(|b| b.certificate.artifact_id.clone())
319            .unwrap_or_else(|| format!("bundle/{bundle_idx}"));
320        if per_coro > self.budget.max_bytes_per_coroutine {
321            return Err(CompositionError::BudgetExceeded {
322                artifact_id,
323                reason: "bytes_per_coroutine".to_string(),
324            });
325        }
326        if per_sess > self.budget.max_bytes_per_session {
327            return Err(CompositionError::BudgetExceeded {
328                artifact_id,
329                reason: "bytes_per_session".to_string(),
330            });
331        }
332        Ok(())
333    }
334
335    fn require_capabilities(&self, bundle: &ProtocolBundle) -> Result<(), CompositionError> {
336        let cert = &bundle.certificate;
337        let caps = &cert.theorem_pack;
338        let runtime_contracts = cert.runtime_contracts.as_ref();
339
340        match enforce_vm_runtime_gates(self.vm.config(), runtime_contracts) {
341            RuntimeGateResult::Admitted => {}
342            RuntimeGateResult::RejectedMissingContracts => {
343                return Err(CompositionError::MissingRuntimeContracts {
344                    artifact_id: cert.artifact_id.clone(),
345                });
346            }
347            RuntimeGateResult::RejectedUnsupportedDeterminismProfile => {
348                return Err(CompositionError::MissingCapability {
349                    artifact_id: cert.artifact_id.clone(),
350                    capability: format!(
351                        "determinism_profile::{:?}",
352                        self.vm.config().determinism_mode
353                    ),
354                });
355            }
356        }
357
358        let required_sched = match self.vm.config().sched_policy {
359            SchedPolicy::Cooperative => SchedulerCapability::Cooperative,
360            SchedPolicy::RoundRobin => SchedulerCapability::RoundRobin,
361            SchedPolicy::Priority(_) => SchedulerCapability::Priority,
362            SchedPolicy::ProgressAware => SchedulerCapability::ProgressAware,
363        };
364        if !caps.schedulers.contains(&required_sched) {
365            return Err(CompositionError::MissingCapability {
366                artifact_id: cert.artifact_id.clone(),
367                capability: format!("scheduler::{required_sched:?}"),
368            });
369        }
370
371        let required_det = match self.vm.config().determinism_mode {
372            DeterminismMode::Full => DeterminismCapability::Full,
373            DeterminismMode::ModuloEffects => DeterminismCapability::ModuloEffects,
374            DeterminismMode::ModuloCommutativity => DeterminismCapability::ModuloCommutativity,
375            DeterminismMode::Replay => DeterminismCapability::Replay,
376        };
377        if !caps.determinism.contains(&required_det) {
378            return Err(CompositionError::MissingCapability {
379                artifact_id: cert.artifact_id.clone(),
380                capability: format!("determinism::{required_det:?}"),
381            });
382        }
383        if !matches!(
384            self.vm.config().output_condition_policy,
385            OutputConditionPolicy::Disabled
386        ) && !caps.output_condition_gating
387        {
388            return Err(CompositionError::MissingCapability {
389                artifact_id: cert.artifact_id.clone(),
390                capability: "output_condition_gating".to_string(),
391            });
392        }
393        Ok(())
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use std::collections::BTreeMap;
400
401    use telltale_types::{GlobalType, Label, LocalTypeR};
402
403    use super::*;
404    use crate::coroutine::Value;
405
406    #[derive(Debug, Clone, Copy)]
407    struct Noop;
408
409    impl EffectHandler for Noop {
410        fn handle_send(
411            &self,
412            _role: &str,
413            _partner: &str,
414            label: &str,
415            _state: &[Value],
416        ) -> Result<Value, String> {
417            Ok(Value::Str(label.to_string()))
418        }
419
420        fn handle_recv(
421            &self,
422            _role: &str,
423            _partner: &str,
424            _label: &str,
425            _state: &mut Vec<Value>,
426            _payload: &Value,
427        ) -> Result<(), String> {
428            Ok(())
429        }
430
431        fn handle_choose(
432            &self,
433            _role: &str,
434            _partner: &str,
435            labels: &[String],
436            _state: &[Value],
437        ) -> Result<String, String> {
438            labels
439                .first()
440                .cloned()
441                .ok_or_else(|| "no labels available".to_string())
442        }
443
444        fn step(&self, _role: &str, _state: &mut Vec<Value>) -> Result<(), String> {
445            Ok(())
446        }
447    }
448
449    fn image(label: &str) -> Arc<CodeImage> {
450        let mut local_types = BTreeMap::new();
451        local_types.insert(
452            "A".to_string(),
453            LocalTypeR::send("B", Label::new(label), LocalTypeR::End),
454        );
455        local_types.insert(
456            "B".to_string(),
457            LocalTypeR::recv("A", Label::new(label), LocalTypeR::End),
458        );
459        let global = GlobalType::send("A", "B", Label::new(label), GlobalType::End);
460        Arc::new(CodeImage::from_local_types(&local_types, &global))
461    }
462
463    #[test]
464    fn proof_carrying_admission_rejects_missing_link_ok_full() {
465        let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
466        let bad = ProtocolBundle::new(
467            image("m"),
468            CompositionCertificate {
469                artifact_id: "cert/bad".to_string(),
470                link_ok_full: false,
471                theorem_pack: TheoremPackCapabilities::full(),
472                runtime_contracts: None,
473            },
474        );
475        assert!(matches!(
476            runtime.admit_bundle(bad),
477            Err(CompositionError::MissingCompatibilityProof { .. })
478        ));
479    }
480
481    #[test]
482    fn immutable_code_artifacts_are_arc_shared() {
483        let shared = image("m");
484        let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
485        let b1 = ProtocolBundle::new(
486            Arc::clone(&shared),
487            CompositionCertificate {
488                artifact_id: "cert/1".to_string(),
489                link_ok_full: true,
490                theorem_pack: TheoremPackCapabilities::full(),
491                runtime_contracts: None,
492            },
493        );
494        let b2 = ProtocolBundle::new(
495            Arc::clone(&shared),
496            CompositionCertificate {
497                artifact_id: "cert/2".to_string(),
498                link_ok_full: true,
499                theorem_pack: TheoremPackCapabilities::full(),
500                runtime_contracts: None,
501            },
502        );
503        runtime.admit_bundle(b1).expect("admit b1");
504        runtime.admit_bundle(b2).expect("admit b2");
505        assert!(
506            Arc::strong_count(&shared) >= 3,
507            "bundle admission should keep shared immutable artifacts in Arc"
508        );
509    }
510
511    #[test]
512    fn composed_execution_runs_and_usage_grows_monotonically() {
513        let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
514        let b = ProtocolBundle::new(
515            image("m"),
516            CompositionCertificate {
517                artifact_id: "cert/ok".to_string(),
518                link_ok_full: true,
519                theorem_pack: TheoremPackCapabilities::full(),
520                runtime_contracts: None,
521            },
522        );
523        runtime.admit_bundle(b).expect("admit");
524        runtime
525            .load_bundle_sessions(0, 8)
526            .expect("load composed sessions");
527        let usage_before = runtime.memory_usage().clone();
528        assert!(usage_before.session_count >= 8);
529        assert!(usage_before.coroutine_count >= 16);
530        runtime.run(&Noop, 512).expect("composed run");
531        let usage_after = runtime.memory_usage().clone();
532        assert!(usage_after.session_count >= usage_before.session_count);
533        assert!(usage_after.coroutine_count >= usage_before.coroutine_count);
534    }
535
536    #[test]
537    fn admission_rejects_missing_scheduler_profile_capability() {
538        let cfg = VMConfig {
539            sched_policy: SchedPolicy::ProgressAware,
540            ..VMConfig::default()
541        };
542        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
543        let bundle = ProtocolBundle::new(
544            image("m"),
545            CompositionCertificate {
546                artifact_id: "cert/no-sched".to_string(),
547                link_ok_full: true,
548                theorem_pack: TheoremPackCapabilities {
549                    determinism: vec![DeterminismCapability::Full],
550                    schedulers: vec![SchedulerCapability::Cooperative],
551                    output_condition_gating: true,
552                },
553                runtime_contracts: Some(RuntimeContracts::full()),
554            },
555        );
556
557        let err = runtime
558            .admit_bundle(bundle)
559            .expect_err("should reject bundle");
560        assert!(matches!(
561            err,
562            CompositionError::MissingCapability { capability, .. }
563            if capability == "scheduler::ProgressAware"
564        ));
565    }
566
567    #[test]
568    fn admission_rejects_missing_determinism_capability() {
569        let cfg = VMConfig {
570            determinism_mode: DeterminismMode::ModuloCommutativity,
571            ..VMConfig::default()
572        };
573        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
574        let bundle = ProtocolBundle::new(
575            image("m"),
576            CompositionCertificate {
577                artifact_id: "cert/no-det".to_string(),
578                link_ok_full: true,
579                theorem_pack: TheoremPackCapabilities {
580                    determinism: vec![DeterminismCapability::Full],
581                    schedulers: vec![SchedulerCapability::Cooperative],
582                    output_condition_gating: true,
583                },
584                runtime_contracts: Some(RuntimeContracts::full()),
585            },
586        );
587
588        let err = runtime
589            .admit_bundle(bundle)
590            .expect_err("should reject bundle");
591        assert!(matches!(
592            err,
593            CompositionError::MissingCapability { capability, .. }
594            if capability == "determinism::ModuloCommutativity"
595        ));
596    }
597
598    #[test]
599    fn admission_rejects_missing_output_condition_capability() {
600        let cfg = VMConfig {
601            output_condition_policy: OutputConditionPolicy::AllowAll,
602            ..VMConfig::default()
603        };
604        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
605        let bundle = ProtocolBundle::new(
606            image("m"),
607            CompositionCertificate {
608                artifact_id: "cert/no-output-gate".to_string(),
609                link_ok_full: true,
610                theorem_pack: TheoremPackCapabilities {
611                    determinism: vec![DeterminismCapability::Full],
612                    schedulers: vec![SchedulerCapability::Cooperative],
613                    output_condition_gating: false,
614                },
615                runtime_contracts: None,
616            },
617        );
618
619        let err = runtime
620            .admit_bundle(bundle)
621            .expect_err("should reject bundle");
622        assert!(matches!(
623            err,
624            CompositionError::MissingCapability { capability, .. }
625            if capability == "output_condition_gating"
626        ));
627    }
628
629    #[test]
630    fn admission_accepts_when_required_capabilities_present() {
631        let cfg = VMConfig {
632            sched_policy: SchedPolicy::RoundRobin,
633            determinism_mode: DeterminismMode::ModuloEffects,
634            output_condition_policy: OutputConditionPolicy::PredicateAllowList(vec![
635                "vm.observable_output".to_string(),
636            ]),
637            ..VMConfig::default()
638        };
639        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
640        let bundle = ProtocolBundle::new(
641            image("m"),
642            CompositionCertificate {
643                artifact_id: "cert/full".to_string(),
644                link_ok_full: true,
645                theorem_pack: TheoremPackCapabilities::full(),
646                runtime_contracts: Some(RuntimeContracts::full()),
647            },
648        );
649        runtime
650            .admit_bundle(bundle)
651            .expect("bundle should be admitted");
652    }
653
654    #[test]
655    fn admission_accepts_minimal_required_capabilities_without_full_parity() {
656        let cfg = VMConfig::default();
657        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
658        let bundle = ProtocolBundle::new(
659            image("m"),
660            CompositionCertificate {
661                artifact_id: "cert/minimal-required".to_string(),
662                link_ok_full: true,
663                theorem_pack: TheoremPackCapabilities {
664                    determinism: vec![DeterminismCapability::Full],
665                    schedulers: vec![SchedulerCapability::Cooperative],
666                    output_condition_gating: true,
667                },
668                runtime_contracts: None,
669            },
670        );
671        runtime
672            .admit_bundle(bundle)
673            .expect("minimal required capabilities should be sufficient");
674    }
675
676    #[test]
677    fn admission_rejects_advanced_mode_without_runtime_contracts() {
678        let cfg = VMConfig {
679            sched_policy: SchedPolicy::RoundRobin,
680            ..VMConfig::default()
681        };
682        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
683        let bundle = ProtocolBundle::new(
684            image("m"),
685            CompositionCertificate {
686                artifact_id: "cert/no-runtime-contracts".to_string(),
687                link_ok_full: true,
688                theorem_pack: TheoremPackCapabilities::full(),
689                runtime_contracts: None,
690            },
691        );
692        let err = runtime
693            .admit_bundle(bundle)
694            .expect_err("advanced mode should reject missing runtime contracts");
695        assert!(matches!(
696            err,
697            CompositionError::MissingRuntimeContracts { .. }
698        ));
699    }
700
701    #[test]
702    fn admission_rejects_replay_profile_without_mixed_profile_gate() {
703        let cfg = VMConfig {
704            determinism_mode: DeterminismMode::Replay,
705            ..VMConfig::default()
706        };
707        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
708        let mut contracts = RuntimeContracts::full();
709        contracts.can_use_mixed_determinism_profiles = false;
710        let bundle = ProtocolBundle::new(
711            image("m"),
712            CompositionCertificate {
713                artifact_id: "cert/no-mixed-profile-gate".to_string(),
714                link_ok_full: true,
715                theorem_pack: TheoremPackCapabilities::full(),
716                runtime_contracts: Some(contracts),
717            },
718        );
719        let err = runtime
720            .admit_bundle(bundle)
721            .expect_err("replay profile should require mixed-profile gate");
722        assert!(matches!(
723            err,
724            CompositionError::MissingCapability { capability, .. }
725            if capability == "determinism_profile::Replay"
726        ));
727    }
728
729    #[test]
730    fn admission_accepts_replay_profile_with_contracts_and_capability() {
731        let cfg = VMConfig {
732            determinism_mode: DeterminismMode::Replay,
733            ..VMConfig::default()
734        };
735        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
736        let bundle = ProtocolBundle::new(
737            image("m"),
738            CompositionCertificate {
739                artifact_id: "cert/replay-ok".to_string(),
740                link_ok_full: true,
741                theorem_pack: TheoremPackCapabilities::full(),
742                runtime_contracts: Some(RuntimeContracts::full()),
743            },
744        );
745        runtime
746            .admit_bundle(bundle)
747            .expect("replay profile should admit with matching contracts");
748    }
749}