Skip to main content

aura_agent/runtime/services/
reconfiguration_manager.rs

1//! Runtime reconfiguration manager for link/delegate operations.
2
3use crate::core::default_context_id_for_authority;
4use crate::reconfiguration::{CoherenceStatus, ReconfigurationController, SessionFootprintClass};
5use crate::runtime::{
6    AuraDelegationCoherence, AuraDelegationWitness, AuraEffectSystem, AuraLinkBoundary,
7    OwnedVmSession, RuntimeBoundaryError, RuntimeReconfigurationEvent, RuntimeSessionOwner,
8    SessionIngressError, SessionOwnerCapabilityScope,
9};
10use aura_core::effects::PhysicalTimeEffects;
11use aura_core::time::{ProvenancedTime, TimeStamp};
12use aura_core::{
13    AuthorityId, ComposedBundle, ContextId, DelegationReceipt, OwnershipCategory, SessionFootprint,
14    SessionId,
15};
16use aura_effects::RuntimeCapabilityHandler;
17use aura_journal::fact::{ProtocolRelationalFact, RelationalFact, SessionDelegationFact};
18use aura_mpst::CompositionManifest;
19use std::collections::{BTreeMap, BTreeSet};
20use std::sync::Arc;
21use thiserror::Error;
22use tokio::sync::RwLock;
23
24#[cfg(feature = "choreo-backend-telltale-machine")]
25use telltale_machine::{
26    OwnershipReceipt, OwnershipScope, ReconfigurationRuntimeSnapshot, RuntimeUpgradeExecution,
27    RuntimeUpgradeRequest,
28};
29
30#[allow(dead_code)] // Declaration-layer ingress inventory; runtime actor wiring lands incrementally.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32enum ReconfigurationManagerCommand {
33    RegisterBundle,
34    LinkBundles,
35    DelegateSession,
36    TransferActiveSessionOwnership,
37}
38
39/// Runtime-owned reconfiguration state and lifecycle methods.
40#[derive(Clone)]
41#[aura_macros::actor_owned(
42    owner = "reconfiguration_manager",
43    domain = "runtime_reconfiguration",
44    gate = "reconfiguration_command_ingress",
45    command = ReconfigurationManagerCommand,
46    capacity = 32,
47    category = "actor_owned"
48)]
49pub struct ReconfigurationManager {
50    shared: Arc<ReconfigurationShared>,
51    runtime_capabilities: RuntimeCapabilityHandler,
52}
53
54struct ReconfigurationShared {
55    controller: RwLock<ReconfigurationController>,
56}
57
58/// Typed runtime delegation request for one session ownership transfer.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct SessionDelegationTransfer {
61    pub context_id: Option<ContextId>,
62    pub session_id: SessionId,
63    pub from_authority: AuthorityId,
64    pub to_authority: AuthorityId,
65    pub bundle_id: String,
66    pub link_boundary: Option<AuraLinkBoundary>,
67    pub capability_scope: SessionOwnerCapabilityScope,
68    #[cfg(feature = "choreo-backend-telltale-machine")]
69    pub runtime_upgrade_request: Option<RuntimeUpgradeRequest>,
70}
71
72/// Typed result for one successful runtime delegation.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct SessionDelegationOutcome {
75    pub receipt: DelegationReceipt,
76    pub witness: AuraDelegationWitness,
77}
78
79/// Typed runtime errors for one live session ownership handoff.
80#[derive(Debug, Error)]
81pub enum ActiveSessionDelegationError {
82    #[error(
83        "active session handoff target {active_session_id} does not match delegation session {transfer_session_id}"
84    )]
85    SessionMismatch {
86        active_session_id: SessionId,
87        transfer_session_id: SessionId,
88    },
89    #[error("failed to transfer live session owner for session {session_id}: {source}")]
90    OwnerTransfer {
91        session_id: SessionId,
92        #[source]
93        source: SessionIngressError,
94    },
95    #[error("live delegation failed for session {session_id}: {source}")]
96    Reconfiguration {
97        session_id: SessionId,
98        #[source]
99        source: ReconfigurationManagerError,
100    },
101    #[error(
102        "live delegation rollback failed for session {session_id} after reconfiguration error: {source}; rollback: {rollback}"
103    )]
104    RollbackFailed {
105        session_id: SessionId,
106        #[source]
107        source: ReconfigurationManagerError,
108        rollback: SessionIngressError,
109    },
110}
111
112/// Typed runtime errors for reconfiguration and delegation.
113#[derive(Debug, Error, Clone, PartialEq, Eq)]
114pub enum ReconfigurationManagerError {
115    #[error("{operation} requires protocol-critical runtime surfaces {surfaces:?}: {message}")]
116    MissingCapability {
117        operation: &'static str,
118        surfaces: &'static [&'static str],
119        message: String,
120    },
121    #[error("register bundle `{bundle_id}` failed: {message}")]
122    RegisterBundle { bundle_id: String, message: String },
123    #[error("link bundles `{left}` + `{right}` into `{linked}` failed: {message}")]
124    LinkBundles {
125        left: String,
126        right: String,
127        linked: String,
128        message: String,
129    },
130    #[error("delegation timestamp unavailable for session {session_id}: {message}")]
131    DelegationTimestamp {
132        session_id: SessionId,
133        message: String,
134    },
135    #[error("delegation requires pre-registered bundle `{bundle_id}`")]
136    BundleNotRegistered { bundle_id: String },
137    #[error(
138        "delegation for session {session_id} rejected link boundary for bundle `{bundle_id}`: {source}"
139    )]
140    InvalidLinkBoundary {
141        session_id: SessionId,
142        bundle_id: String,
143        #[source]
144        source: RuntimeBoundaryError,
145    },
146    #[error("session delegation failed for session {session_id}: {message}")]
147    DelegateSession {
148        session_id: SessionId,
149        message: String,
150    },
151    #[cfg(feature = "choreo-backend-telltale-machine")]
152    #[error("runtime upgrade for bundle `{bundle_id}` failed: {message}")]
153    RuntimeUpgrade { bundle_id: String, message: String },
154    #[error("failed to persist delegation fact for session {session_id}: {message}")]
155    PersistDelegationFact {
156        session_id: SessionId,
157        message: String,
158    },
159    #[error(
160        "reconfiguration coherence violation after delegation for session {session_id}: {details}"
161    )]
162    CoherenceViolation {
163        session_id: SessionId,
164        details: String,
165    },
166}
167
168impl SessionDelegationTransfer {
169    pub const OWNERSHIP_CATEGORY: OwnershipCategory = OwnershipCategory::MoveOwned;
170
171    #[must_use]
172    pub fn new(
173        session_id: SessionId,
174        from_authority: AuthorityId,
175        to_authority: AuthorityId,
176        bundle_id: impl Into<String>,
177    ) -> Self {
178        let bundle_id = bundle_id.into();
179        let link_boundary = AuraLinkBoundary::for_bundle_id(bundle_id.clone());
180        Self {
181            context_id: None,
182            session_id,
183            from_authority,
184            to_authority,
185            bundle_id,
186            capability_scope: link_boundary.capability_scope.clone(),
187            link_boundary: Some(link_boundary),
188            #[cfg(feature = "choreo-backend-telltale-machine")]
189            runtime_upgrade_request: None,
190        }
191    }
192
193    #[must_use]
194    pub fn with_context(mut self, context_id: ContextId) -> Self {
195        self.context_id = Some(context_id);
196        self
197    }
198
199    #[must_use]
200    pub fn with_link_boundary(mut self, link_boundary: AuraLinkBoundary) -> Self {
201        self.capability_scope = link_boundary.capability_scope.clone();
202        self.link_boundary = Some(link_boundary);
203        self
204    }
205
206    #[must_use]
207    pub fn with_capability_scope(mut self, capability_scope: SessionOwnerCapabilityScope) -> Self {
208        self.capability_scope = capability_scope;
209        self
210    }
211
212    #[cfg(feature = "choreo-backend-telltale-machine")]
213    #[must_use]
214    pub fn with_runtime_upgrade_request(
215        mut self,
216        runtime_upgrade_request: RuntimeUpgradeRequest,
217    ) -> Self {
218        self.runtime_upgrade_request = Some(runtime_upgrade_request);
219        self
220    }
221}
222
223impl Default for ReconfigurationManager {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229impl ReconfigurationManager {
230    pub const OWNERSHIP_CATEGORY: OwnershipCategory = OwnershipCategory::MoveOwned;
231    const REQUIRED_RUNTIME_SURFACES: &'static [&'static str] = &[
232        "ownership_receipt",
233        "semantic_handoff",
234        "reconfiguration_transition",
235    ];
236
237    /// Create a new manager.
238    #[must_use]
239    pub fn new() -> Self {
240        Self::with_runtime_capabilities(default_runtime_capability_handler())
241    }
242
243    /// Create a new manager with explicit runtime capability admission state.
244    #[must_use]
245    pub fn with_runtime_capabilities(runtime_capabilities: RuntimeCapabilityHandler) -> Self {
246        let mut controller = ReconfigurationController::new();
247        for bundle in generated_linkable_bundles() {
248            controller
249                .register_bundle(bundle)
250                .expect("generated reconfiguration bundles must be unique and valid");
251        }
252        Self {
253            shared: Arc::new(ReconfigurationShared {
254                controller: RwLock::new(controller),
255            }),
256            runtime_capabilities,
257        }
258    }
259
260    /// Clone the runtime capability admission snapshot used by this manager.
261    #[must_use]
262    pub fn runtime_capability_handler(&self) -> RuntimeCapabilityHandler {
263        self.runtime_capabilities.clone()
264    }
265
266    async fn require_reconfiguration_capability(
267        &self,
268        operation: &'static str,
269    ) -> Result<(), ReconfigurationManagerError> {
270        self.runtime_capabilities
271            .require_protocol_critical_surfaces(Self::REQUIRED_RUNTIME_SURFACES)
272            .map_err(|error| ReconfigurationManagerError::MissingCapability {
273                operation,
274                surfaces: Self::REQUIRED_RUNTIME_SURFACES,
275                message: error.to_string(),
276            })
277    }
278
279    /// Register one composable bundle in the runtime lifecycle.
280    pub async fn register_bundle(
281        &self,
282        bundle: ComposedBundle,
283    ) -> Result<(), ReconfigurationManagerError> {
284        let bundle_id = bundle.bundle_id.clone();
285        let mut controller = self.shared.controller.write().await;
286        controller.register_bundle(bundle).map_err(|e| {
287            ReconfigurationManagerError::RegisterBundle {
288                bundle_id: bundle_id.clone(),
289                message: e.to_string(),
290            }
291        })?;
292        tracing::info!(bundle_id = %bundle_id, "registered reconfiguration bundle");
293        Ok(())
294    }
295
296    /// Snapshot one registered bundle by id.
297    pub async fn bundle(&self, bundle_id: &str) -> Option<ComposedBundle> {
298        let controller = self.shared.controller.read().await;
299        controller.bundle(bundle_id).cloned()
300    }
301
302    /// Snapshot one authority footprint by id.
303    pub async fn footprint(&self, authority: AuthorityId) -> Option<SessionFootprint> {
304        let controller = self.shared.controller.read().await;
305        controller.footprint(&authority).cloned()
306    }
307
308    /// Link two bundles into one composed runtime bundle.
309    pub async fn link_bundles(
310        &self,
311        bundle_a: &str,
312        bundle_b: &str,
313        linked_bundle_id: impl Into<String>,
314    ) -> Result<ComposedBundle, ReconfigurationManagerError> {
315        self.require_reconfiguration_capability("bundle linking")
316            .await?;
317        let linked_bundle_id = linked_bundle_id.into();
318        let mut controller = self.shared.controller.write().await;
319        let linked = controller
320            .link(bundle_a, bundle_b, linked_bundle_id.clone())
321            .map_err(|e| ReconfigurationManagerError::LinkBundles {
322                left: bundle_a.to_string(),
323                right: bundle_b.to_string(),
324                linked: linked_bundle_id.clone(),
325                message: e.to_string(),
326            })?;
327        tracing::info!(
328            left = %bundle_a,
329            right = %bundle_b,
330            linked = %linked_bundle_id,
331            sessions = linked.session_footprint.all_sessions().len(),
332            boundaries = ?AuraLinkBoundary::for_bundle(&linked),
333            "linked reconfiguration bundle"
334        );
335        Ok(linked)
336    }
337
338    /// Record that `authority` currently owns `session_id` natively.
339    pub async fn record_native_session(&self, authority: AuthorityId, session_id: SessionId) {
340        let mut controller = self.shared.controller.write().await;
341        controller.footprint_extend(authority, session_id, SessionFootprintClass::Native);
342    }
343
344    /// Delegate one live session by moving the active owner first and rolling it back on failure.
345    pub async fn delegate_active_session(
346        &self,
347        effects: &AuraEffectSystem,
348        session: &mut OwnedVmSession,
349        transfer: SessionDelegationTransfer,
350        next_owner_label: impl Into<String>,
351    ) -> Result<SessionDelegationOutcome, ActiveSessionDelegationError> {
352        let active_session_id = session.owner().session_id.into_aura_session_id();
353        if active_session_id != transfer.session_id {
354            return Err(ActiveSessionDelegationError::SessionMismatch {
355                active_session_id,
356                transfer_session_id: transfer.session_id,
357            });
358        }
359
360        let next_owner_label = next_owner_label.into();
361        let next_boundary = self
362            .resolve_delegation_boundary(
363                transfer.session_id,
364                &transfer.bundle_id,
365                transfer.link_boundary.clone(),
366                &transfer.capability_scope,
367            )
368            .await
369            .map_err(|source| ActiveSessionDelegationError::Reconfiguration {
370                session_id: transfer.session_id,
371                source,
372            })?;
373
374        let previous_owner_label = session.owner().owner_label.clone();
375        let previous_boundary = session.routing_boundary().clone();
376        let previous_owner = session.owner().clone();
377
378        session
379            .transfer_owner_in_place(next_owner_label, next_boundary.clone())
380            .map_err(|source| ActiveSessionDelegationError::OwnerTransfer {
381                session_id: transfer.session_id,
382                source,
383            })?;
384        #[cfg(feature = "choreo-backend-telltale-machine")]
385        let ownership_receipt = build_active_session_ownership_receipt(
386            session.vm_session_id(),
387            &previous_owner,
388            session.owner(),
389        );
390
391        let delegation = self
392            .delegate_session(effects, transfer.with_link_boundary(next_boundary))
393            .await;
394        match delegation {
395            Ok(mut outcome) => {
396                #[cfg(feature = "choreo-backend-telltale-machine")]
397                {
398                    outcome.witness = outcome.witness.with_ownership_receipt(ownership_receipt);
399                }
400                Ok(outcome)
401            }
402            Err(source) => {
403                let rollback =
404                    session.transfer_owner_in_place(previous_owner_label, previous_boundary);
405                match rollback {
406                    Ok(()) => Err(ActiveSessionDelegationError::Reconfiguration {
407                        session_id: active_session_id,
408                        source,
409                    }),
410                    Err(rollback) => Err(ActiveSessionDelegationError::RollbackFailed {
411                        session_id: active_session_id,
412                        source,
413                        rollback,
414                    }),
415                }
416            }
417        }
418    }
419
420    /// Delegate one session with audit fact persistence and coherence checks.
421    pub async fn delegate_session(
422        &self,
423        effects: &AuraEffectSystem,
424        transfer: SessionDelegationTransfer,
425    ) -> Result<SessionDelegationOutcome, ReconfigurationManagerError> {
426        self.require_reconfiguration_capability("session delegation")
427            .await?;
428        let SessionDelegationTransfer {
429            context_id,
430            session_id,
431            from_authority,
432            to_authority,
433            bundle_id,
434            link_boundary,
435            capability_scope,
436            #[cfg(feature = "choreo-backend-telltale-machine")]
437            runtime_upgrade_request,
438        } = transfer;
439        let timestamp = effects.physical_time().await.map_err(|e| {
440            ReconfigurationManagerError::DelegationTimestamp {
441                session_id,
442                message: e.to_string(),
443            }
444        })?;
445        let delegated_at = ProvenancedTime {
446            stamp: TimeStamp::PhysicalClock(timestamp.clone()),
447            proofs: vec![],
448            origin: None,
449        };
450
451        let resolved_context_id =
452            context_id.unwrap_or_else(|| default_context_id_for_authority(from_authority));
453
454        let boundary = self
455            .resolve_delegation_boundary(session_id, &bundle_id, link_boundary, &capability_scope)
456            .await?;
457
458        let (receipt, runtime_upgrade_execution, runtime_upgrade_snapshot, controller_snapshot) = {
459            let mut controller = self.shared.controller.write().await;
460            let controller_snapshot = controller.clone();
461            let receipt = controller
462                .delegate(
463                    session_id,
464                    from_authority,
465                    to_authority,
466                    Some(bundle_id.clone()),
467                    delegated_at,
468                )
469                .map_err(|e| ReconfigurationManagerError::DelegateSession {
470                    session_id,
471                    message: e.to_string(),
472                })?;
473            #[cfg(feature = "choreo-backend-telltale-machine")]
474            let runtime_upgrade_execution = if let Some(request) = runtime_upgrade_request.as_ref()
475            {
476                match controller.execute_runtime_upgrade(&bundle_id, request) {
477                    Ok(execution) => Some(execution),
478                    Err(error) => {
479                        *controller = controller_snapshot.clone();
480                        return Err(ReconfigurationManagerError::RuntimeUpgrade {
481                            bundle_id: bundle_id.clone(),
482                            message: error.to_string(),
483                        });
484                    }
485                }
486            } else {
487                None
488            };
489            #[cfg(not(feature = "choreo-backend-telltale-machine"))]
490            let runtime_upgrade_execution = None;
491            #[cfg(feature = "choreo-backend-telltale-machine")]
492            let runtime_upgrade_snapshot = if runtime_upgrade_request.is_some() {
493                match controller.runtime_upgrade_snapshot(&bundle_id) {
494                    Ok(snapshot) => Some(snapshot),
495                    Err(error) => {
496                        *controller = controller_snapshot.clone();
497                        return Err(ReconfigurationManagerError::RuntimeUpgrade {
498                            bundle_id: bundle_id.clone(),
499                            message: error.to_string(),
500                        });
501                    }
502                }
503            } else {
504                None
505            };
506            #[cfg(not(feature = "choreo-backend-telltale-machine"))]
507            let runtime_upgrade_snapshot = None;
508            (
509                receipt,
510                runtime_upgrade_execution,
511                runtime_upgrade_snapshot,
512                controller_snapshot,
513            )
514        };
515
516        let fact = RelationalFact::Protocol(ProtocolRelationalFact::SessionDelegation(
517            SessionDelegationFact {
518                context_id: resolved_context_id,
519                session_id,
520                from_authority,
521                to_authority,
522                bundle_id: Some(bundle_id.clone()),
523                timestamp,
524            },
525        ));
526
527        if let Err(error) = effects.commit_relational_facts(vec![fact]).await {
528            let mut controller = self.shared.controller.write().await;
529            *controller = controller_snapshot;
530            return Err(ReconfigurationManagerError::PersistDelegationFact {
531                session_id,
532                message: error.to_string(),
533            });
534        }
535
536        let witness = AuraDelegationWitness::new(
537            resolved_context_id,
538            session_id,
539            from_authority,
540            to_authority,
541            bundle_id.clone(),
542            boundary,
543            capability_scope,
544        )
545        .with_coherence(AuraDelegationCoherence::Preserved);
546        #[cfg(feature = "choreo-backend-telltale-machine")]
547        let witness = {
548            let witness = if let Some(request) = runtime_upgrade_request {
549                witness.with_runtime_upgrade_request(request)
550            } else {
551                witness
552            };
553            let witness = if let Some(snapshot) = runtime_upgrade_snapshot {
554                witness.with_runtime_upgrade_snapshot(snapshot)
555            } else {
556                witness
557            };
558            if let Some(execution) = runtime_upgrade_execution {
559                witness.with_runtime_upgrade_execution(execution)
560            } else {
561                witness
562            }
563        };
564
565        tracing::info!(
566            event = RuntimeReconfigurationEvent::DelegationPersisted.as_event_name(),
567            session_id = %session_id,
568            from_authority = %from_authority,
569            to_authority = %to_authority,
570            bundle_id = %bundle_id,
571            witness = ?witness,
572            "delegated session and persisted audit fact"
573        );
574
575        Ok(SessionDelegationOutcome { receipt, witness })
576    }
577
578    /// Verify global coherence and emit diagnostics on violations.
579    pub async fn verify_coherence(&self) -> CoherenceStatus {
580        let controller = self.shared.controller.read().await;
581        let status = controller.verify_coherence();
582        if let CoherenceStatus::Violations(violations) = &status {
583            tracing::error!(
584                violations = ?violations,
585                "reconfiguration coherence check failed"
586            );
587        }
588        status
589    }
590
591    #[cfg(feature = "choreo-backend-telltale-machine")]
592    pub async fn seed_runtime_upgrade_membership<I, S>(
593        &self,
594        bundle_id: &str,
595        members: I,
596    ) -> Result<(), ReconfigurationManagerError>
597    where
598        I: IntoIterator<Item = S>,
599        S: Into<String>,
600    {
601        let mut controller = self.shared.controller.write().await;
602        controller
603            .seed_runtime_upgrade_membership(bundle_id, members)
604            .map_err(|error| ReconfigurationManagerError::RuntimeUpgrade {
605                bundle_id: bundle_id.to_string(),
606                message: error.to_string(),
607            })
608    }
609
610    #[cfg(feature = "choreo-backend-telltale-machine")]
611    pub async fn runtime_upgrade_snapshot(
612        &self,
613        bundle_id: &str,
614    ) -> Result<ReconfigurationRuntimeSnapshot, ReconfigurationManagerError> {
615        let controller = self.shared.controller.read().await;
616        controller
617            .runtime_upgrade_snapshot(bundle_id)
618            .map_err(|error| ReconfigurationManagerError::RuntimeUpgrade {
619                bundle_id: bundle_id.to_string(),
620                message: error.to_string(),
621            })
622    }
623
624    #[cfg(feature = "choreo-backend-telltale-machine")]
625    pub async fn execute_runtime_upgrade(
626        &self,
627        bundle_id: &str,
628        request: &RuntimeUpgradeRequest,
629    ) -> Result<RuntimeUpgradeExecution, ReconfigurationManagerError> {
630        self.require_reconfiguration_capability("runtime upgrade")
631            .await?;
632        let mut controller = self.shared.controller.write().await;
633        controller
634            .execute_runtime_upgrade(bundle_id, request)
635            .map_err(|error| ReconfigurationManagerError::RuntimeUpgrade {
636                bundle_id: bundle_id.to_string(),
637                message: error.to_string(),
638            })
639    }
640}
641
642#[allow(clippy::result_large_err)]
643fn validate_link_boundary(
644    session_id: SessionId,
645    bundle_id: &str,
646    boundary: &AuraLinkBoundary,
647    capability_scope: &SessionOwnerCapabilityScope,
648) -> Result<(), ReconfigurationManagerError> {
649    if boundary.bundle_id.as_deref() != Some(bundle_id) {
650        return Err(ReconfigurationManagerError::InvalidLinkBoundary {
651            session_id,
652            bundle_id: bundle_id.to_string(),
653            source: RuntimeBoundaryError::LinkBoundaryBundleMismatch {
654                session_id,
655                bundle_id: bundle_id.to_string(),
656                boundary_bundle_id: boundary.bundle_id.clone(),
657            },
658        });
659    }
660
661    if &boundary.capability_scope != capability_scope {
662        return Err(ReconfigurationManagerError::InvalidLinkBoundary {
663            session_id,
664            bundle_id: bundle_id.to_string(),
665            source: RuntimeBoundaryError::LinkBoundaryScopeMismatch {
666                session_id,
667                bundle_id: bundle_id.to_string(),
668                boundary_scope: boundary.capability_scope.clone(),
669                capability_scope: capability_scope.clone(),
670            },
671        });
672    }
673
674    Ok(())
675}
676
677fn composed_bundle_from_manifest(manifest: &CompositionManifest) -> Vec<ComposedBundle> {
678    let mut bundles = BTreeMap::<String, ComposedBundle>::new();
679    for spec in &manifest.link_specs {
680        let bundle = bundles.entry(spec.bundle_id.clone()).or_insert_with(|| {
681            ComposedBundle::new(
682                spec.bundle_id.clone(),
683                vec![manifest.protocol_id.clone()],
684                BTreeSet::new(),
685                BTreeSet::new(),
686                SessionFootprint::new(),
687            )
688        });
689        if !bundle
690            .protocol_ids
691            .iter()
692            .any(|id| id == &manifest.protocol_id)
693        {
694            bundle.protocol_ids.push(manifest.protocol_id.clone());
695        }
696        bundle.exports.extend(spec.exports.iter().cloned());
697        bundle.imports.extend(spec.imports.iter().cloned());
698    }
699    bundles.into_values().collect()
700}
701
702fn generated_linkable_bundles() -> Vec<ComposedBundle> {
703    let manifests = [
704        aura_invitation::protocol::device_enrollment::telltale_session_types_invitation_device_enrollment::vm_artifacts::composition_manifest(),
705        aura_recovery::guardian_membership::telltale_session_types_guardian_membership_change::vm_artifacts::composition_manifest(),
706        aura_sync::protocols::epochs::telltale_session_types_epoch_rotation::vm_artifacts::composition_manifest(),
707    ];
708    let mut grouped = BTreeMap::<String, ComposedBundle>::new();
709
710    for manifest in manifests {
711        for bundle in composed_bundle_from_manifest(&manifest) {
712            let entry = grouped.entry(bundle.bundle_id.clone()).or_insert_with(|| {
713                ComposedBundle::new(
714                    bundle.bundle_id.clone(),
715                    Vec::new(),
716                    BTreeSet::new(),
717                    BTreeSet::new(),
718                    SessionFootprint::new(),
719                )
720            });
721            entry.protocol_ids.extend(bundle.protocol_ids);
722            entry.exports.extend(bundle.exports);
723            entry.imports.extend(bundle.imports);
724        }
725    }
726
727    grouped.into_values().collect()
728}
729
730impl ReconfigurationManager {
731    async fn resolve_delegation_boundary(
732        &self,
733        session_id: SessionId,
734        bundle_id: &str,
735        requested_boundary: Option<AuraLinkBoundary>,
736        capability_scope: &SessionOwnerCapabilityScope,
737    ) -> Result<AuraLinkBoundary, ReconfigurationManagerError> {
738        let controller = self.shared.controller.read().await;
739        let Some(bundle) = controller.bundle(bundle_id).cloned() else {
740            return Err(ReconfigurationManagerError::BundleNotRegistered {
741                bundle_id: bundle_id.to_string(),
742            });
743        };
744        let boundary = requested_boundary.unwrap_or_else(|| AuraLinkBoundary::for_bundle(&bundle));
745        validate_link_boundary(session_id, bundle_id, &boundary, capability_scope)?;
746        Ok(boundary)
747    }
748}
749
750fn default_runtime_capability_handler() -> RuntimeCapabilityHandler {
751    #[cfg(feature = "choreo-backend-telltale-machine")]
752    {
753        let contracts = telltale_machine::runtime_contracts::RuntimeContracts::full();
754        RuntimeCapabilityHandler::from_protocol_machine_runtime_contracts(&contracts)
755    }
756
757    #[cfg(not(feature = "choreo-backend-telltale-machine"))]
758    {
759        RuntimeCapabilityHandler::from_pairs([("reconfiguration", true)])
760    }
761}
762
763#[cfg(feature = "choreo-backend-telltale-machine")]
764fn build_active_session_ownership_receipt(
765    vm_session_id: telltale_machine::SessionId,
766    previous_owner: &RuntimeSessionOwner,
767    next_owner: &RuntimeSessionOwner,
768) -> OwnershipReceipt {
769    OwnershipReceipt {
770        session_id: vm_session_id,
771        claim_id: next_owner.capability.generation,
772        from_owner_id: previous_owner.owner_label.clone(),
773        from_generation: previous_owner.capability.generation,
774        to_owner_id: next_owner.owner_label.clone(),
775        to_generation: next_owner.capability.generation,
776        scope: telltale_scope_for_capability_scope(&next_owner.capability.scope),
777    }
778}
779
780#[cfg(feature = "choreo-backend-telltale-machine")]
781fn telltale_scope_for_capability_scope(scope: &SessionOwnerCapabilityScope) -> OwnershipScope {
782    match scope {
783        SessionOwnerCapabilityScope::Session => OwnershipScope::Session,
784        SessionOwnerCapabilityScope::Fragments(fragments) => {
785            OwnershipScope::Fragments(fragments.clone())
786        }
787    }
788}
789
790#[cfg(test)]
791mod tests {
792    use super::*;
793    use crate::core::AgentConfig;
794    use crate::runtime::{
795        open_owned_manifest_vm_session_admitted, AuraEffectSystem, AuraLinkBoundary,
796        AuraVmSchedulerSignals,
797    };
798    use aura_protocol::effects::{ChoreographicRole, RoleIndex};
799    use std::collections::BTreeSet as StdBTreeSet;
800    use std::sync::Arc;
801    use uuid::Uuid;
802
803    fn authority(seed: u8) -> AuthorityId {
804        AuthorityId::new_from_entropy([seed; 32])
805    }
806
807    fn session(seed: u8) -> SessionId {
808        SessionId::from_uuid(Uuid::from_bytes([seed; 16]))
809    }
810
811    fn epoch_rotation_vm_artifacts(
812        participant_authority: AuthorityId,
813        coordinator_authority: AuthorityId,
814    ) -> (
815        Vec<ChoreographicRole>,
816        CompositionManifest,
817        aura_mpst::upstream::types::GlobalType,
818        BTreeMap<String, aura_mpst::upstream::types::LocalTypeR>,
819    ) {
820        let roles = vec![
821            ChoreographicRole::for_authority(
822                coordinator_authority,
823                RoleIndex::new(0).expect("coordinator"),
824            ),
825            ChoreographicRole::for_authority(
826                participant_authority,
827                RoleIndex::new(0).expect("participant"),
828            ),
829        ];
830        (
831            roles,
832            aura_sync::protocols::epochs::telltale_session_types_epoch_rotation::vm_artifacts::composition_manifest(),
833            aura_sync::protocols::epochs::telltale_session_types_epoch_rotation::vm_artifacts::global_type(),
834            aura_sync::protocols::epochs::telltale_session_types_epoch_rotation::vm_artifacts::local_types(),
835        )
836    }
837
838    #[test]
839    fn session_delegation_transfer_keeps_context_separate_from_bundle_evidence() {
840        let session_id = SessionId::new_from_entropy([1; 32]);
841        let from_authority = AuthorityId::new_from_entropy([2; 32]);
842        let to_authority = AuthorityId::new_from_entropy([3; 32]);
843        let context_id = ContextId::new_from_entropy([4; 32]);
844
845        let transfer =
846            SessionDelegationTransfer::new(session_id, from_authority, to_authority, "bundle-a")
847                .with_context(context_id);
848
849        assert_eq!(transfer.session_id, session_id);
850        assert_eq!(transfer.from_authority, from_authority);
851        assert_eq!(transfer.to_authority, to_authority);
852        assert_eq!(transfer.context_id, Some(context_id));
853        assert_eq!(transfer.bundle_id, "bundle-a");
854        assert_eq!(
855            transfer
856                .link_boundary
857                .as_ref()
858                .and_then(|boundary| boundary.bundle_id.as_deref()),
859            Some("bundle-a")
860        );
861        assert!(matches!(
862            transfer.capability_scope,
863            SessionOwnerCapabilityScope::Fragments(_)
864        ));
865    }
866
867    #[tokio::test]
868    async fn active_session_handoff_moves_owner_fragment_and_footprint_together() {
869        let from_authority = authority(1);
870        let to_authority = authority(2);
871        let session_id = session(77);
872        let effects = Arc::new(
873            AuraEffectSystem::simulation_for_test_for_authority(
874                &AgentConfig::default(),
875                from_authority,
876            )
877            .expect("simulation effect system"),
878        );
879        let manager = ReconfigurationManager::new();
880        manager
881            .record_native_session(from_authority, session_id)
882            .await;
883
884        let (roles, manifest, global_type, local_types) =
885            epoch_rotation_vm_artifacts(from_authority, authority(9));
886        let bundle_id = manifest
887            .link_specs
888            .first()
889            .expect("epoch manifest should expose one link boundary")
890            .bundle_id
891            .clone();
892        let mut active_session = open_owned_manifest_vm_session_admitted(
893            effects.clone(),
894            session_id.uuid(),
895            roles,
896            &manifest,
897            "Participant",
898            &global_type,
899            &local_types,
900            AuraVmSchedulerSignals::default(),
901        )
902        .await
903        .expect("open active session");
904        let stale_owner = active_session.owner().clone();
905
906        let outcome = manager
907            .delegate_active_session(
908                effects.as_ref(),
909                &mut active_session,
910                SessionDelegationTransfer::new(
911                    session_id,
912                    from_authority,
913                    to_authority,
914                    bundle_id.clone(),
915                ),
916                "delegated-owner",
917            )
918            .await
919            .expect("delegate active session");
920
921        assert_eq!(active_session.owner().owner_label, "delegated-owner");
922        assert_eq!(
923            active_session.owner().capability.scope,
924            SessionOwnerCapabilityScope::Fragments(StdBTreeSet::from([format!(
925                "bundle:{bundle_id}"
926            ),]))
927        );
928        assert_eq!(
929            outcome.witness.link_boundary.bundle_id.as_deref(),
930            Some(bundle_id.as_str())
931        );
932        #[cfg(feature = "choreo-backend-telltale-machine")]
933        {
934            let ownership_receipt = outcome
935                .witness
936                .ownership_receipt
937                .as_ref()
938                .expect("active handoff should publish an ownership receipt");
939            assert_eq!(ownership_receipt.session_id, active_session.vm_session_id());
940            assert_eq!(ownership_receipt.from_owner_id, stale_owner.owner_label);
941            assert_eq!(ownership_receipt.to_owner_id, "delegated-owner");
942            assert_eq!(
943                ownership_receipt.scope,
944                telltale_machine::OwnershipScope::Fragments(StdBTreeSet::from([format!(
945                    "bundle:{bundle_id}"
946                ),]))
947            );
948            assert_eq!(
949                ownership_receipt.to_generation,
950                stale_owner.capability.generation.saturating_add(1)
951            );
952        }
953        assert_eq!(manager.verify_coherence().await, CoherenceStatus::Coherent);
954        assert!(manager
955            .footprint(from_authority)
956            .await
957            .expect("from footprint")
958            .delegated_out_sessions
959            .contains(&session_id));
960        assert!(manager
961            .footprint(to_authority)
962            .await
963            .expect("to footprint")
964            .delegated_in_sessions
965            .contains(&session_id));
966
967        let fragment_snapshot = effects.vm_fragment_snapshot();
968        assert!(
969            fragment_snapshot
970                .iter()
971                .all(|(_, owner)| owner.owner_label == "delegated-owner"),
972            "fragment ownership must move with the live session handoff"
973        );
974        assert!(
975            effects
976                .assert_owned_choreography_session(active_session.owner())
977                .is_err(),
978            "fragment-scoped delegated owner should no longer authorize full-session ingress"
979        );
980        effects
981            .assert_owned_choreography_boundary(
982                active_session.owner(),
983                active_session.routing_boundary(),
984            )
985            .expect("delegated owner should retain its delegated boundary");
986        assert!(
987            effects
988                .assert_owned_choreography_boundary(
989                    &stale_owner,
990                    active_session.routing_boundary(),
991                )
992                .is_err(),
993            "stale owner must be rejected after live handoff"
994        );
995
996        active_session
997            .transfer_owner_in_place(
998                "owner-a",
999                AuraLinkBoundary::for_scope(SessionOwnerCapabilityScope::Session),
1000            )
1001            .expect("restore full-session owner for cleanup");
1002        active_session
1003            .close()
1004            .await
1005            .expect("close restored session");
1006    }
1007
1008    #[tokio::test]
1009    async fn active_session_handoff_rolls_back_owner_and_fragments_on_failure() {
1010        let from_authority = authority(1);
1011        let to_authority = authority(3);
1012        let session_id = session(78);
1013        let effects = Arc::new(
1014            AuraEffectSystem::simulation_for_test_for_authority(
1015                &AgentConfig::default(),
1016                from_authority,
1017            )
1018            .expect("simulation effect system"),
1019        );
1020        let manager = ReconfigurationManager::with_runtime_capabilities(
1021            RuntimeCapabilityHandler::from_pairs([("reconfiguration", false)]),
1022        );
1023        manager
1024            .record_native_session(from_authority, session_id)
1025            .await;
1026
1027        let (roles, manifest, global_type, local_types) =
1028            epoch_rotation_vm_artifacts(from_authority, authority(9));
1029        let bundle_id = manifest
1030            .link_specs
1031            .first()
1032            .expect("epoch manifest should expose one link boundary")
1033            .bundle_id
1034            .clone();
1035        let mut active_session = open_owned_manifest_vm_session_admitted(
1036            effects.clone(),
1037            session_id.uuid(),
1038            roles,
1039            &manifest,
1040            "Participant",
1041            &global_type,
1042            &local_types,
1043            AuraVmSchedulerSignals::default(),
1044        )
1045        .await
1046        .expect("open active session");
1047        let original_owner = active_session.owner().clone();
1048
1049        let error = manager
1050            .delegate_active_session(
1051                effects.as_ref(),
1052                &mut active_session,
1053                SessionDelegationTransfer::new(session_id, from_authority, to_authority, bundle_id),
1054                "delegated-owner",
1055            )
1056            .await
1057            .expect_err("missing reconfiguration capability must fail closed");
1058
1059        assert!(matches!(
1060            error,
1061            ActiveSessionDelegationError::Reconfiguration { .. }
1062        ));
1063        assert_eq!(
1064            active_session.owner().owner_label,
1065            original_owner.owner_label
1066        );
1067        assert_eq!(
1068            active_session.owner().capability.scope,
1069            active_session.routing_boundary().capability_scope
1070        );
1071        effects
1072            .assert_owned_choreography_boundary(
1073                active_session.owner(),
1074                active_session.routing_boundary(),
1075            )
1076            .expect("original owner must be restored after rollback");
1077        assert!(
1078            manager.footprint(to_authority).await.is_none(),
1079            "failed handoff must not create delegated-in ownership"
1080        );
1081        let fragment_snapshot = effects.vm_fragment_snapshot();
1082        assert!(
1083            fragment_snapshot
1084                .iter()
1085                .all(|(_, owner)| owner.owner_label == original_owner.owner_label),
1086            "fragment ownership must roll back with the owner"
1087        );
1088
1089        active_session
1090            .transfer_owner_in_place(
1091                original_owner.owner_label,
1092                AuraLinkBoundary::for_scope(SessionOwnerCapabilityScope::Session),
1093            )
1094            .expect("restore full-session owner for cleanup");
1095        active_session
1096            .close()
1097            .await
1098            .expect("close rolled-back session");
1099    }
1100}