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    use crate::effect::{EffectFailure, EffectResult};
406
407    #[derive(Debug, Clone, Copy)]
408    struct Noop;
409
410    impl EffectHandler for Noop {
411        fn handle_send(
412            &self,
413            _role: &str,
414            _partner: &str,
415            label: &str,
416            _state: &[Value],
417        ) -> EffectResult<Value> {
418            EffectResult::success(Value::Str(label.to_string()))
419        }
420
421        fn handle_recv(
422            &self,
423            _role: &str,
424            _partner: &str,
425            _label: &str,
426            _state: &mut Vec<Value>,
427            _payload: &Value,
428        ) -> EffectResult<()> {
429            EffectResult::success(())
430        }
431
432        fn handle_choose(
433            &self,
434            _role: &str,
435            _partner: &str,
436            labels: &[String],
437            _state: &[Value],
438        ) -> EffectResult<String> {
439            match labels.first().cloned() {
440                Some(label) => EffectResult::success(label),
441                None => EffectResult::failure(EffectFailure::invalid_input("no labels available")),
442            }
443        }
444
445        fn step(&self, _role: &str, _state: &mut Vec<Value>) -> EffectResult<()> {
446            EffectResult::success(())
447        }
448    }
449
450    fn image(label: &str) -> Arc<CodeImage> {
451        let mut local_types = BTreeMap::new();
452        local_types.insert(
453            "A".to_string(),
454            LocalTypeR::send("B", Label::new(label), LocalTypeR::End),
455        );
456        local_types.insert(
457            "B".to_string(),
458            LocalTypeR::recv("A", Label::new(label), LocalTypeR::End),
459        );
460        let global = GlobalType::send("A", "B", Label::new(label), GlobalType::End);
461        Arc::new(CodeImage::from_local_types(&local_types, &global))
462    }
463
464    #[test]
465    fn proof_carrying_admission_rejects_missing_link_ok_full() {
466        let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
467        let bad = ProtocolBundle::new(
468            image("m"),
469            CompositionCertificate {
470                artifact_id: "cert/bad".to_string(),
471                link_ok_full: false,
472                theorem_pack: TheoremPackCapabilities::full(),
473                runtime_contracts: None,
474            },
475        );
476        assert!(matches!(
477            runtime.admit_bundle(bad),
478            Err(CompositionError::MissingCompatibilityProof { .. })
479        ));
480    }
481
482    #[test]
483    fn immutable_code_artifacts_are_arc_shared() {
484        let shared = image("m");
485        let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
486        let b1 = ProtocolBundle::new(
487            Arc::clone(&shared),
488            CompositionCertificate {
489                artifact_id: "cert/1".to_string(),
490                link_ok_full: true,
491                theorem_pack: TheoremPackCapabilities::full(),
492                runtime_contracts: None,
493            },
494        );
495        let b2 = ProtocolBundle::new(
496            Arc::clone(&shared),
497            CompositionCertificate {
498                artifact_id: "cert/2".to_string(),
499                link_ok_full: true,
500                theorem_pack: TheoremPackCapabilities::full(),
501                runtime_contracts: None,
502            },
503        );
504        runtime.admit_bundle(b1).expect("admit b1");
505        runtime.admit_bundle(b2).expect("admit b2");
506        assert!(
507            Arc::strong_count(&shared) >= 3,
508            "bundle admission should keep shared immutable artifacts in Arc"
509        );
510    }
511
512    #[test]
513    fn composed_execution_runs_and_usage_grows_monotonically() {
514        let mut runtime = ComposedRuntime::new(VMConfig::default(), MemoryBudget::default());
515        let b = ProtocolBundle::new(
516            image("m"),
517            CompositionCertificate {
518                artifact_id: "cert/ok".to_string(),
519                link_ok_full: true,
520                theorem_pack: TheoremPackCapabilities::full(),
521                runtime_contracts: None,
522            },
523        );
524        runtime.admit_bundle(b).expect("admit");
525        runtime
526            .load_bundle_sessions(0, 8)
527            .expect("load composed sessions");
528        let usage_before = runtime.memory_usage().clone();
529        assert!(usage_before.session_count >= 8);
530        assert!(usage_before.coroutine_count >= 16);
531        runtime.run(&Noop, 512).expect("composed run");
532        let usage_after = runtime.memory_usage().clone();
533        assert!(usage_after.session_count >= usage_before.session_count);
534        assert!(usage_after.coroutine_count >= usage_before.coroutine_count);
535    }
536
537    #[test]
538    fn admission_rejects_missing_scheduler_profile_capability() {
539        let cfg = VMConfig {
540            sched_policy: SchedPolicy::ProgressAware,
541            ..VMConfig::default()
542        };
543        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
544        let bundle = ProtocolBundle::new(
545            image("m"),
546            CompositionCertificate {
547                artifact_id: "cert/no-sched".to_string(),
548                link_ok_full: true,
549                theorem_pack: TheoremPackCapabilities {
550                    determinism: vec![DeterminismCapability::Full],
551                    schedulers: vec![SchedulerCapability::Cooperative],
552                    output_condition_gating: true,
553                },
554                runtime_contracts: Some(RuntimeContracts::full()),
555            },
556        );
557
558        let err = runtime
559            .admit_bundle(bundle)
560            .expect_err("should reject bundle");
561        assert!(matches!(
562            err,
563            CompositionError::MissingCapability { capability, .. }
564            if capability == "scheduler::ProgressAware"
565        ));
566    }
567
568    #[test]
569    fn admission_rejects_missing_determinism_capability() {
570        let cfg = VMConfig {
571            determinism_mode: DeterminismMode::ModuloCommutativity,
572            ..VMConfig::default()
573        };
574        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
575        let bundle = ProtocolBundle::new(
576            image("m"),
577            CompositionCertificate {
578                artifact_id: "cert/no-det".to_string(),
579                link_ok_full: true,
580                theorem_pack: TheoremPackCapabilities {
581                    determinism: vec![DeterminismCapability::Full],
582                    schedulers: vec![SchedulerCapability::Cooperative],
583                    output_condition_gating: true,
584                },
585                runtime_contracts: Some(RuntimeContracts::full()),
586            },
587        );
588
589        let err = runtime
590            .admit_bundle(bundle)
591            .expect_err("should reject bundle");
592        assert!(matches!(
593            err,
594            CompositionError::MissingCapability { capability, .. }
595            if capability == "determinism::ModuloCommutativity"
596        ));
597    }
598
599    #[test]
600    fn admission_rejects_missing_output_condition_capability() {
601        let cfg = VMConfig {
602            output_condition_policy: OutputConditionPolicy::AllowAll,
603            ..VMConfig::default()
604        };
605        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
606        let bundle = ProtocolBundle::new(
607            image("m"),
608            CompositionCertificate {
609                artifact_id: "cert/no-output-gate".to_string(),
610                link_ok_full: true,
611                theorem_pack: TheoremPackCapabilities {
612                    determinism: vec![DeterminismCapability::Full],
613                    schedulers: vec![SchedulerCapability::Cooperative],
614                    output_condition_gating: false,
615                },
616                runtime_contracts: None,
617            },
618        );
619
620        let err = runtime
621            .admit_bundle(bundle)
622            .expect_err("should reject bundle");
623        assert!(matches!(
624            err,
625            CompositionError::MissingCapability { capability, .. }
626            if capability == "output_condition_gating"
627        ));
628    }
629
630    #[test]
631    fn admission_accepts_when_required_capabilities_present() {
632        let cfg = VMConfig {
633            sched_policy: SchedPolicy::RoundRobin,
634            determinism_mode: DeterminismMode::ModuloEffects,
635            output_condition_policy: OutputConditionPolicy::PredicateAllowList(vec![
636                "vm.observable_output".to_string(),
637            ]),
638            ..VMConfig::default()
639        };
640        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
641        let bundle = ProtocolBundle::new(
642            image("m"),
643            CompositionCertificate {
644                artifact_id: "cert/full".to_string(),
645                link_ok_full: true,
646                theorem_pack: TheoremPackCapabilities::full(),
647                runtime_contracts: Some(RuntimeContracts::full()),
648            },
649        );
650        runtime
651            .admit_bundle(bundle)
652            .expect("bundle should be admitted");
653    }
654
655    #[test]
656    fn admission_accepts_minimal_required_capabilities_without_full_parity() {
657        let cfg = VMConfig::default();
658        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
659        let bundle = ProtocolBundle::new(
660            image("m"),
661            CompositionCertificate {
662                artifact_id: "cert/minimal-required".to_string(),
663                link_ok_full: true,
664                theorem_pack: TheoremPackCapabilities {
665                    determinism: vec![DeterminismCapability::Full],
666                    schedulers: vec![SchedulerCapability::Cooperative],
667                    output_condition_gating: true,
668                },
669                runtime_contracts: None,
670            },
671        );
672        runtime
673            .admit_bundle(bundle)
674            .expect("minimal required capabilities should be sufficient");
675    }
676
677    #[test]
678    fn admission_rejects_advanced_mode_without_runtime_contracts() {
679        let cfg = VMConfig {
680            sched_policy: SchedPolicy::RoundRobin,
681            ..VMConfig::default()
682        };
683        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
684        let bundle = ProtocolBundle::new(
685            image("m"),
686            CompositionCertificate {
687                artifact_id: "cert/no-runtime-contracts".to_string(),
688                link_ok_full: true,
689                theorem_pack: TheoremPackCapabilities::full(),
690                runtime_contracts: None,
691            },
692        );
693        let err = runtime
694            .admit_bundle(bundle)
695            .expect_err("advanced mode should reject missing runtime contracts");
696        assert!(matches!(
697            err,
698            CompositionError::MissingRuntimeContracts { .. }
699        ));
700    }
701
702    #[test]
703    fn admission_rejects_replay_profile_without_mixed_profile_gate() {
704        let cfg = VMConfig {
705            determinism_mode: DeterminismMode::Replay,
706            ..VMConfig::default()
707        };
708        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
709        let mut contracts = RuntimeContracts::full();
710        contracts.can_use_mixed_determinism_profiles = false;
711        let bundle = ProtocolBundle::new(
712            image("m"),
713            CompositionCertificate {
714                artifact_id: "cert/no-mixed-profile-gate".to_string(),
715                link_ok_full: true,
716                theorem_pack: TheoremPackCapabilities::full(),
717                runtime_contracts: Some(contracts),
718            },
719        );
720        let err = runtime
721            .admit_bundle(bundle)
722            .expect_err("replay profile should require mixed-profile gate");
723        assert!(matches!(
724            err,
725            CompositionError::MissingCapability { capability, .. }
726            if capability == "determinism_profile::Replay"
727        ));
728    }
729
730    #[test]
731    fn admission_accepts_replay_profile_with_contracts_and_capability() {
732        let cfg = VMConfig {
733            determinism_mode: DeterminismMode::Replay,
734            ..VMConfig::default()
735        };
736        let mut runtime = ComposedRuntime::new(cfg, MemoryBudget::default());
737        let bundle = ProtocolBundle::new(
738            image("m"),
739            CompositionCertificate {
740                artifact_id: "cert/replay-ok".to_string(),
741                link_ok_full: true,
742                theorem_pack: TheoremPackCapabilities::full(),
743                runtime_contracts: Some(RuntimeContracts::full()),
744            },
745        );
746        runtime
747            .admit_bundle(bundle)
748            .expect("replay profile should admit with matching contracts");
749    }
750}