1use 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
32enum ReconfigurationManagerCommand {
33 RegisterBundle,
34 LinkBundles,
35 DelegateSession,
36 TransferActiveSessionOwnership,
37}
38
39#[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#[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#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct SessionDelegationOutcome {
75 pub receipt: DelegationReceipt,
76 pub witness: AuraDelegationWitness,
77}
78
79#[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#[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 #[must_use]
239 pub fn new() -> Self {
240 Self::with_runtime_capabilities(default_runtime_capability_handler())
241 }
242
243 #[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 #[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 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 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 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 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 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 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 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 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}