1use std::{
2 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
3 fmt,
4 path::{Path, PathBuf},
5 sync::{Arc, Mutex, RwLock},
6 time::{Duration, Instant as StdInstant},
7};
8
9use serde::{Deserialize, Serialize};
10use subc_control::{
11 ops, CapabilityRequirementStatus, CatalogEntry, ClientControlPush, ClientControlRequest,
12 ClientControlResponse, ConsumerIdentity, DaemonBuildProvenance, DaemonObservedProcess,
13 ModuleDeclaredProvenance, ModuleProtocol, NotReadyReason, PendingReloadVerdict, PollKind,
14 ReloadPathAgreement, ReloadPathUnavailableReason, RouteCloseReason, SpawnCursor,
15 StderrCaptureState, StderrTail, StderrTailEntry, SupervisorDaemonProvenance, SupervisorEntry,
16 SupervisorHealthEntry, SupervisorModuleProvenance, SupervisorObservedProcess,
17 SupervisorRescanResult, SupervisorRoute, SupervisorRouteConsumer, SupervisorRouteModule,
18};
19use subc_protocol::{
20 error_codes,
21 manifest::{
22 validate_hello_capability_grammar, validate_hello_event_declarations,
23 validate_hello_self_signal_declarations, CapabilityDeclarations, CapabilityNeed,
24 Concurrency, ManifestProvenance, ModuleManifest, ProviderRole,
25 },
26 scope::{
27 ScopeRecord, ScopeRecordOutcome, ScopeRecordResult, ScopeSelector,
28 CAP_ROUTE_ROLE_VERSIONS_V1, CAP_SCOPES_V1, SCOPE_DESCRIBE_OP, SCOPE_SYNC_OP,
29 },
30 session::{
31 validate_role_versions, HealthReport, ModuleControlPush, ModuleControlRequest,
32 ModuleControlRequestFromModule, ModuleControlResponse, ModuleControlResponseToModule,
33 MODULE_CONTROL_OP_HEALTH_CHECK, MODULE_TO_SUBC_OP_CATALOG_UPDATE, ROLE_VERSIONS_FIELD,
34 },
35 BindIdentity, ErrorBody, Flags, FrameType, ModuleHelloAckBody, ModuleHelloBody, Principal,
36 Priority, RouteTarget, PROTOCOL_VERSION,
37};
38use tokio::time::{timeout_at, Instant};
39use tracing::{debug, info, warn};
40
41use crate::{
42 capability_requirements::{
43 log_duplicate_claim_events, log_requirement_events, CapabilityRequirementEvaluator,
44 CapabilityVerdict, DuplicateClaimSource, RegisteredModule, RequirementStatus,
45 RuntimeModule,
46 },
47 daemon_config::RestartRequiredSection,
48 forwarding::{
49 CloseReason, EndpointRoute, ForwardingError, ForwardingTable, GoodbyeTarget,
50 ModuleControlRpcCompletion, ModuleControlRpcOutcome, ModuleEndpointId,
51 PendingModuleControlRpc, RouteBindRelayOutcome, RoutePollSnapshot, RouteRelease,
52 },
53 observability::{
54 ROUTE_OPEN_REFUSED_DECLARED_NOT_READY, ROUTE_OPEN_REFUSED_REQUIRED_CAPABILITY_UNPROVIDED,
55 },
56 provenance::{
57 process_start_time, spawned_file_identity, ExecutableIdentityProbe, SpawnedFileIdentity,
58 },
59 registry::{ChannelState, ConnectionId, Registry, RegistryError},
60 router::{RouteCtx, RouterError},
61 scopes::{BoundScope, HelloLaunchNonces, ScopeTable},
62 server::MAX_PENDING_ROUTE_BINDS_PER_TARGET,
63 stderr_tail::{CaptureState, TailEntry},
64 supervise::{
65 validate_spec, ModuleProcessLiveness, ReservedHelloRejection, SpawnSubscribeRefusal,
66 SupervisorHandle, SwapHelloAdmission,
67 },
68 ConnectedClients, DaemonCounters, Frame, ProjectRootId, Supervisor,
69};
70
71pub const MIN_SUPPORTED_VERSION: u8 = PROTOCOL_VERSION;
77
78const CAP_MANIFEST_REGISTRATION: &str = "manifest_registration_v1";
79const CAP_CHANNEL_LIFECYCLE: &str = "channel_lifecycle_v1";
80const CAP_PING_PONG: &str = "ping_pong_v1";
81const CAP_SESSION_ATTACH: &str = "session_attach_v1";
82const CAP_ADMISSION_FACTS_RELAY: &str = "admission_facts_relay_v1";
83
84const SUBC_CONTROL_OPS: &[&str] = &[
85 ops::SERVER_DESCRIBE,
86 ops::CATALOG_LIST,
87 ops::ROUTE_OPEN,
88 ops::ROUTE_POLL,
89 ops::ROUTE_CLOSING,
90 ops::ROUTE_CLOSED,
91 ops::SUPERVISOR_LIST,
92 ops::SUPERVISOR_RESTART,
93 ops::SUPERVISOR_SWAP,
94 ops::SUPERVISOR_RELOAD,
95 ops::SUPERVISOR_RESCAN,
96 ops::SUPERVISOR_RELEASE_RESERVED,
97 ops::SUPERVISOR_SET_ENABLED,
98 ops::SUPERVISOR_HEALTH_PROBE,
99 ops::SUPERVISOR_HEALTH,
100 ops::SUPERVISOR_STDERR_TAIL,
101 ops::SUPERVISOR_TERMINALS,
102 ops::SUPERVISOR_ROUTES,
103 ops::SUPERVISOR_PROVENANCE,
104 ops::SUPERVISOR_SPAWN_SNAPSHOT,
105 ops::SUPERVISOR_SPAWN_SUBSCRIBE,
106];
107
108const MODULE_TO_SUBC_CONTROL_OPS: &[&str] = &[
109 MODULE_TO_SUBC_OP_CATALOG_UPDATE,
110 "supervisor.live_roots",
111 SCOPE_SYNC_OP,
112 SCOPE_DESCRIBE_OP,
113];
114
115const MODULE_TO_SUBC_UNADVERTISED_OPS: &[&str] = &[];
119
120const MODULE_BASELINE_CONTROL_OPS: &[&str] = &["route.bind", "route.status"];
121
122pub const DEFAULT_ROUTE_BIND_RELAY_TIMEOUT: Duration = Duration::from_secs(12);
130
131pub const DEFAULT_ROUTE_BIND_BREAKER_THRESHOLD: u32 = 3;
146
147pub const DEFAULT_ROUTE_BIND_BREAKER_COOLDOWN: Duration = Duration::from_secs(20);
165
166const DEFAULT_HEALTH_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
167const SLOW_CONTROL_DISPATCH_THRESHOLD: Duration = Duration::from_secs(1);
168
169fn reload_verdict(
170 configured: &Path,
171 spawned_from: Option<&Path>,
172 image: subc_control::RunningImageAgreement,
173) -> PendingReloadVerdict {
174 let path = match spawned_from {
175 Some(spawned_from) if configured == spawned_from => ReloadPathAgreement::Match,
176 Some(spawned_from) => ReloadPathAgreement::Mismatch {
177 configured: configured.to_path_buf(),
178 spawned_from: spawned_from.to_path_buf(),
179 },
180 None => ReloadPathAgreement::Unavailable {
181 reason: if matches!(
182 image,
183 subc_control::RunningImageAgreement::Unavailable {
184 reason: subc_control::RunningImageUnavailableReason::NotRunning
185 }
186 ) {
187 ReloadPathUnavailableReason::NotRunning
188 } else {
189 ReloadPathUnavailableReason::SpawnedPathUnavailable
190 },
191 },
192 };
193 PendingReloadVerdict { path, image }
194}
195
196#[derive(Clone)]
197struct DaemonProvenanceFacts {
198 build: DaemonBuildProvenance,
199 pid: Option<u32>,
200 started_at_ms: Option<u64>,
201 start_clock: Option<crate::clock::StartClock>,
202 executable_path: Option<PathBuf>,
203 executable_identity: Option<SpawnedFileIdentity>,
204 process_start_time: Option<u64>,
205 probe: ExecutableIdentityProbe,
206}
207
208impl Default for DaemonProvenanceFacts {
209 fn default() -> Self {
210 Self {
211 build: DaemonBuildProvenance {
212 build_git_sha: None,
213 build_lock_digest: None,
214 },
215 pid: None,
216 started_at_ms: None,
217 start_clock: None,
218 executable_path: None,
219 executable_identity: None,
220 process_start_time: None,
221 probe: ExecutableIdentityProbe::default(),
222 }
223 }
224}
225
226#[derive(Debug, Clone)]
227struct SupervisorRescanContext {
228 supervisor: Supervisor,
229 config_path: PathBuf,
230 configured_port: Option<u16>,
231 storage_config: Option<crate::daemon_config::StorageConfig>,
232 admission_facts_carrier_module_id: Option<String>,
233 admission_facts_targets: Option<Vec<String>>,
234 scope_authority_owners: Vec<String>,
235}
236
237const ROUTE_OPEN_NOT_SERVING_REASONS: &[&str] = &[
258 "reloading",
259 "supervisor_not_live",
260 "registration_not_active",
261 "no_forwarding_connection",
262 "relay_send_failed",
263];
264
265#[derive(Clone)]
267pub struct ControlHandler {
268 registry: Arc<Registry>,
269 forwarding: Arc<ForwardingTable>,
270 process_liveness: Option<Arc<dyn ModuleProcessLiveness>>,
271 supervisor: SupervisorHandle,
272 subc_capabilities: Arc<[String]>,
273 route_bind_relay_timeout: Duration,
277 route_bind_relay_timeouts: BTreeMap<String, Duration>,
281 route_bind_breakers: RouteBindBreakers,
284 route_bind_concurrency: RouteBindConcurrency,
287 route_outages: Arc<crate::route_outage::RouteOutageTracker>,
292 route_bind_breaker_threshold: u32,
294 route_bind_breaker_cooldown: Duration,
296 health_probe_timeout: Duration,
297 storage_config: Option<crate::daemon_config::StorageConfig>,
300 machine_id: Option<crate::machine_id::MachineId>,
304 admission_facts_carrier_module_id: Option<String>,
305 admission_facts_targets: Option<Vec<String>>,
306 scopes: Arc<RwLock<ScopeTable>>,
310 scope_authority_owners: Vec<String>,
313 hello_launch_nonces: Arc<Mutex<HelloLaunchNonces>>,
316 rescan: Option<SupervisorRescanContext>,
317 connected_clients: ConnectedClients,
318 counters: DaemonCounters,
319 capability_evaluator: Arc<CapabilityRequirementEvaluator>,
320 daemon_provenance: DaemonProvenanceFacts,
321 #[cfg(test)]
322 control_dispatch_delay: Option<Duration>,
323 #[cfg(test)]
324 provenance_probe_override: Option<subc_control::RunningImageAgreement>,
325}
326
327impl fmt::Debug for ControlHandler {
328 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329 f.debug_struct("ControlHandler")
330 .field("registry", &self.registry)
331 .field("forwarding", &self.forwarding)
332 .field("process_liveness", &self.process_liveness.is_some())
333 .field("supervisor", &self.supervisor)
334 .field("subc_capabilities", &self.subc_capabilities)
335 .finish()
336 }
337}
338
339struct RouteOpenRequest {
340 target: RouteTarget,
341 identity: BindIdentity,
342 consumer_identity: Option<ConsumerIdentity>,
343 consumer_capabilities: Option<Vec<String>>,
344 role_versions: Option<BTreeMap<String, String>>,
345 admission_facts: Option<serde_json::Value>,
346 scope: Option<ScopeSelector>,
347}
348
349struct RouteBindReservationGuard {
350 forwarding: Arc<ForwardingTable>,
351 endpoint: ModuleEndpointId,
352 relay_corr: u64,
353 armed: bool,
354}
355
356struct ModuleControlRpcGuard {
357 forwarding: Arc<ForwardingTable>,
358 endpoint: ModuleEndpointId,
359 corr: u64,
360 armed: bool,
361}
362
363impl ModuleControlRpcGuard {
364 fn new(forwarding: Arc<ForwardingTable>, endpoint: ModuleEndpointId, corr: u64) -> Self {
365 Self {
366 forwarding,
367 endpoint,
368 corr,
369 armed: true,
370 }
371 }
372
373 fn disarm(&mut self) {
374 self.armed = false;
375 }
376}
377
378impl Drop for ModuleControlRpcGuard {
379 fn drop(&mut self) {
380 if self.armed {
381 let _ = self
382 .forwarding
383 .cancel_module_control_rpc(self.endpoint, self.corr);
384 }
385 }
386}
387
388impl RouteBindReservationGuard {
389 fn new(forwarding: Arc<ForwardingTable>, endpoint: ModuleEndpointId, relay_corr: u64) -> Self {
390 Self {
391 forwarding,
392 endpoint,
393 relay_corr,
394 armed: true,
395 }
396 }
397
398 fn release_and_disarm(&mut self) {
399 if !self.armed {
400 return;
401 }
402 if let Ok(Some(target)) = self.forwarding.abort_pending_relay(
403 self.endpoint,
404 self.relay_corr,
405 RouteBindRelayOutcome::ModuleGone("route.open handler canceled".to_string()),
406 ) {
407 send_goodbye_target_best_effort(
408 &self.forwarding.counters(),
409 &target,
410 "canceled route.bind",
411 );
412 }
413 self.armed = false;
414 }
415
416 fn disarm(&mut self) {
417 self.armed = false;
418 }
419}
420
421impl Drop for RouteBindReservationGuard {
422 fn drop(&mut self) {
423 self.release_and_disarm();
424 }
425}
426
427#[derive(Debug, Clone, Default)]
457pub(crate) struct RouteBindBreakers {
458 modules: Arc<Mutex<HashMap<String, ModuleBreakerState>>>,
459}
460
461#[derive(Debug, Clone, Default)]
462pub(crate) struct RouteBindConcurrency {
463 modules: Arc<Mutex<HashMap<String, usize>>>,
464}
465
466struct RouteBindConcurrencyGuard {
467 concurrency: RouteBindConcurrency,
468 module_id: String,
469}
470
471impl RouteBindConcurrency {
472 fn try_admit(&self, module_id: &str, limit: usize) -> Result<RouteBindConcurrencyGuard, usize> {
475 let mut modules = self
476 .modules
477 .lock()
478 .expect("route.bind concurrency mutex poisoned");
479 let in_flight = modules.entry(module_id.to_string()).or_default();
480 if *in_flight >= limit {
481 return Err(*in_flight);
482 }
483 *in_flight += 1;
484 Ok(RouteBindConcurrencyGuard {
485 concurrency: self.clone(),
486 module_id: module_id.to_string(),
487 })
488 }
489}
490
491impl Drop for RouteBindConcurrencyGuard {
492 fn drop(&mut self) {
493 let mut modules = self
494 .concurrency
495 .modules
496 .lock()
497 .expect("route.bind concurrency mutex poisoned");
498 let remove = {
499 let in_flight = modules
500 .get_mut(&self.module_id)
501 .expect("admitted route.bind has a concurrency entry");
502 *in_flight -= 1;
503 *in_flight == 0
504 };
505 if remove {
506 modules.remove(&self.module_id);
507 }
508 }
509}
510
511#[derive(Debug, Default)]
512struct ModuleBreakerState {
513 consecutive_timeouts: u32,
515 cooldown_until: Option<Instant>,
518 probe_in_flight: Option<Arc<()>>,
523}
524
525enum RouteBindAdmission<'a> {
527 Admitted {
528 guard: RouteBindBreakerGuard<'a>,
529 probe: bool,
532 },
533 Refused {
534 consecutive_timeouts: u32,
535 retry_in: Duration,
539 probe_in_flight: bool,
540 },
541}
542
543struct RouteBindBreakerGuard<'a> {
550 breakers: RouteBindBreakers,
551 module_id: &'a str,
552 probe_token: Option<Arc<()>>,
553 settled: bool,
554}
555
556impl RouteBindBreakerGuard<'_> {
557 fn record_accepted(&mut self) -> bool {
561 self.settled = true;
562 self.breakers.record_accepted(self.module_id)
563 }
564
565 fn record_timeout(&mut self, threshold: u32, cooldown: Duration) -> Option<BreakerOpened> {
568 self.settled = true;
569 self.breakers.record_timeout(
570 self.module_id,
571 self.probe_token.as_ref(),
572 threshold,
573 cooldown,
574 )
575 }
576
577 fn record_inconclusive(&mut self) {
586 self.settled = true;
587 self.breakers
588 .record_inconclusive(self.module_id, self.probe_token.as_ref());
589 }
590}
591
592impl Drop for RouteBindBreakerGuard<'_> {
593 fn drop(&mut self) {
594 if !self.settled {
595 self.breakers
596 .record_inconclusive(self.module_id, self.probe_token.as_ref());
597 }
598 }
599}
600
601struct BreakerOpened {
605 consecutive_timeouts: u32,
606 reopened_after_probe: bool,
609}
610
611impl RouteBindBreakers {
612 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, ModuleBreakerState>> {
613 self.modules
614 .lock()
615 .expect("route.bind breaker mutex poisoned")
616 }
617
618 fn admit<'a>(&self, module_id: &'a str) -> RouteBindAdmission<'a> {
621 let admitted = |probe_token: Option<Arc<()>>| RouteBindAdmission::Admitted {
622 probe: probe_token.is_some(),
623 guard: RouteBindBreakerGuard {
624 breakers: self.clone(),
625 module_id,
626 probe_token,
627 settled: false,
628 },
629 };
630
631 let mut modules = self.lock();
632 let Some(state) = modules.get_mut(module_id) else {
633 return admitted(None);
634 };
635 let Some(cooldown_until) = state.cooldown_until else {
636 return admitted(None);
637 };
638 if state.probe_in_flight.is_some() {
639 return RouteBindAdmission::Refused {
640 consecutive_timeouts: state.consecutive_timeouts,
641 retry_in: Duration::ZERO,
642 probe_in_flight: true,
643 };
644 }
645 let now = Instant::now();
646 if now < cooldown_until {
647 return RouteBindAdmission::Refused {
648 consecutive_timeouts: state.consecutive_timeouts,
649 retry_in: cooldown_until - now,
650 probe_in_flight: false,
651 };
652 }
653 let token = Arc::new(());
654 state.probe_in_flight = Some(Arc::clone(&token));
655 admitted(Some(token))
656 }
657
658 fn record_accepted(&self, module_id: &str) -> bool {
659 self.lock()
660 .remove(module_id)
661 .is_some_and(|state| state.cooldown_until.is_some())
662 }
663
664 fn record_timeout(
665 &self,
666 module_id: &str,
667 probe_token: Option<&Arc<()>>,
668 threshold: u32,
669 cooldown: Duration,
670 ) -> Option<BreakerOpened> {
671 let mut modules = self.lock();
672 let state = modules.entry(module_id.to_string()).or_default();
673 let was_open = state.cooldown_until.is_some();
674 let was_probe = Self::owns_probe(state, probe_token);
675 if was_probe {
676 state.probe_in_flight = None;
677 }
678 state.consecutive_timeouts = state.consecutive_timeouts.saturating_add(1);
679 if state.consecutive_timeouts < threshold {
680 return None;
681 }
682 state.cooldown_until = Some(Instant::now() + cooldown);
683 Some(BreakerOpened {
684 consecutive_timeouts: state.consecutive_timeouts,
685 reopened_after_probe: was_open && was_probe,
686 })
687 }
688
689 fn owns_probe(state: &ModuleBreakerState, token: Option<&Arc<()>>) -> bool {
690 state
694 .probe_in_flight
695 .as_ref()
696 .zip(token)
697 .is_some_and(|(active, token)| Arc::ptr_eq(active, token))
698 }
699
700 fn record_inconclusive(&self, module_id: &str, probe_token: Option<&Arc<()>>) {
701 if let Some(state) = self.lock().get_mut(module_id) {
702 if Self::owns_probe(state, probe_token) {
703 state.probe_in_flight = None;
704 }
705 }
706 }
707
708 pub(crate) fn reset_for_new_module_connection(&self, module_id: &str) -> Option<u32> {
723 self.lock()
724 .remove(module_id)
725 .map(|state| state.consecutive_timeouts)
726 .filter(|discarded| *discarded > 0)
727 }
728
729 fn open_snapshot(&self) -> Option<serde_json::Value> {
736 let now = Instant::now();
737 let modules = self.lock();
738 let open = modules
739 .iter()
740 .filter_map(|(module_id, state)| {
741 let cooldown_until = state.cooldown_until?;
742 Some((
743 module_id.clone(),
744 serde_json::json!({
745 "consecutive_timeouts": state.consecutive_timeouts,
746 "cooldown_remaining_ms":
747 cooldown_until.saturating_duration_since(now).as_millis() as u64,
748 "probe_in_flight": state.probe_in_flight.is_some(),
749 }),
750 ))
751 })
752 .collect::<serde_json::Map<String, serde_json::Value>>();
753 (!open.is_empty()).then_some(serde_json::Value::Object(open))
754 }
755}
756
757impl ControlHandler {
758 pub fn new(registry: Arc<Registry>) -> Self {
759 Self::with_forwarding(registry, Arc::new(ForwardingTable::default()))
760 }
761
762 pub fn with_forwarding(registry: Arc<Registry>, forwarding: Arc<ForwardingTable>) -> Self {
763 let counters = forwarding.counters();
764 let route_bind_breakers = forwarding.route_bind_breakers();
768 let route_bind_concurrency = forwarding.route_bind_concurrency();
769 let route_outages = forwarding.route_outages();
770 Self {
771 registry,
772 forwarding,
773 process_liveness: None,
774 supervisor: SupervisorHandle::new(),
775 subc_capabilities: Arc::from([
776 CAP_MANIFEST_REGISTRATION.to_string(),
777 CAP_CHANNEL_LIFECYCLE.to_string(),
778 CAP_PING_PONG.to_string(),
779 CAP_SESSION_ATTACH.to_string(),
780 CAP_ADMISSION_FACTS_RELAY.to_string(),
781 CAP_SCOPES_V1.to_string(),
782 CAP_ROUTE_ROLE_VERSIONS_V1.to_string(),
783 ]),
784 route_bind_relay_timeout: DEFAULT_ROUTE_BIND_RELAY_TIMEOUT,
785 route_bind_relay_timeouts: BTreeMap::new(),
786 route_bind_breakers,
787 route_bind_concurrency,
788 route_outages,
789 route_bind_breaker_threshold: DEFAULT_ROUTE_BIND_BREAKER_THRESHOLD,
790 route_bind_breaker_cooldown: DEFAULT_ROUTE_BIND_BREAKER_COOLDOWN,
791 health_probe_timeout: DEFAULT_HEALTH_PROBE_TIMEOUT,
792 storage_config: None,
793 machine_id: None,
794 admission_facts_carrier_module_id: None,
795 admission_facts_targets: None,
796 scopes: Arc::new(RwLock::new(ScopeTable::new(
797 crate::daemon_config::default_scope_authority_owners(),
798 ))),
799 scope_authority_owners: crate::daemon_config::default_scope_authority_owners(),
800 hello_launch_nonces: Arc::new(Mutex::new(HelloLaunchNonces::default())),
801 rescan: None,
802 connected_clients: ConnectedClients::new(),
803 counters,
804 capability_evaluator: Arc::new(CapabilityRequirementEvaluator::new()),
805 daemon_provenance: DaemonProvenanceFacts::default(),
806 #[cfg(test)]
807 control_dispatch_delay: None,
808 #[cfg(test)]
809 provenance_probe_override: None,
810 }
811 }
812
813 pub fn with_storage_config(
816 mut self,
817 storage_config: Option<crate::daemon_config::StorageConfig>,
818 ) -> Self {
819 self.storage_config = storage_config;
820 self
821 }
822
823 pub fn with_machine_id(mut self, machine_id: Option<crate::machine_id::MachineId>) -> Self {
826 self.machine_id = machine_id;
827 self
828 }
829
830 pub fn with_admission_facts_config(
834 mut self,
835 carrier_module_id: Option<String>,
836 targets: Option<Vec<String>>,
837 ) -> Self {
838 self.admission_facts_carrier_module_id = carrier_module_id;
839 self.admission_facts_targets = targets;
840 self
841 }
842
843 pub fn with_scope_authority_owners(mut self, owners: Vec<String>) -> Self {
847 self.scopes = Arc::new(RwLock::new(ScopeTable::new(owners.iter().cloned())));
848 self.scope_authority_owners = owners;
849 self
850 }
851
852 pub fn with_route_bind_relay_timeout(mut self, timeout: Duration) -> Self {
855 self.route_bind_relay_timeout = timeout;
856 self
857 }
858
859 pub fn with_route_bind_relay_timeouts(
865 mut self,
866 timeouts: impl IntoIterator<Item = (String, Duration)>,
867 ) -> Self {
868 self.route_bind_relay_timeouts = timeouts.into_iter().collect();
869 self
870 }
871
872 pub fn route_bind_relay_timeout_for(&self, module_id: &str) -> Duration {
878 self.route_bind_relay_timeouts
879 .get(module_id)
880 .copied()
881 .unwrap_or(self.route_bind_relay_timeout)
882 }
883
884 pub fn with_route_bind_breaker(mut self, threshold: u32, cooldown: Duration) -> Self {
892 self.route_bind_breaker_threshold = threshold.max(1);
893 self.route_bind_breaker_cooldown = cooldown;
894 self
895 }
896
897 #[cfg(test)]
898 pub(crate) fn with_health_probe_timeout(mut self, timeout: Duration) -> Self {
899 self.health_probe_timeout = timeout;
900 self
901 }
902
903 #[cfg(test)]
904 pub(crate) fn with_control_dispatch_delay(mut self, delay: Duration) -> Self {
905 self.control_dispatch_delay = Some(delay);
906 self
907 }
908
909 pub fn with_process_liveness(
910 mut self,
911 process_liveness: Arc<dyn ModuleProcessLiveness>,
912 ) -> Self {
913 self.process_liveness = Some(process_liveness);
914 self
915 }
916
917 pub fn with_supervisor(mut self, supervisor: SupervisorHandle) -> Self {
918 self.supervisor = supervisor;
919 self
920 }
921
922 pub fn with_daemon_provenance(
923 mut self,
924 pid: u32,
925 started_at_ms: u64,
926 executable_path: Option<PathBuf>,
927 build_git_sha: Option<String>,
928 build_lock_digest: Option<String>,
929 ) -> Self {
930 let executable_identity = executable_path.as_deref().and_then(spawned_file_identity);
931 let process_start_time = process_start_time(pid);
932 self.daemon_provenance = DaemonProvenanceFacts {
933 build: DaemonBuildProvenance {
934 build_git_sha,
935 build_lock_digest,
936 },
937 pid: Some(pid),
938 started_at_ms: Some(started_at_ms),
939 start_clock: None,
940 executable_path,
941 executable_identity,
942 process_start_time,
943 probe: ExecutableIdentityProbe::default(),
944 };
945 self
946 }
947
948 pub(crate) fn with_daemon_start_clock(mut self, clock: crate::clock::StartClock) -> Self {
949 self.daemon_provenance.start_clock = Some(clock);
950 self
951 }
952
953 #[cfg(test)]
954 fn with_provenance_probe_result(mut self, result: subc_control::RunningImageAgreement) -> Self {
955 self.provenance_probe_override = Some(result);
956 self
957 }
958
959 pub fn with_capability_config(
963 self,
964 modules: impl IntoIterator<Item = (String, bool)>,
965 reserved_capabilities: BTreeMap<String, String>,
966 ) -> Self {
967 self.capability_evaluator
968 .configure(modules, reserved_capabilities);
969 self
970 }
971
972 pub fn with_supervisor_rescan(
973 mut self,
974 supervisor: Supervisor,
975 config_path: impl Into<PathBuf>,
976 configured_port: Option<u16>,
977 ) -> Self {
978 self.rescan = Some(SupervisorRescanContext {
979 supervisor,
980 config_path: config_path.into(),
981 configured_port,
982 storage_config: self.storage_config.clone(),
983 admission_facts_carrier_module_id: self.admission_facts_carrier_module_id.clone(),
984 admission_facts_targets: self.admission_facts_targets.clone(),
985 scope_authority_owners: self.scope_authority_owners.clone(),
986 });
987 self
988 }
989
990 pub fn with_connected_clients(mut self, connected_clients: ConnectedClients) -> Self {
991 self.connected_clients = connected_clients;
992 self
993 }
994
995 pub fn forwarding(&self) -> Arc<ForwardingTable> {
996 Arc::clone(&self.forwarding)
997 }
998
999 pub(crate) fn counters(&self) -> DaemonCounters {
1000 self.counters.clone()
1001 }
1002
1003 pub fn spawn_capability_deadline_loop(self: Arc<Self>) {
1006 tokio::spawn(async move {
1007 loop {
1008 self.capability_evaluator
1009 .wait_for_change_or_deadline()
1010 .await;
1011 self.refresh_capability_requirements();
1012 }
1013 });
1014 }
1015
1016 fn runtime_capability_snapshot(
1017 &self,
1018 ) -> Result<(Vec<RuntimeModule>, Vec<RegisteredModule>), RouterError> {
1019 let runtime = self
1020 .supervisor
1021 .list()
1022 .into_iter()
1023 .map(|module| {
1024 let status = module.status().map_err(|err| {
1025 RouterError::backend(0, 0, format!("failed to read capability status: {err}"))
1026 })?;
1027 Ok(RuntimeModule {
1028 module_id: status.module_id,
1029 state: status.state,
1030 enabled: status.enabled,
1031 })
1032 })
1033 .collect::<Result<Vec<_>, RouterError>>()?;
1034 let (_, registrations) = self.registry.list_modules().map_err(|err| {
1035 RouterError::backend(
1036 0,
1037 0,
1038 format!("failed to list capability registrations: {err}"),
1039 )
1040 })?;
1041 let registrations = registrations
1042 .into_iter()
1043 .map(|registration| RegisteredModule {
1044 module_id: registration.manifest.module_id,
1045 module_version: registration.manifest.module_version,
1046 capabilities: registration.manifest.capabilities,
1047 })
1048 .collect();
1049 Ok((runtime, registrations))
1050 }
1051
1052 fn apply_registration_capabilities(&self, registration: &crate::registry::ModuleRegistration) {
1059 let cached_registration = RegisteredModule {
1060 module_id: registration.manifest.module_id.clone(),
1061 module_version: registration.manifest.module_version.clone(),
1062 capabilities: registration.manifest.capabilities.clone(),
1063 };
1064 if self.capability_evaluator.record_hello(&cached_registration) {
1065 warn!(
1066 module_id = %cached_registration.module_id,
1067 "capability claims drifted from the cached manifest"
1068 );
1069 }
1070 if capability_census_trigger(None, registration.manifest.capabilities.as_ref()) {
1071 self.enforce_capability_denies();
1072 }
1073 self.refresh_capability_requirements();
1074 }
1075
1076 pub(crate) fn install_swap_promotion_observer(self: &Arc<Self>) {
1080 let observer: std::sync::Weak<dyn crate::supervise::SwapPromotionObserver> =
1081 Arc::downgrade(self) as std::sync::Weak<ControlHandler>;
1082 self.supervisor.set_swap_promotion_observer(observer);
1083 }
1084
1085 pub fn refresh_capability_requirements(&self) {
1086 match self.runtime_capability_snapshot() {
1087 Ok((runtime, registrations)) => {
1088 log_requirement_events(
1089 self.capability_evaluator
1090 .evaluate_now(&runtime, ®istrations),
1091 );
1092 }
1093 Err(err) => warn!(error = %err, "failed to recompute capability requirements"),
1094 }
1095 }
1096
1097 fn enforce_capability_denies(&self) {
1101 let (_, registrations) = match self.registry.list_modules() {
1102 Ok(snapshot) => snapshot,
1103 Err(err) => {
1104 warn!(error = %err, "failed to read registrations for capability deny census");
1105 return;
1106 }
1107 };
1108 let manifests = registrations
1109 .into_iter()
1110 .map(|registration| {
1111 (
1112 registration.manifest.module_id.clone(),
1113 registration.manifest,
1114 )
1115 })
1116 .collect::<BTreeMap<_, _>>();
1117 let census = match self.forwarding.route_census(None) {
1118 Ok(census) => census,
1119 Err(err) => {
1120 warn!(error = %err, "failed to read route census for capability deny enforcement");
1121 return;
1122 }
1123 };
1124
1125 for (target_module_id, routes) in census {
1126 let Some(target_manifest) = manifests.get(&target_module_id) else {
1127 continue;
1128 };
1129 let mut closed_routes = Vec::new();
1130 let mut module_goodbyes = Vec::new();
1131 for route in routes {
1132 let Principal::Reserved {
1133 module_id: opening_module_id,
1134 } = &route.principal
1135 else {
1136 continue;
1137 };
1138 let Some(opening_manifest) = manifests.get(opening_module_id) else {
1139 continue;
1140 };
1141 let Some(capability) = denied_capability(opening_manifest, target_manifest) else {
1142 continue;
1143 };
1144
1145 match self.forwarding.release_client_route(
1146 route.goodbye_target.connection_id,
1147 route.goodbye_target.channel,
1148 route.goodbye_target.epoch,
1149 ) {
1150 Ok(RouteRelease::Removed(module_goodbye)) => {
1151 warn!(
1152 opening_module_id,
1153 target_module_id,
1154 capability,
1155 "force-closing route because an attested capability deny edge now matches"
1156 );
1157 closed_routes.push(route);
1158 module_goodbyes.push(module_goodbye);
1159 }
1160 Ok(RouteRelease::Stale | RouteRelease::Absent) => {}
1161 Err(err) => warn!(
1162 opening_module_id,
1163 target_module_id,
1164 capability,
1165 error = %err,
1166 "failed to force-close capability-denied route"
1167 ),
1168 }
1169 }
1170
1171 if closed_routes.is_empty() {
1172 continue;
1173 }
1174 send_route_control_pushes(
1175 &self.forwarding,
1176 closed_routes,
1177 ClientControlPush::RouteClosed {
1178 module_id: target_module_id,
1179 channels: Vec::new(),
1180 reason: RouteCloseReason::CapabilityDenied,
1181 drained: false,
1182 abandoned: 0,
1183 excluded_subscriptions: 0,
1184 terminal: Some(false),
1185 },
1186 );
1187 self.emit_route_goodbyes(module_goodbyes);
1188 }
1189 }
1190
1191 fn not_ready_reason(
1197 &self,
1198 registration: &crate::registry::ModuleRegistration,
1199 ) -> Option<NotReadyReason> {
1200 if !registration.ready {
1201 return Some(NotReadyReason {
1202 reason: NotReadyReason::DECLARED_NOT_READY.to_string(),
1203 capability: None,
1204 });
1205 }
1206 self.first_unprovided_required_capability(registration)
1207 .map(|capability| NotReadyReason {
1208 reason: NotReadyReason::REQUIRED_CAPABILITY_UNPROVIDED.to_string(),
1209 capability: Some(capability),
1210 })
1211 }
1212
1213 fn first_unprovided_required_capability(
1231 &self,
1232 registration: &crate::registry::ModuleRegistration,
1233 ) -> Option<String> {
1234 let required = registration
1235 .manifest
1236 .capabilities
1237 .iter()
1238 .flat_map(|declarations| declarations.requires.iter())
1239 .filter(|requirement| requirement.need == CapabilityNeed::Required)
1240 .map(|requirement| requirement.capability.as_str())
1241 .collect::<BTreeSet<_>>();
1242 if required.is_empty() {
1243 return None;
1244 }
1245 let module_id = registration.manifest.module_id.as_str();
1246 let verdict = |capability: &str| self.capability_evaluator.verdict(module_id, capability);
1247 if required
1248 .iter()
1249 .any(|capability| verdict(capability).is_none())
1250 {
1251 self.refresh_capability_requirements();
1252 }
1253 required
1254 .into_iter()
1255 .find(|capability| verdict(capability) != Some(CapabilityVerdict::Provided))
1256 .map(str::to_string)
1257 }
1258
1259 fn capability_requirement_statuses(&self) -> Vec<CapabilityRequirementStatus> {
1260 self.capability_evaluator
1261 .statuses()
1262 .into_iter()
1263 .map(capability_requirement_status)
1264 .collect()
1265 }
1266
1267 fn deregister_connection(
1274 &self,
1275 connection_id: ConnectionId,
1276 ) -> Result<Vec<crate::registry::ModuleRegistration>, RegistryError> {
1277 self.registry.deregister_connection(connection_id)
1278 }
1279
1280 pub(crate) fn route_open_target(&self, frame: &Frame) -> Option<String> {
1281 if frame.header.channel != 0 || frame.header.ty != FrameType::Request {
1282 return None;
1283 }
1284 let Ok(ClientControlRequest::RouteOpen { target, .. }) =
1285 parse_client_control_request(&frame.body)
1286 else {
1287 return None;
1288 };
1289 Some(target_module_id(&target).to_string())
1290 }
1291
1292 pub(crate) fn route_open_capacity_refusal(
1293 &self,
1294 ctx: &RouteCtx,
1295 frame: &Frame,
1296 target_module_id: &str,
1297 in_flight: usize,
1298 limit: usize,
1299 ) -> Result<Frame, RouterError> {
1300 self.route_open_admission_refusal_frame(
1301 ctx,
1302 frame,
1303 target_module_id,
1304 "open_admission_full",
1305 (in_flight, limit),
1306 format!(
1307 "connection already has {in_flight} route.open binds in flight (limit {limit}); retry after one settles"
1308 ),
1309 )
1310 }
1311
1312 fn route_open_target_capacity_refusal(
1313 &self,
1314 ctx: &RouteCtx,
1315 frame: &Frame,
1316 target_module_id: &str,
1317 in_flight: usize,
1318 ) -> Result<Frame, RouterError> {
1319 self.route_open_admission_refusal_frame(
1320 ctx,
1321 frame,
1322 target_module_id,
1323 "target_binds_full",
1324 (in_flight, MAX_PENDING_ROUTE_BINDS_PER_TARGET),
1325 format!(
1326 "module_id '{target_module_id}' already has {in_flight} route.bind relays in flight; retry after one settles"
1327 ),
1328 )
1329 }
1330
1331 fn route_open_admission_refusal_frame(
1339 &self,
1340 ctx: &RouteCtx,
1341 frame: &Frame,
1342 target_module_id: &str,
1343 reason: &'static str,
1344 (in_flight, limit): (usize, usize),
1345 message: impl Into<String>,
1346 ) -> Result<Frame, RouterError> {
1347 let code = error_codes::TARGET_UNAVAILABLE;
1348 self.counters.increment_route_open_refused(code);
1349 info!(
1350 target: "control",
1351 code,
1352 reason,
1353 module_id = ?target_module_id,
1354 connection_id = ctx.connection_id.get(),
1355 in_flight,
1356 limit,
1357 "route.open refused"
1358 );
1359 control_error_frame(frame, code, message.into())
1360 }
1361
1362 #[cfg(test)]
1368 pub fn handle_control(
1369 &self,
1370 connection_id: ConnectionId,
1371 frame: Frame,
1372 ) -> Result<Vec<Frame>, RouterError> {
1373 match frame.header.ty {
1374 FrameType::Ping => Ok(vec![pong(&frame)?]),
1375 FrameType::Hello => self.handle_hello(connection_id, None, frame),
1376 FrameType::Goodbye => self.handle_goodbye(connection_id),
1377 ty => Ok(vec![control_error_frame(
1378 &frame,
1379 "unsupported_control_frame",
1380 format!("unsupported channel-0 frame {ty:?}"),
1381 )?]),
1382 }
1383 }
1384
1385 pub async fn handle_control_frame(
1386 &self,
1387 ctx: &RouteCtx,
1388 frame: Frame,
1389 ) -> Result<Vec<Frame>, RouterError> {
1390 self.handle_control_frame_timed(ctx, frame, None).await
1391 }
1392
1393 pub(crate) async fn handle_control_frame_timed(
1394 &self,
1395 ctx: &RouteCtx,
1396 frame: Frame,
1397 dispatch_started_at: Option<StdInstant>,
1398 ) -> Result<Vec<Frame>, RouterError> {
1399 match frame.header.ty {
1400 FrameType::Ping => Ok(vec![pong(&frame)?]),
1401 FrameType::Hello => {
1402 self.handle_hello(ctx.connection_id, Some(ctx.egress.clone()), frame)
1403 }
1404 FrameType::Goodbye => self.handle_goodbye(ctx.connection_id),
1405 FrameType::Cancel => {
1406 if self
1407 .supervisor
1408 .cancel_spawn_subscription(ctx.connection_id, frame.header.corr)
1409 {
1410 Ok(Vec::new())
1411 } else {
1412 Ok(vec![control_error_frame(
1413 &frame,
1414 "unknown_subscription",
1415 "no supervisor spawn subscription has this correlation id",
1416 )?])
1417 }
1418 }
1419 FrameType::Request => {
1420 if self
1421 .forwarding
1422 .module_endpoint_for_connection(ctx.connection_id)
1423 .map_err(RouterError::Forwarding)?
1424 .is_some()
1425 {
1426 if !is_known_module_request_op(&frame.body) {
1427 return Ok(vec![control_error_frame(
1428 &frame,
1429 "unsupported_control_frame",
1430 "module-originated channel-0 REQUEST is not supported",
1431 )?]);
1432 }
1433 let request = match parse_module_control_request_from_module(&frame.body) {
1434 Ok(request) => request,
1435 Err((err, ControlRequestBodyError::UnknownOp)) => {
1436 return Ok(vec![control_error_frame(
1437 &frame,
1438 "unsupported_control_frame",
1439 format!("unsupported module-originated channel-0 REQUEST: {err}"),
1440 )?])
1441 }
1442 Err((err, ControlRequestBodyError::InvalidBody)) => {
1443 return Ok(vec![control_error_frame(
1444 &frame,
1445 "invalid_control_body",
1446 format!("malformed module control body: {err}"),
1447 )?])
1448 }
1449 };
1450 let op = module_control_request_op(&request);
1451 let corr = frame.header.corr;
1452 log_control_dispatch_arrival(op, ctx.connection_id, corr);
1453 let result =
1454 self.handle_module_control_request(ctx.connection_id, frame, request);
1455 log_slow_control_dispatch(dispatch_started_at, op, ctx.connection_id, corr);
1456 return result;
1457 }
1458
1459 if is_known_module_request_op(&frame.body) {
1460 return Ok(vec![control_error_frame(
1461 &frame,
1462 "not_registered",
1463 "catalog.update requires an active module registration owned by this connection",
1464 )?]);
1465 }
1466
1467 let request = match parse_client_control_request(&frame.body) {
1468 Ok(request) => request,
1469 Err((err, ControlRequestBodyError::UnknownOp)) => {
1470 return Ok(vec![control_error_frame(
1471 &frame,
1472 "unknown_control_op",
1473 format!("unknown client control op: {err}"),
1474 )?])
1475 }
1476 Err((err, ControlRequestBodyError::InvalidBody)) => {
1477 return Ok(vec![control_error_frame(
1478 &frame,
1479 "invalid_control_body",
1480 format!("malformed client control body: {err}"),
1481 )?])
1482 }
1483 };
1484 let op = client_control_request_op(&request);
1485 let corr = frame.header.corr;
1486 log_control_dispatch_arrival(op, ctx.connection_id, corr);
1487 #[cfg(test)]
1488 if let Some(delay) = self.control_dispatch_delay {
1489 tokio::time::sleep(delay).await;
1490 }
1491 let result = self
1492 .handle_client_control_request(ctx, frame, request)
1493 .await;
1494 log_slow_control_dispatch(dispatch_started_at, op, ctx.connection_id, corr);
1495 result
1496 }
1497 FrameType::Push => {
1498 let Some(endpoint) = self
1499 .forwarding
1500 .module_endpoint_for_connection(ctx.connection_id)
1501 .map_err(RouterError::Forwarding)?
1502 else {
1503 return Ok(vec![control_error_frame(
1504 &frame,
1505 "unsupported_control_frame",
1506 "client-originated channel-0 PUSH is not supported",
1507 )?]);
1508 };
1509 self.handle_status_update(endpoint, frame)
1510 }
1511 FrameType::Response | FrameType::Error
1512 if self
1513 .forwarding
1514 .module_endpoint_for_connection(ctx.connection_id)
1515 .map_err(RouterError::Forwarding)?
1516 .is_some() =>
1517 {
1518 self.handle_module_relay_response(ctx.connection_id, frame)
1519 }
1520 ty => Ok(vec![control_error_frame(
1521 &frame,
1522 "unsupported_control_frame",
1523 format!("unsupported channel-0 frame {ty:?}"),
1524 )?]),
1525 }
1526 }
1527
1528 pub fn cleanup_connection(
1529 &self,
1530 connection_id: ConnectionId,
1531 ) -> Result<Vec<crate::registry::ModuleRegistration>, RegistryError> {
1532 let crash_closed = self
1533 .registry
1534 .get_module_by_connection(connection_id)?
1535 .and_then(|registration| {
1536 self.forwarding
1537 .module_endpoint_for_connection(connection_id)
1538 .ok()
1539 .flatten()
1540 .and_then(|endpoint| self.forwarding.endpoint_routes(endpoint).ok())
1541 .map(|routes| (registration.manifest.module_id, routes))
1542 });
1543 let crash_closed = crash_closed.map(|(module_id, routes)| {
1544 let terminal = match self.supervisor.get(&module_id) {
1545 None => false,
1546 Some(module) => match module.will_recover_after_connection_loss() {
1547 Ok(will_recover) => !will_recover,
1548 Err(err) => {
1549 warn!(
1550 %module_id,
1551 error = %err,
1552 "failed to read crash recovery verdict; reporting non-terminal conservatively"
1553 );
1554 false
1555 }
1556 },
1557 };
1558 let reason = match self.forwarding.is_daemon_draining() {
1562 Ok(true) => RouteCloseReason::Restart,
1563 Ok(false) => RouteCloseReason::Crash,
1564 Err(err) => {
1565 warn!(error = %err, "failed to read daemon drain state; reporting crash conservatively");
1566 RouteCloseReason::Crash
1567 }
1568 };
1569 (module_id, routes, reason, terminal)
1570 });
1571 let registrations = self.deregister_connection(connection_id);
1572 let cleanup = if crash_closed.is_some() {
1573 self.forwarding.cleanup_connection_counted(connection_id)
1574 } else {
1575 self.forwarding
1581 .cleanup_connection(connection_id)
1582 .map(|released| crate::forwarding::ConnectionCleanup {
1583 released,
1584 abandoned_relays: 0,
1585 })
1586 };
1587 if let Some((module_id, routes, reason, terminal)) = crash_closed {
1592 let abandoned = cleanup
1593 .as_ref()
1594 .map_or(0, |cleanup| cleanup.abandoned_relays);
1595 send_route_control_pushes(
1596 &self.forwarding,
1597 routes,
1598 ClientControlPush::RouteClosed {
1599 module_id,
1600 channels: Vec::new(),
1601 reason,
1602 drained: false,
1603 abandoned,
1604 excluded_subscriptions: 0,
1605 terminal: Some(terminal),
1606 },
1607 );
1608 }
1609 if let Ok(cleanup) = cleanup {
1610 self.emit_route_goodbyes(cleanup.released);
1611 }
1612 if matches!(®istrations, Ok(r) if !r.is_empty()) {
1616 crate::supervise::notify_registration_release();
1617 self.capability_evaluator.wake_deadline_loop();
1618 self.refresh_capability_requirements();
1619 }
1620 self.supervisor.remove_spawn_subscribers(connection_id);
1621 self.hello_launch_nonces
1624 .lock()
1625 .unwrap_or_else(|poisoned| poisoned.into_inner())
1626 .forget(connection_id);
1627 self.scopes
1628 .write()
1629 .unwrap_or_else(|poisoned| poisoned.into_inner())
1630 .release_connection(connection_id);
1631 registrations
1632 }
1633
1634 pub(crate) fn handle_route_goodbye(
1635 &self,
1636 connection_id: ConnectionId,
1637 route_channel: u16,
1638 route_epoch: u32,
1639 ) -> Result<bool, RouterError> {
1640 debug!(
1641 connection_id = connection_id.get(),
1642 route_channel, route_epoch, "handling route GOODBYE"
1643 );
1644 let RouteRelease::Removed(released_route) = self
1645 .forwarding
1646 .release_client_route(connection_id, route_channel, route_epoch)
1647 .map_err(RouterError::Forwarding)?
1648 else {
1649 return Ok(false);
1650 };
1651 self.emit_route_goodbyes(vec![released_route]);
1652 Ok(true)
1653 }
1654
1655 fn emit_route_goodbyes(&self, released_routes: Vec<GoodbyeTarget>) {
1656 for released in released_routes {
1657 let frame = match Frame::build_with_version(
1658 released.negotiated_ver,
1659 FrameType::Goodbye,
1660 control_flags(),
1661 released.channel,
1662 released.epoch,
1663 0,
1664 Vec::new(),
1665 ) {
1666 Ok(frame) => frame,
1667 Err(err) => {
1668 warn!(
1669 route_channel = released.channel,
1670 error = %err,
1671 "failed to build route GOODBYE frame"
1672 );
1673 continue;
1674 }
1675 };
1676 if !released.close_on_delivery_failure() {
1677 crate::forwarding::send_module_route_goodbye(
1678 &self.counters,
1679 &released.sink,
1680 frame,
1681 released.module_id.as_deref(),
1682 "client route released",
1683 );
1684 continue;
1685 }
1686 if let Err(err) = released.sink.try_send(frame) {
1687 warn!(
1688 target_connection_id = released.connection_id.get(),
1689 route_channel = released.channel,
1690 error = %err,
1691 "route GOODBYE was not delivered to client; closing target connection"
1692 );
1693 if self
1694 .forwarding
1695 .escalate_client_delivery_failure(
1696 released.connection_id,
1697 released.channel,
1698 released.epoch,
1699 CloseReason::new(
1700 "route_goodbye_delivery_failed",
1701 format!(
1702 "failed to enqueue route GOODBYE for channel {}: {err}",
1703 released.channel
1704 ),
1705 ),
1706 crate::forwarding::UndeliveredFrame {
1707 module_id: released.module_id.as_deref(),
1708 sink: &released.sink,
1709 },
1710 )
1711 .unwrap_or(false)
1712 {
1713 self.counters.increment_goodbye_relay_client_failed();
1714 }
1715 }
1716 }
1717 }
1718
1719 fn send_abandoned_route_bind_goodbye(
1731 &self,
1732 module_sink: &crate::FrameSink,
1733 negotiated_ver: u8,
1734 module_channel: u16,
1735 module_epoch: u32,
1736 ) {
1737 let frame = match Frame::build_with_version(
1738 negotiated_ver,
1739 FrameType::Goodbye,
1740 control_flags(),
1741 module_channel,
1742 module_epoch,
1743 0,
1744 Vec::new(),
1745 ) {
1746 Ok(frame) => frame,
1747 Err(err) => {
1748 warn!(
1749 route_channel = module_channel,
1750 error = %err,
1751 "failed to build GOODBYE for abandoned route.bind"
1752 );
1753 return;
1754 }
1755 };
1756 crate::forwarding::send_module_route_goodbye(
1757 &self.counters,
1758 module_sink,
1759 frame,
1760 None,
1761 "abandoned route.bind",
1762 );
1763 }
1764
1765 fn handle_hello(
1766 &self,
1767 connection_id: ConnectionId,
1768 sink: Option<crate::FrameSink>,
1769 frame: Frame,
1770 ) -> Result<Vec<Frame>, RouterError> {
1771 debug!(
1772 connection_id = connection_id.get(),
1773 corr = frame.header.corr,
1774 "handling HELLO"
1775 );
1776 if self
1780 .registry
1781 .get_module_by_connection(connection_id)
1782 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
1783 .is_some()
1784 {
1785 return Ok(vec![control_error_frame(
1786 &frame,
1787 "invalid_hello",
1788 "connection is already registered as a module",
1789 )?]);
1790 }
1791 let hello_value = match serde_json::from_slice::<serde_json::Value>(&frame.body) {
1792 Ok(value) => value,
1793 Err(err) => {
1794 return Ok(vec![control_error_frame(
1795 &frame,
1796 "invalid_hello",
1797 format!("malformed HELLO body: {err}"),
1798 )?])
1799 }
1800 };
1801 if let Err(err) = validate_hello_capability_grammar(&hello_value) {
1802 return Ok(vec![control_error_frame(
1803 &frame,
1804 "invalid_capability_grammar",
1805 err.to_string(),
1806 )?]);
1807 }
1808 if let Err(err) = validate_hello_self_signal_declarations(&hello_value) {
1809 return Ok(vec![control_error_frame(
1810 &frame,
1811 "invalid_manifest",
1812 err.to_string(),
1813 )?]);
1814 }
1815 if let Err(err) = validate_hello_event_declarations(&hello_value) {
1816 return Ok(vec![control_error_frame(
1817 &frame,
1818 "invalid_event_declaration",
1819 err.to_string(),
1820 )?]);
1821 }
1822 if let Some(provenance) = hello_value
1823 .get("manifest")
1824 .and_then(|manifest| manifest.get("provenance"))
1825 {
1826 if let Err(err) = serde_json::from_value::<ManifestProvenance>(provenance.clone()) {
1827 return Ok(vec![control_error_frame(
1828 &frame,
1829 "invalid_manifest",
1830 format!("malformed manifest provenance: {err}"),
1831 )?]);
1832 }
1833 }
1834 let hello = match serde_json::from_value::<ModuleHelloBody>(hello_value) {
1835 Ok(hello) => hello,
1836 Err(err) => {
1837 return Ok(vec![control_error_frame(
1838 &frame,
1839 "invalid_hello",
1840 format!("malformed HELLO body: {err}"),
1841 )?])
1842 }
1843 };
1844
1845 if hello.protocol_ver != hello.manifest.protocol_ver {
1846 return Ok(vec![control_error_frame(
1847 &frame,
1848 "invalid_manifest",
1849 format!(
1850 "HELLO protocol_ver {} does not match manifest protocol_ver {}",
1851 hello.protocol_ver, hello.manifest.protocol_ver
1852 ),
1853 )?]);
1854 }
1855
1856 if hello.manifest.module_id.trim().is_empty() {
1857 return Ok(vec![control_error_frame(
1858 &frame,
1859 "invalid_manifest",
1860 "manifest module_id must not be empty",
1861 )?]);
1862 }
1863
1864 let negotiated_ver = match negotiate_version(hello.protocol_ver) {
1865 Ok(negotiated_ver) => negotiated_ver,
1866 Err(message) => {
1867 return Ok(vec![control_error_frame(
1868 &frame,
1869 "version_unsupported",
1870 message,
1871 )?])
1872 }
1873 };
1874
1875 let swap_admission = self
1883 .supervisor
1884 .swap_hello_admission(&hello.manifest.module_id, hello.launch_nonce.as_deref());
1885 if swap_admission == SwapHelloAdmission::Refused {
1886 warn!(
1887 module_id = %hello.manifest.module_id,
1888 connection_id = connection_id.get(),
1889 "HELLO refused: a swap is open for this module_id and the launch nonce is not one the supervisor minted for it"
1890 );
1891 return Ok(vec![control_error_frame(
1892 &frame,
1893 "swap_token_invalid",
1894 format!(
1895 "module_id '{}' is being swapped; HELLO without the swap candidate's launch nonce is rejected",
1896 hello.manifest.module_id
1897 ),
1898 )?]);
1899 }
1900 let swap_candidate = swap_admission == SwapHelloAdmission::Candidate;
1901
1902 if let Some(rejection) = (!swap_candidate)
1910 .then(|| {
1911 self.supervisor.reserved_hello_rejection(
1912 &hello.manifest.module_id,
1913 hello.launch_nonce.as_deref(),
1914 )
1915 })
1916 .flatten()
1917 {
1918 let message = match rejection {
1919 ReservedHelloRejection::Exact { module_id } => format!(
1920 "module_id '{module_id}' is reserved; HELLO without a valid launch nonce is rejected"
1921 ),
1922 ReservedHelloRejection::Prefix {
1923 prefix,
1924 owner_module_id,
1925 } => format!(
1926 "module_id '{}' matches reserved prefix '{prefix}' owned by '{owner_module_id}'; HELLO without the owner launch nonce is rejected",
1927 hello.manifest.module_id
1928 ),
1929 };
1930 return Ok(vec![control_error_frame(
1931 &frame,
1932 "reserved_module",
1933 message,
1934 )?]);
1935 }
1936
1937 let reserved_capability_refusals = self.capability_evaluator.reserved_hello_refusals(
1938 &hello.manifest.module_id,
1939 hello.manifest.capabilities.as_ref(),
1940 );
1941 if let Some(refusal) = reserved_capability_refusals.first() {
1942 let capability = refusal.capability.clone();
1943 let bound_module = refusal.claimants[0].clone();
1944 log_duplicate_claim_events(reserved_capability_refusals);
1945 return Ok(vec![control_error_frame(
1946 &frame,
1947 "reserved_capability",
1948 format!(
1949 "capability '{}' is reserved for module_id '{}'; claimant '{}' was refused",
1950 capability, bound_module, hello.manifest.module_id
1951 ),
1952 )?]);
1953 }
1954
1955 if self
1958 .forwarding
1959 .connection_has_client_routes(connection_id)
1960 .map_err(RouterError::Forwarding)?
1961 {
1962 return Ok(vec![control_error_frame(
1963 &frame,
1964 "invalid_hello",
1965 "connection has open client routes and cannot also register as a module",
1966 )?]);
1967 }
1968
1969 self.hello_launch_nonces
1974 .lock()
1975 .unwrap_or_else(|poisoned| poisoned.into_inner())
1976 .record(connection_id, hello.launch_nonce.as_deref());
1977 let control_ops = effective_module_control_ops(hello.control_ops);
1978 let hello_ack = self.build_hello_ack(&frame, negotiated_ver, &hello.manifest.module_id)?;
1981 if swap_candidate {
1982 return self.register_swap_candidate(
1983 connection_id,
1984 sink,
1985 &frame,
1986 hello.manifest,
1987 negotiated_ver,
1988 control_ops,
1989 hello_ack,
1990 );
1991 }
1992 let registration = match self.registry.register_with_control_ops(
1993 hello.manifest,
1994 negotiated_ver,
1995 connection_id,
1996 control_ops,
1997 ) {
1998 Ok(registration) => registration,
1999 Err(RegistryError::DuplicateModuleId { module_id }) => {
2000 return Ok(vec![control_error_frame(
2001 &frame,
2002 "duplicate_module_id",
2003 format!(
2004 "module_id '{module_id}' is already registered; duplicate HELLO rejected"
2005 ),
2006 )?])
2007 }
2008 Err(err @ RegistryError::PathHazardModuleId { .. }) => {
2009 return Ok(vec![control_error_frame(
2010 &frame,
2011 "invalid_module_id",
2012 err.to_string(),
2013 )?])
2014 }
2015 Err(err) => {
2016 return Ok(vec![control_error_frame(
2017 &frame,
2018 "registry_error",
2019 err.to_string(),
2020 )?])
2021 }
2022 };
2023
2024 let reply = if let Some(sink) = sink {
2025 let concurrency = manifest_concurrency(®istration.manifest);
2041 if let Err(err) = self.forwarding.register_module_connection_acked(
2042 connection_id,
2043 registration.manifest.module_id.clone(),
2044 negotiated_ver,
2045 concurrency,
2046 sink,
2047 hello_ack,
2048 ) {
2049 if matches!(self.deregister_connection(connection_id), Ok(r) if !r.is_empty()) {
2053 crate::supervise::notify_registration_release();
2054 }
2055 return Ok(vec![control_error_frame(
2056 &frame,
2057 if matches!(err, ForwardingError::ConnectionRoleConflict { .. }) {
2058 "invalid_hello"
2059 } else {
2060 forwarding_error_code(&err)
2061 },
2062 err.to_string(),
2063 )?]);
2064 }
2065 Vec::new()
2066 } else {
2067 vec![hello_ack]
2070 };
2071
2072 if manifest_concurrency_was_defaulted(&frame.body, ®istration.manifest) {
2081 info!(
2082 module_id = %registration.manifest.module_id,
2083 "management surface registered with DEFAULTED concurrency=module_managed (manifest predates the field; declare the real lane)"
2084 );
2085 }
2086
2087 self.apply_registration_capabilities(®istration);
2088
2089 info!(
2090 module_id = %registration.manifest.module_id,
2091 module_version = %registration.manifest.module_version,
2092 negotiated_ver,
2093 routable_provider = manifest_provides_routable_role(®istration.manifest),
2094 connection_id = connection_id.get(),
2095 "module registered"
2096 );
2097
2098 Ok(reply)
2099 }
2100
2101 #[allow(clippy::too_many_arguments)]
2111 fn register_swap_candidate(
2112 &self,
2113 connection_id: ConnectionId,
2114 sink: Option<crate::FrameSink>,
2115 frame: &Frame,
2116 manifest: ModuleManifest,
2117 negotiated_ver: u8,
2118 control_ops: Vec<String>,
2119 hello_ack: Frame,
2120 ) -> Result<Vec<Frame>, RouterError> {
2121 let module_id = manifest.module_id.clone();
2122 let registration = match self.registry.register_candidate_with_control_ops(
2123 manifest,
2124 negotiated_ver,
2125 connection_id,
2126 control_ops,
2127 ) {
2128 Ok(registration) => registration,
2129 Err(RegistryError::DuplicateModuleId { module_id }) => {
2130 return Ok(vec![control_error_frame(
2131 frame,
2132 "duplicate_module_id",
2133 format!(
2134 "module_id '{module_id}' already has a swap candidate registered; duplicate HELLO rejected"
2135 ),
2136 )?])
2137 }
2138 Err(err @ RegistryError::PathHazardModuleId { .. }) => {
2139 return Ok(vec![control_error_frame(
2140 frame,
2141 "invalid_module_id",
2142 err.to_string(),
2143 )?])
2144 }
2145 Err(err) => {
2146 return Ok(vec![control_error_frame(
2147 frame,
2148 "registry_error",
2149 err.to_string(),
2150 )?])
2151 }
2152 };
2153 let reply = if let Some(sink) = sink {
2154 let concurrency = manifest_concurrency(®istration.manifest);
2158 if let Err(err) = self.forwarding.register_candidate_module_connection_acked(
2159 connection_id,
2160 module_id.clone(),
2161 negotiated_ver,
2162 concurrency,
2163 sink,
2164 hello_ack,
2165 ) {
2166 if matches!(self.deregister_connection(connection_id), Ok(r) if !r.is_empty()) {
2167 crate::supervise::notify_registration_release();
2168 }
2169 return Ok(vec![control_error_frame(
2170 frame,
2171 forwarding_error_code(&err),
2172 err.to_string(),
2173 )?]);
2174 }
2175 Vec::new()
2176 } else {
2177 vec![hello_ack]
2178 };
2179 self.supervisor.mark_swap_candidate_admitted(&module_id);
2180 info!(
2181 module_id = %module_id,
2182 module_version = %registration.manifest.module_version,
2183 negotiated_ver,
2184 ready = registration.ready,
2185 connection_id = connection_id.get(),
2186 "swap candidate registered; not routable until cutover"
2187 );
2188 Ok(reply)
2189 }
2190
2191 fn build_hello_ack(
2192 &self,
2193 frame: &Frame,
2194 negotiated_ver: u8,
2195 module_id: &str,
2196 ) -> Result<Frame, RouterError> {
2197 let ack = ModuleHelloAckBody {
2198 negotiated_ver,
2199 subc_ops: module_subc_ops(),
2200 subc_capabilities: self.subc_capabilities.as_ref().to_vec(),
2201 storage: self
2202 .storage_config
2203 .as_ref()
2204 .map(|cfg| cfg.descriptor_for(module_id)),
2205 machine_id: self.machine_id.as_ref().map(|id| id.as_str().to_owned()),
2206 };
2207 let body = serde_json::to_vec(&ack).map_err(|err| {
2208 RouterError::backend(
2209 0,
2210 frame.header.corr,
2211 format!("failed to encode HELLO_ACK: {err}"),
2212 )
2213 })?;
2214
2215 Frame::build_with_version(
2216 negotiated_ver,
2217 FrameType::HelloAck,
2218 control_flags(),
2219 0,
2220 0,
2221 frame.header.corr,
2222 body,
2223 )
2224 .map_err(RouterError::FrameBuild)
2225 }
2226
2227 async fn handle_client_control_request(
2228 &self,
2229 ctx: &RouteCtx,
2230 frame: Frame,
2231 request: ClientControlRequest,
2232 ) -> Result<Vec<Frame>, RouterError> {
2233 match request {
2234 ClientControlRequest::ServerDescribe {} => self.handle_server_describe(frame),
2235 ClientControlRequest::CatalogList { module_id } => {
2236 self.handle_catalog_list(frame, module_id)
2237 }
2238 ClientControlRequest::RouteOpen {
2239 target,
2240 identity,
2241 consumer_identity,
2242 consumer_capabilities,
2243 role_versions,
2244 admission_facts,
2245 scope,
2246 } => {
2247 self.handle_route_open(
2248 ctx,
2249 frame,
2250 RouteOpenRequest {
2251 target,
2252 identity,
2253 consumer_identity,
2254 consumer_capabilities,
2255 role_versions,
2256 admission_facts,
2257 scope,
2258 },
2259 )
2260 .await
2261 }
2262 ClientControlRequest::RoutePoll {
2263 route_channel,
2264 route_epoch,
2265 kind,
2266 } => self.handle_route_poll(ctx, frame, route_channel, route_epoch, kind),
2267 ClientControlRequest::SupervisorList {} => self.handle_supervisor_list(frame).await,
2268 ClientControlRequest::SupervisorSpawnSnapshot {} => {
2269 self.handle_supervisor_spawn_snapshot(frame)
2270 }
2271 ClientControlRequest::SupervisorSpawnSubscribe { since } => {
2272 self.handle_supervisor_spawn_subscribe(ctx, frame, since)
2273 }
2274 ClientControlRequest::SupervisorRestart {
2275 module_id,
2276 drain_timeout_ms,
2277 } => {
2278 self.handle_supervisor_restart(frame, module_id, drain_timeout_ms)
2279 .await
2280 }
2281 ClientControlRequest::SupervisorSwap {
2282 module_id,
2283 ready_timeout_ms,
2284 } => {
2285 self.handle_supervisor_swap(frame, module_id, ready_timeout_ms)
2286 .await
2287 }
2288 ClientControlRequest::SupervisorReload { module_id } => {
2289 self.handle_supervisor_reload(frame, module_id).await
2290 }
2291 ClientControlRequest::SupervisorRescan { preview } => {
2292 self.handle_supervisor_rescan(frame, preview).await
2293 }
2294 ClientControlRequest::SupervisorReleaseReserved { module_id } => {
2295 self.handle_supervisor_release_reserved(frame, module_id)
2296 .await
2297 }
2298 ClientControlRequest::SupervisorSetEnabled { module_id, enabled } => {
2299 self.handle_supervisor_set_enabled(frame, module_id, enabled)
2300 .await
2301 }
2302 ClientControlRequest::SupervisorHealthProbe { module_id } => {
2303 self.handle_supervisor_health_probe(frame, module_id).await
2304 }
2305 ClientControlRequest::SupervisorHealth {} => self.handle_supervisor_health(frame),
2306 ClientControlRequest::SupervisorRoutes { module_id } => {
2307 self.handle_supervisor_routes(frame, module_id)
2308 }
2309 ClientControlRequest::SupervisorProvenance { module_id } => {
2310 self.handle_supervisor_provenance(frame, module_id).await
2311 }
2312 ClientControlRequest::SupervisorStderrTail {
2313 module_id,
2314 max_lines,
2315 max_bytes,
2316 } => self.handle_supervisor_stderr_tail(frame, module_id, max_lines, max_bytes),
2317 ClientControlRequest::SupervisorTerminals { module_id } => {
2318 self.handle_supervisor_terminals(frame, module_id).await
2319 }
2320 }
2321 }
2322
2323 fn handle_module_control_request(
2324 &self,
2325 connection_id: ConnectionId,
2326 frame: Frame,
2327 request: ModuleControlRequestFromModule,
2328 ) -> Result<Vec<Frame>, RouterError> {
2329 match request {
2330 ModuleControlRequestFromModule::CatalogUpdate {
2331 provides,
2332 capabilities,
2333 ready,
2334 } => self.handle_catalog_update(connection_id, frame, provides, capabilities, ready),
2335 ModuleControlRequestFromModule::LiveRoots {} => {
2336 let registered = self
2337 .registry
2338 .get_module_by_connection(connection_id)
2339 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
2340 let Some(registration) = registered else {
2341 return Ok(vec![control_error_frame(&frame, "not_registered", "supervisor.live_roots requires an active module registration owned by this connection")?]);
2342 };
2343 let response = self
2344 .forwarding
2345 .live_roots(®istration.manifest.module_id)
2346 .map_err(RouterError::Forwarding)?;
2347 Ok(vec![control_response_body_frame(
2348 &frame,
2349 &response,
2350 "ModuleControlResponseToModule::LiveRoots",
2351 )?])
2352 }
2353 ModuleControlRequestFromModule::ScopeSync { generation, scopes } => {
2354 self.handle_scope_sync(connection_id, frame, generation, scopes)
2355 }
2356 ModuleControlRequestFromModule::ScopeDescribe { owner, scope_ref } => {
2357 self.handle_scope_describe(connection_id, frame, owner, scope_ref)
2358 }
2359 }
2360 }
2361
2362 fn handle_scope_sync(
2366 &self,
2367 connection_id: ConnectionId,
2368 frame: Frame,
2369 generation: u64,
2370 scopes: Vec<ScopeRecord>,
2371 ) -> Result<Vec<Frame>, RouterError> {
2372 let Some(registration) = self
2373 .registry
2374 .get_module_by_connection(connection_id)
2375 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
2376 else {
2377 return Ok(vec![control_error_frame(
2378 &frame,
2379 "not_registered",
2380 "scope.sync requires an active module registration owned by this connection",
2381 )?]);
2382 };
2383 let owner = registration.manifest.module_id;
2384 let current_nonce = self.supervisor.spawn_launch_nonce_for(&owner);
2385 let is_current_launch = |connection: ConnectionId| {
2386 self.hello_launch_nonces
2387 .lock()
2388 .unwrap_or_else(|poisoned| poisoned.into_inner())
2389 .presented(connection, current_nonce.as_deref())
2390 };
2391 let mut table = self
2396 .scopes
2397 .write()
2398 .unwrap_or_else(|poisoned| poisoned.into_inner());
2399 let outcome = table.sync(&owner, connection_id, is_current_launch, generation, scopes);
2400 let drained = match &outcome {
2401 Ok(applied) => self
2402 .forwarding
2403 .publish_scope_changes(&applied.tag_changes)
2404 .map_err(RouterError::Forwarding)?,
2405 Err(_) => Vec::new(),
2406 };
2407 drop(table);
2408 match outcome {
2409 Ok(applied) => {
2410 let counts = ScopeOutcomeCounts::of(&applied.results);
2411 info!(
2412 owner = %owner,
2413 generation,
2414 records = applied.results.len(),
2415 created = counts.created,
2416 replaced = counts.replaced,
2417 updated = counts.updated,
2418 unchanged = counts.unchanged,
2419 refused = counts.refused,
2420 ended = applied.ended.len(),
2421 tag_changes = applied.tag_changes.len(),
2422 routes_closed = drained.len(),
2423 "scope sync accepted"
2424 );
2425 for refused in applied
2431 .results
2432 .iter()
2433 .filter(|result| result.outcome == ScopeRecordOutcome::Refused)
2434 .take(MAX_LOGGED_REFUSED_SCOPE_RECORDS)
2435 {
2436 warn!(
2437 owner = %owner,
2438 generation,
2439 scope_ref = %refused.scope_ref,
2440 scope_epoch = refused.scope_epoch,
2441 code = refused.code.as_deref().unwrap_or(""),
2442 "scope record refused"
2443 );
2444 }
2445 self.close_scope_drained_routes(drained);
2446 let response = ModuleControlResponseToModule::ScopeSync {
2447 generation,
2448 results: applied.results,
2449 ended: applied.ended,
2450 };
2451 Ok(vec![control_response_body_frame(
2452 &frame,
2453 &response,
2454 "ModuleControlResponseToModule::ScopeSync",
2455 )?])
2456 }
2457 Err(refusal) => {
2458 info!(
2459 owner = %owner,
2460 generation,
2461 code = refusal.code,
2462 "scope sync refused"
2463 );
2464 Ok(vec![control_error_frame(
2465 &frame,
2466 refusal.code,
2467 refusal.message,
2468 )?])
2469 }
2470 }
2471 }
2472
2473 fn close_scope_drained_routes(&self, drained: Vec<crate::forwarding::ScopeDrainedRoute>) {
2479 if drained.is_empty() {
2480 return;
2481 }
2482 let mut pushes: BTreeMap<(String, String), (RouteCloseReason, Vec<EndpointRoute>)> =
2483 BTreeMap::new();
2484 let mut goodbyes = Vec::with_capacity(drained.len() * 2);
2485 for route in drained {
2486 warn!(
2487 module_id = %route.module_id,
2488 reason = ?route.reason,
2489 client_connection_id = route.client.connection_id.get(),
2490 route_channel = route.client.channel,
2491 "closing route because its scope changed"
2492 );
2493 pushes
2494 .entry((route.module_id.clone(), format!("{:?}", route.reason)))
2495 .or_insert_with(|| (route.reason, Vec::new()))
2496 .1
2497 .push(EndpointRoute {
2498 goodbye_target: route.client.clone(),
2499 principal: Principal::Unverified,
2500 bound_at: Instant::now(),
2501 draining: false,
2502 drain_reason: None,
2503 });
2504 goodbyes.push(route.module);
2505 goodbyes.push(route.client);
2506 }
2507 for ((module_id, _), (reason, routes)) in pushes {
2508 send_route_control_pushes(
2509 &self.forwarding,
2510 routes,
2511 ClientControlPush::RouteClosed {
2512 module_id,
2513 channels: Vec::new(),
2514 reason,
2515 drained: false,
2516 abandoned: 0,
2517 excluded_subscriptions: 0,
2518 terminal: Some(false),
2519 },
2520 );
2521 }
2522 self.emit_route_goodbyes(goodbyes);
2523 }
2524
2525 fn handle_scope_describe(
2528 &self,
2529 connection_id: ConnectionId,
2530 frame: Frame,
2531 owner: Principal,
2532 scope_ref: String,
2533 ) -> Result<Vec<Frame>, RouterError> {
2534 let registered = self
2535 .registry
2536 .get_module_by_connection(connection_id)
2537 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
2538 if registered.is_none() {
2539 return Ok(vec![control_error_frame(
2540 &frame,
2541 "not_registered",
2542 "scope.describe requires an active module registration owned by this connection",
2543 )?]);
2544 }
2545 let description = self
2546 .scopes
2547 .read()
2548 .unwrap_or_else(|poisoned| poisoned.into_inner())
2549 .describe(&owner, &scope_ref);
2550 let owner_configured = match &owner {
2551 Principal::Reserved { module_id } => self.supervisor.is_configured(module_id),
2556 _ => false,
2557 };
2558 let response = ModuleControlResponseToModule::ScopeDescribe {
2559 status: description.status,
2560 scope_epoch: description.scope_epoch,
2561 daemon_incarnation: self.supervisor.spawn_snapshot().cursor.daemon_incarnation,
2562 owner_synced: description.owner_synced,
2563 owner_configured,
2564 scope: description.stamp,
2565 };
2566 Ok(vec![control_response_body_frame(
2567 &frame,
2568 &response,
2569 "ModuleControlResponseToModule::ScopeDescribe",
2570 )?])
2571 }
2572
2573 fn handle_catalog_update(
2574 &self,
2575 connection_id: ConnectionId,
2576 frame: Frame,
2577 provides: Vec<ProviderRole>,
2578 capabilities: Option<CapabilityDeclarations>,
2579 ready: Option<bool>,
2580 ) -> Result<Vec<Frame>, RouterError> {
2581 self.refresh_capability_requirements();
2582 let Some(registration) = self
2583 .registry
2584 .get_module_by_connection(connection_id)
2585 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
2586 else {
2587 return Ok(vec![control_error_frame(
2588 &frame,
2589 "not_registered",
2590 "catalog.update requires an active module registration owned by this connection",
2591 )?]);
2592 };
2593
2594 if let Some(message) =
2595 catalog_update_frozen_field_message(®istration.manifest, &provides)
2596 {
2597 return Ok(vec![control_error_frame(
2598 &frame,
2599 "catalog_update_frozen_field",
2600 message,
2601 )?]);
2602 }
2603
2604 let mut candidate = registration.manifest.clone();
2605 candidate.provides = provides.clone();
2606 candidate.capabilities = capabilities
2607 .clone()
2608 .or_else(|| registration.manifest.capabilities.clone());
2609 if let Err(err) = candidate.validate_capability_grammar() {
2610 return Ok(vec![control_error_frame(
2611 &frame,
2612 "invalid_capability_grammar",
2613 err.to_string(),
2614 )?]);
2615 }
2616
2617 let mut conflicts = self
2620 .capability_evaluator
2621 .reserved_hello_refusals(&candidate.module_id, candidate.capabilities.as_ref());
2622 if let Some(conflict) = conflicts.first() {
2623 let message = format!(
2624 "capability '{}' is reserved for module_id '{}'; claimant '{}' was refused",
2625 conflict.capability, conflict.claimants[0], candidate.module_id
2626 );
2627 for conflict in &mut conflicts {
2628 conflict.source = DuplicateClaimSource::CatalogUpdate;
2629 }
2630 log_duplicate_claim_events(conflicts);
2631 return Ok(vec![control_error_frame(
2632 &frame,
2633 "reserved_capability",
2634 message,
2635 )?]);
2636 }
2637
2638 let updated = self
2639 .registry
2640 .replace_catalog_for_connection(connection_id, provides, capabilities, ready)
2641 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
2642 if updated.is_none() {
2643 return Ok(vec![control_error_frame(
2644 &frame,
2645 "not_registered",
2646 "catalog.update requires an active module registration owned by this connection",
2647 )?]);
2648 }
2649 if let Ok((_, registrations)) = self.runtime_capability_snapshot() {
2650 log_duplicate_claim_events(
2651 self.capability_evaluator
2652 .duplicate_claims(DuplicateClaimSource::CatalogUpdate, ®istrations),
2653 );
2654 }
2655 if capability_census_trigger(
2656 registration.manifest.capabilities.as_ref(),
2657 updated
2658 .as_ref()
2659 .and_then(|entry| entry.manifest.capabilities.as_ref()),
2660 ) {
2661 self.enforce_capability_denies();
2662 }
2663 self.refresh_capability_requirements();
2664
2665 let response = ModuleControlResponseToModule::CatalogUpdate {};
2666 control_response_body_frame(
2667 &frame,
2668 &response,
2669 "ModuleControlResponseToModule::CatalogUpdate",
2670 )
2671 .map(|frame| vec![frame])
2672 }
2673
2674 fn handle_server_describe(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
2675 self.refresh_capability_requirements();
2676 let mut counters = self.counters.snapshot();
2682 if let (Ok((connections_with_routes, max)), Some(obj)) = (
2683 self.forwarding.client_route_concentration(),
2684 counters.as_object_mut(),
2685 ) {
2686 obj.insert(
2687 "client_connections_with_routes".into(),
2688 connections_with_routes.into(),
2689 );
2690 obj.insert("max_routes_on_one_connection".into(), max.into());
2691 }
2692 if let (Some(open_breakers), Some(obj)) = (
2698 self.route_bind_breakers.open_snapshot(),
2699 counters.as_object_mut(),
2700 ) {
2701 obj.insert("route_bind_breakers_open".into(), open_breakers);
2702 }
2703 let response = ClientControlResponse::ServerDescribe {
2704 protocol_ver: PROTOCOL_VERSION,
2705 subc_ops: subc_ops(),
2706 capabilities: self.subc_capabilities.as_ref().to_vec(),
2707 connected_clients: self.connected_clients.count(),
2708 counters: Some(counters),
2709 build_git_sha: Some(env!("SUBC_BUILD_GIT_SHA").to_string()),
2710 build_lock_digest: Some(env!("SUBC_BUILD_LOCK_DIGEST").to_string()),
2711 capability_requirements: self.capability_requirement_statuses(),
2712 machine_id: self.machine_id.as_ref().map(|id| id.as_str().to_owned()),
2713 };
2714 Ok(vec![control_response_body_frame(
2715 &frame,
2716 &response,
2717 "ClientControlResponse::ServerDescribe",
2718 )?])
2719 }
2720
2721 fn handle_catalog_list(
2722 &self,
2723 frame: Frame,
2724 module_id: Option<String>,
2725 ) -> Result<Vec<Frame>, RouterError> {
2726 let (generation, modules) = self.registry.list_modules().map_err(|err| {
2727 RouterError::backend(0, frame.header.corr, format!("registry error: {err}"))
2728 })?;
2729 let entries = modules
2730 .into_iter()
2731 .filter(|registration| {
2732 module_id
2733 .as_deref()
2734 .map(|wanted| registration.manifest.module_id == wanted)
2735 .unwrap_or(true)
2736 })
2737 .map(|registration| {
2738 let not_ready = self.not_ready_reason(®istration);
2739 let roles = registration.manifest.provides;
2740 CatalogEntry {
2741 module_id: registration.manifest.module_id,
2742 ready: not_ready.is_none(),
2743 not_ready,
2744 module_version: Some(registration.manifest.module_version),
2745 roles,
2746 control_ops: registration.control_ops,
2747 capabilities: registration.manifest.capabilities,
2748 self_signals: registration.manifest.self_signals,
2749 }
2750 })
2751 .collect();
2752 let response = ClientControlResponse::CatalogList {
2753 generation,
2754 modules: entries,
2755 subc_ops: subc_ops(),
2756 };
2757 Ok(vec![control_response_body_frame(
2758 &frame,
2759 &response,
2760 "ClientControlResponse::CatalogList",
2761 )?])
2762 }
2763
2764 fn route_open_principal(
2765 &self,
2766 frame: &Frame,
2767 consumer_identity: Option<ConsumerIdentity>,
2768 ) -> Result<Result<Principal, Frame>, RouterError> {
2769 let Some(consumer_identity) = consumer_identity else {
2770 return Ok(Ok(Principal::Direct));
2771 };
2772
2773 if self.supervisor.spawned_consumer_authorized(
2774 &consumer_identity.module_id,
2775 &consumer_identity.launch_nonce,
2776 ) {
2777 return Ok(Ok(Principal::Reserved {
2778 module_id: consumer_identity.module_id,
2779 }));
2780 }
2781
2782 Ok(Err(control_error_frame(
2783 frame,
2784 "bad_consumer_identity",
2785 format!(
2786 "consumer_identity for module_id '{}' did not match a supervised launch nonce",
2787 consumer_identity.module_id
2788 ),
2789 )?))
2790 }
2791
2792 fn route_open_refusal_frame(
2799 &self,
2800 ctx: &RouteCtx,
2801 frame: &Frame,
2802 module_id: &str,
2803 reason: &'static str,
2804 code: &'static str,
2805 message: impl Into<String>,
2806 ) -> Result<Frame, RouterError> {
2807 self.observe_route_open_refusal(ctx, module_id, reason, code);
2808 control_error_frame(frame, code, message.into())
2809 }
2810
2811 fn route_open_breaker_refusal_frame(
2829 &self,
2830 ctx: &RouteCtx,
2831 frame: &Frame,
2832 module_id: &str,
2833 consecutive_timeouts: u32,
2834 retry_in: Duration,
2835 probe_in_flight: bool,
2836 ) -> Result<Frame, RouterError> {
2837 self.counters
2838 .increment_route_open_refused(crate::observability::ROUTE_OPEN_REFUSED_BREAKER_OPEN);
2839 debug!(
2840 target: "control",
2841 code = "module_timeout",
2842 module_id = ?module_id,
2843 connection_id = ctx.connection_id.get(),
2844 consecutive_timeouts,
2845 retry_in_ms = retry_in.as_millis() as u64,
2846 probe_in_flight,
2847 "route.open refused by open bind-relay breaker"
2848 );
2849 let detail = if probe_in_flight {
2856 "one test route is already being tried; retry once it settles".to_string()
2857 } else {
2858 format!("retrying new routes in {}s", retry_in.as_secs().max(1))
2859 };
2860 control_error_frame(
2861 frame,
2862 "module_timeout",
2863 format!(
2864 "module '{module_id}' is slow to accept new routes ({consecutive_timeouts} \
2865 timed out in a row); {detail}; its established routes are unaffected"
2866 ),
2867 )
2868 }
2869
2870 fn observe_route_open_refusal(
2883 &self,
2884 ctx: &RouteCtx,
2885 module_id: &str,
2886 reason: &'static str,
2887 code: &'static str,
2888 ) {
2889 self.counters.increment_route_open_refused(code);
2890 info!(
2891 target: "control",
2892 code,
2893 reason,
2894 module_id = ?module_id,
2895 connection_id = ctx.connection_id.get(),
2896 "route.open refused"
2897 );
2898 if ROUTE_OPEN_NOT_SERVING_REASONS.contains(&reason) {
2899 self.route_outages.record_not_serving(module_id, reason);
2900 }
2901 }
2902
2903 fn observe_route_open_accept(&self, ctx: &RouteCtx, module_id: &str, principal: &str) {
2948 self.route_outages.record_accepted(module_id);
2949 self.counters.increment_route_open_accepted(principal);
2950 info!(
2951 target: "control",
2952 principal,
2953 module_id,
2954 connection_id = ctx.connection_id.get(),
2955 "route.open accepted"
2956 );
2957 }
2958
2959 fn supervised_absent_route_open_refusal_frame(
2960 &self,
2961 ctx: &RouteCtx,
2962 frame: &Frame,
2963 module_id: &str,
2964 code: &'static str,
2965 status: &crate::supervise::ModuleStatus,
2966 ) -> Result<Frame, RouterError> {
2967 self.counters.increment_route_open_refused(code);
2968 info!(
2969 target: "control",
2970 code,
2971 reason = "supervised_not_registered",
2972 module_id = ?module_id,
2973 connection_id = ctx.connection_id.get(),
2974 state = %status.state,
2975 enabled = status.enabled,
2976 live = status.live,
2977 "route.open refused"
2978 );
2979 self.route_outages
2983 .record_not_serving(module_id, "supervised_not_registered");
2984 control_error_frame(
2985 frame,
2986 code,
2987 format!(
2988 "module_id '{module_id}' is supervised but not available (state={}, enabled={}, live={})",
2989 status.state, status.enabled, status.live
2990 ),
2991 )
2992 }
2993
2994 async fn handle_route_open(
2995 &self,
2996 ctx: &RouteCtx,
2997 frame: Frame,
2998 request: RouteOpenRequest,
2999 ) -> Result<Vec<Frame>, RouterError> {
3000 let RouteOpenRequest {
3001 target,
3002 mut identity,
3003 consumer_identity,
3004 consumer_capabilities,
3005 role_versions,
3006 admission_facts,
3007 scope,
3008 } = request;
3009 let target_module_id = target_module_id(&target).to_string();
3010 if self
3011 .registry
3012 .get_module_by_connection(ctx.connection_id)
3013 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
3014 .is_some()
3015 {
3016 return Ok(vec![control_error_frame(
3017 &frame,
3018 "invalid_request",
3019 "module connections cannot open client routes",
3020 )?]);
3021 }
3022 debug!(
3023 connection_id = ctx.connection_id.get(),
3024 corr = frame.header.corr,
3025 module_id = %target_module_id,
3026 "handling route.open"
3027 );
3028
3029 let role_versions = role_versions.filter(|role_versions| !role_versions.is_empty());
3035 if let Some(Err(error)) = role_versions.as_ref().map(validate_role_versions) {
3036 self.observe_route_open_refusal(
3037 ctx,
3038 &target_module_id,
3039 "invalid_role_versions",
3040 error_codes::INVALID_REQUEST,
3041 );
3042 return Ok(vec![control_error_body_frame(
3043 &frame,
3044 ErrorBody {
3045 code: error_codes::INVALID_REQUEST.to_string(),
3046 message: error.to_string(),
3047 detail: Some(serde_json::json!({ "field": ROLE_VERSIONS_FIELD })),
3048 },
3049 )?]);
3050 }
3051
3052 let Some(registration) = self
3071 .registry
3072 .get_module(&target_module_id)
3073 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
3074 else {
3075 if let Some((status, warming)) =
3076 self.supervisor_status(&target_module_id, frame.header.corr)?
3077 {
3078 if status.protocol == ModuleProtocol::None {
3085 return Ok(vec![self.route_open_refusal_frame(
3086 ctx,
3087 &frame,
3088 &target_module_id,
3089 "protocol_none",
3090 error_codes::MODULE_NO_PROTOCOL,
3091 format!(
3092 "module_id '{target_module_id}' is declared protocol: none; \
3093 it speaks no subc wire and serves no routes"
3094 ),
3095 )?]);
3096 }
3097 let code = if warming {
3098 "module_warming"
3099 } else {
3100 "target_unavailable"
3101 };
3102 return Ok(vec![self.supervised_absent_route_open_refusal_frame(
3103 ctx,
3104 &frame,
3105 &target_module_id,
3106 code,
3107 &status,
3108 )?]);
3109 }
3110 if let Some(removed_ago_ms) =
3111 self.supervisor.removal_tombstone_age_ms(&target_module_id)
3112 {
3113 return Ok(vec![self.route_open_refusal_frame(
3114 ctx,
3115 &frame,
3116 &target_module_id,
3117 "removed",
3118 error_codes::MODULE_REMOVED,
3119 format!("module_id '{target_module_id}' was removed {removed_ago_ms} ms ago"),
3120 )?]);
3121 }
3122 return Ok(vec![self.route_open_refusal_frame(
3123 ctx,
3124 &frame,
3125 &target_module_id,
3126 "not_registered",
3127 error_codes::UNKNOWN_MODULE,
3128 format!("module_id '{target_module_id}' is not registered"),
3129 )?]);
3130 };
3131
3132 if !registration.ready {
3136 self.counters
3137 .increment_route_open_refused(ROUTE_OPEN_REFUSED_DECLARED_NOT_READY);
3138 info!(
3139 target: "control",
3140 code = error_codes::MODULE_WARMING,
3141 module_id = ?target_module_id,
3142 connection_id = ctx.connection_id.get(),
3143 reason = "declared_not_ready",
3144 "route.open refused"
3145 );
3146 self.route_outages
3149 .record_not_serving(&target_module_id, "declared_not_ready");
3150 return Ok(vec![control_error_body_frame(
3151 &frame,
3152 ErrorBody {
3153 code: error_codes::MODULE_WARMING.to_string(),
3154 message: format!(
3155 "module_id '{target_module_id}' is registered and has declared itself not ready; retry"
3156 ),
3157 detail: Some(serde_json::json!({
3158 "reason": "declared_not_ready"
3159 })),
3160 },
3161 )?]);
3162 }
3163
3164 if let Some(capability) = self.first_unprovided_required_capability(®istration) {
3183 self.counters
3184 .increment_route_open_refused(ROUTE_OPEN_REFUSED_REQUIRED_CAPABILITY_UNPROVIDED);
3185 info!(
3186 target: "control",
3187 code = error_codes::MODULE_WARMING,
3188 module_id = ?target_module_id,
3189 connection_id = ctx.connection_id.get(),
3190 reason = NotReadyReason::REQUIRED_CAPABILITY_UNPROVIDED,
3191 capability = %capability,
3192 "route.open refused"
3193 );
3194 return Ok(vec![control_error_body_frame(
3195 &frame,
3196 ErrorBody {
3197 code: error_codes::MODULE_WARMING.to_string(),
3198 message: format!(
3199 "module_id '{target_module_id}' requires capability '{capability}', \
3200 which no registered module provides; retry"
3201 ),
3202 detail: Some(serde_json::json!({
3203 "reason": NotReadyReason::REQUIRED_CAPABILITY_UNPROVIDED,
3204 "capability": capability,
3205 })),
3206 },
3207 )?]);
3208 }
3209
3210 if !target_has_required_role(&target, ®istration.manifest.provides) {
3211 return Ok(vec![self.route_open_refusal_frame(
3212 ctx,
3213 &frame,
3214 &target_module_id,
3215 "role_not_provided",
3216 "target_unavailable",
3217 format!("module_id '{target_module_id}' does not provide the requested target"),
3218 )?]);
3219 }
3220
3221 if registration.state != ChannelState::Active {
3222 return Ok(vec![self.route_open_refusal_frame(
3223 ctx,
3224 &frame,
3225 &target_module_id,
3226 "registration_not_active",
3227 "target_unavailable",
3228 format!("module_id '{target_module_id}' is not active"),
3229 )?]);
3230 }
3231
3232 if self
3233 .forwarding
3234 .module_is_draining(&target_module_id)
3235 .map_err(RouterError::Forwarding)?
3236 {
3237 return Ok(vec![self.route_open_refusal_frame(
3238 ctx,
3239 &frame,
3240 &target_module_id,
3241 "reloading",
3242 "module_reloading",
3243 format!("module_id '{target_module_id}' is reloading"),
3244 )?]);
3245 }
3246
3247 if let Some(process_liveness) = self.process_liveness.as_ref().filter(|process_liveness| {
3248 process_liveness.process_live(&target_module_id) == Some(false)
3249 }) {
3250 if process_liveness.process_replacing(&target_module_id) {
3257 return Ok(vec![self.route_open_refusal_frame(
3258 ctx,
3259 &frame,
3260 &target_module_id,
3261 "reloading",
3262 "module_reloading",
3263 format!("module_id '{target_module_id}' is reloading"),
3264 )?]);
3265 }
3266 return Ok(vec![self.route_open_refusal_frame(
3267 ctx,
3268 &frame,
3269 &target_module_id,
3270 "supervisor_not_live",
3271 "target_unavailable",
3272 format!("module_id '{target_module_id}' is not live"),
3273 )?]);
3274 }
3275
3276 if !self
3277 .forwarding
3278 .has_live_module_connection(&target_module_id)
3279 .map_err(RouterError::Forwarding)?
3280 {
3281 return Ok(vec![self.route_open_refusal_frame(
3282 ctx,
3283 &frame,
3284 &target_module_id,
3285 "no_forwarding_connection",
3286 "target_unavailable",
3287 format!("module_id '{target_module_id}' has no live forwarding connection"),
3288 )?]);
3289 }
3290
3291 if let Some(error) =
3292 self.guard_module_control_op(&frame, &target_module_id, "route.bind")?
3293 {
3294 self.observe_route_open_refusal(
3295 ctx,
3296 &target_module_id,
3297 "op_not_allowed",
3298 "op_not_allowed",
3299 );
3300 return Ok(vec![error]);
3301 }
3302
3303 let principal = match self.route_open_principal(&frame, consumer_identity)? {
3304 Ok(principal) => principal,
3305 Err(error) => {
3306 self.observe_route_open_refusal(
3307 ctx,
3308 &target_module_id,
3309 "bad_consumer_identity",
3310 "bad_consumer_identity",
3311 );
3312 return Ok(vec![error]);
3313 }
3314 };
3315
3316 if let Principal::Reserved {
3320 module_id: opening_module_id,
3321 } = &principal
3322 {
3323 if let Some(opening_registration) = self
3324 .registry
3325 .get_module(opening_module_id)
3326 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
3327 {
3328 if let Some(capability) =
3329 denied_capability(&opening_registration.manifest, ®istration.manifest)
3330 {
3331 warn!(
3332 opening_module_id,
3333 target_module_id,
3334 capability,
3335 "refusing route.open because an attested capability deny edge matches"
3336 );
3337 return Ok(vec![self.route_open_refusal_frame(
3338 ctx,
3339 &frame,
3340 &target_module_id,
3341 "capability_deny_edge",
3342 "capability_forbidden",
3343 format!(
3344 "module_id '{opening_module_id}' must never reach capability '{capability}' provided by '{target_module_id}'"
3345 ),
3346 )?]);
3347 }
3348 }
3349 }
3350
3351 if admission_facts.is_some() {
3352 let carrier_matches = matches!(
3353 &principal,
3354 Principal::Reserved { module_id }
3355 if self.admission_facts_carrier_module_id.as_deref() == Some(module_id)
3356 );
3357 if !carrier_matches {
3358 return Ok(vec![self.route_open_refusal_frame(
3359 ctx,
3360 &frame,
3361 &target_module_id,
3362 "admission_facts_carrier_not_permitted",
3363 "admission_facts_not_permitted",
3364 "admission facts may only be carried by the configured reserved module",
3365 )?]);
3366 }
3367
3368 let target_allowed = self
3369 .admission_facts_targets
3370 .as_ref()
3371 .is_some_and(|targets| targets.iter().any(|id| id == &target_module_id));
3372 if !target_allowed {
3373 return Ok(vec![self.route_open_refusal_frame(
3374 ctx,
3375 &frame,
3376 &target_module_id,
3377 "admission_facts_target_not_listed",
3378 "admission_facts_target_not_allowed",
3379 format!(
3380 "admission facts are not permitted for target module_id '{target_module_id}'"
3381 ),
3382 )?]);
3383 }
3384
3385 }
3389
3390 let (bound_scope, scope_stamp) = match scope {
3396 None => (None, None),
3397 Some(selector) => {
3398 let owner_configured = match &selector.owner {
3399 Principal::Reserved { module_id } => self.supervisor.is_configured(module_id),
3405 _ => false,
3406 };
3407 let admitted = self
3408 .scopes
3409 .read()
3410 .unwrap_or_else(|poisoned| poisoned.into_inner())
3411 .admit(&principal, &target_module_id, &selector, owner_configured)
3412 .and_then(|admission| {
3413 crate::scopes::check_target_flow_support(
3414 &admission.stamp,
3415 &target_module_id,
3416 registration.manifest.capabilities.as_ref(),
3417 )?;
3418 Ok(admission)
3419 });
3420 match admitted {
3421 Ok(admission) => (
3422 Some(BoundScope {
3423 owner: admission.owner,
3424 scope_ref: admission.stamp.scope_ref.clone(),
3425 tag: admission.tag,
3426 }),
3427 Some(admission.stamp),
3428 ),
3429 Err(refusal) => {
3430 return Ok(vec![self.route_open_refusal_frame(
3431 ctx,
3432 &frame,
3433 &target_module_id,
3434 refusal.code,
3435 refusal.code,
3436 refusal.message,
3437 )?]);
3438 }
3439 }
3440 }
3441 };
3442
3443 let project_root = match ProjectRootId::from_path_allowing_missing(&identity.project_root) {
3465 Ok(project_root) => project_root,
3466 Err(err) => {
3467 return Ok(vec![control_error_frame(
3468 &frame,
3469 "invalid_project_root",
3470 err.to_string(),
3471 )?])
3472 }
3473 };
3474 identity.project_root = project_root.as_path().to_path_buf();
3475
3476 let _concurrency_guard = match self
3487 .route_bind_concurrency
3488 .try_admit(&target_module_id, MAX_PENDING_ROUTE_BINDS_PER_TARGET)
3489 {
3490 Ok(guard) => guard,
3491 Err(in_flight) => {
3492 return Ok(vec![self.route_open_target_capacity_refusal(
3493 ctx,
3494 &frame,
3495 &target_module_id,
3496 in_flight,
3497 )?]);
3498 }
3499 };
3500
3501 let mut breaker = match self.route_bind_breakers.admit(&target_module_id) {
3504 RouteBindAdmission::Admitted { guard, probe } => {
3505 if probe {
3506 info!(
3507 module_id = %target_module_id,
3508 connection_id = ctx.connection_id.get(),
3509 "route.bind breaker half-open: admitting one probe"
3510 );
3511 }
3512 guard
3513 }
3514 RouteBindAdmission::Refused {
3515 consecutive_timeouts,
3516 retry_in,
3517 probe_in_flight,
3518 } => {
3519 return Ok(vec![self.route_open_breaker_refusal_frame(
3520 ctx,
3521 &frame,
3522 &target_module_id,
3523 consecutive_timeouts,
3524 retry_in,
3525 probe_in_flight,
3526 )?]);
3527 }
3528 };
3529
3530 let route_bind_relay_timeout = self.route_bind_relay_timeout_for(&target_module_id);
3535 let relay_deadline = Instant::now() + route_bind_relay_timeout;
3536 let pending = match self
3537 .forwarding
3538 .begin_route_bind_relay_for(
3539 ctx.connection_id,
3540 ctx.egress.clone(),
3541 response_version(&frame),
3542 frame.header.corr,
3543 &target_module_id,
3544 principal.clone(),
3545 bound_scope,
3546 Some(project_root),
3547 relay_deadline,
3548 )
3549 .await
3550 {
3551 Ok(pending) => pending,
3552 Err(err) => {
3553 return Ok(vec![self.route_open_refusal_frame(
3554 ctx,
3555 &frame,
3556 &target_module_id,
3557 "relay_reservation_failed",
3558 forwarding_error_code(&err),
3559 err.to_string(),
3560 )?])
3561 }
3562 };
3563 let crate::forwarding::PendingRouteBindRelay {
3564 endpoint,
3565 module_sink,
3566 negotiated_ver,
3567 client_channel,
3568 client_epoch,
3569 module_channel,
3570 module_epoch,
3571 corr: relay_corr,
3572 receiver,
3573 } = pending;
3574 let mut reservation =
3575 RouteBindReservationGuard::new(Arc::clone(&self.forwarding), endpoint, relay_corr);
3576
3577 if let Some(stamp) = scope_stamp
3582 .as_ref()
3583 .filter(|stamp| stamp.attributes.flow_id.is_some())
3584 {
3585 let relay_registration = self
3586 .registry
3587 .get_module_by_connection(endpoint.connection_id)
3588 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
3589 if let Err(refusal) = crate::scopes::check_target_flow_support(
3590 stamp,
3591 &target_module_id,
3592 relay_registration
3593 .as_ref()
3594 .and_then(|registration| registration.manifest.capabilities.as_ref()),
3595 ) {
3596 reservation.release_and_disarm();
3597 return Ok(vec![self.route_open_refusal_frame(
3598 ctx,
3599 &frame,
3600 &target_module_id,
3601 refusal.code,
3602 refusal.code,
3603 refusal.message,
3604 )?]);
3605 }
3606 }
3607
3608 debug!(
3609 connection_id = ctx.connection_id.get(),
3610 client_channel,
3611 client_epoch,
3612 module_channel,
3613 module_epoch,
3614 "reserved route handle pair"
3615 );
3616 let principal_label = match &principal {
3619 Principal::Reserved { module_id } => format!("reserved:{module_id}"),
3620 Principal::Direct => "direct".to_string(),
3621 other => format!("{other:?}"),
3622 };
3623 let relay = ModuleControlRequest::RouteBind {
3624 route_channel: module_channel,
3625 epoch: module_epoch,
3626 target,
3627 identity,
3628 principal: Some(principal),
3629 consumer_capabilities,
3630 role_versions,
3631 admission_facts,
3632 scope: scope_stamp,
3633 };
3634 let relay_body = serde_json::to_vec(&relay).map_err(|err| {
3635 RouterError::backend(
3636 0,
3637 frame.header.corr,
3638 format!("failed to encode route.bind request: {err}"),
3639 )
3640 })?;
3641 let relay_frame = Frame::build_with_version(
3642 negotiated_ver,
3643 FrameType::Request,
3644 control_flags(),
3645 0,
3646 0,
3647 relay_corr,
3648 relay_body,
3649 )
3650 .map_err(RouterError::FrameBuild)?;
3651
3652 if let Err(err) = module_sink.send(relay_frame).await {
3653 reservation.release_and_disarm();
3654 return Ok(vec![self.route_open_refusal_frame(
3655 ctx,
3656 &frame,
3657 &target_module_id,
3658 "relay_send_failed",
3659 "target_unavailable",
3660 err.to_string(),
3661 )?]);
3662 }
3663
3664 if !self
3665 .forwarding
3666 .mark_route_bind_relay_enqueued(endpoint, relay_corr)
3667 .map_err(RouterError::Forwarding)?
3668 {
3669 self.send_abandoned_route_bind_goodbye(
3670 &module_sink,
3671 negotiated_ver,
3672 module_channel,
3673 module_epoch,
3674 );
3675 }
3676
3677 match timeout_at(relay_deadline, receiver).await {
3678 Ok(Ok(RouteBindRelayOutcome::Accepted)) => {
3679 reservation.disarm();
3680 if breaker.record_accepted() {
3681 info!(
3682 module_id = %target_module_id,
3683 "route.bind breaker closed: the probe was accepted"
3684 );
3685 }
3686 self.observe_route_open_accept(ctx, &target_module_id, &principal_label);
3687 Ok(Vec::new())
3688 }
3689 Ok(Ok(RouteBindRelayOutcome::Rejected(body))) => {
3690 reservation.release_and_disarm();
3691 breaker.record_inconclusive();
3695 let scope_code = match body.code.as_str() {
3699 error_codes::SCOPE_CHANGED => Some(error_codes::SCOPE_CHANGED),
3700 error_codes::SCOPE_ENDED => Some(error_codes::SCOPE_ENDED),
3701 _ => None,
3702 };
3703 if let Some(code) = scope_code {
3704 self.observe_route_open_refusal(
3705 ctx,
3706 &target_module_id,
3707 "scope_changed_before_commit",
3708 code,
3709 );
3710 return Ok(vec![control_error_body_frame(&frame, body)?]);
3711 }
3712 self.counters
3713 .increment_route_open_refused("module_rejected");
3714 info!(
3715 target: "control",
3716 code = "module_rejected",
3717 module_code = ?body.code,
3718 module_id = ?target_module_id,
3719 connection_id = ctx.connection_id.get(),
3720 "route.open refused"
3721 );
3722 Ok(vec![control_error_body_frame(&frame, body)?])
3723 }
3724 Ok(Ok(RouteBindRelayOutcome::ModuleGone(message))) => {
3725 reservation.release_and_disarm();
3726 breaker.record_inconclusive();
3727 tracing::warn!(
3734 module_id = %target_module_id,
3735 "route.bind relay abandoned: {message}"
3736 );
3737 Ok(vec![self.route_open_refusal_frame(
3738 ctx,
3739 &frame,
3740 &target_module_id,
3741 "relay_abandoned",
3742 "target_unavailable",
3743 message,
3744 )?])
3745 }
3746 Ok(Err(_)) => {
3747 reservation.release_and_disarm();
3748 breaker.record_inconclusive();
3749 Ok(vec![self.route_open_refusal_frame(
3750 ctx,
3751 &frame,
3752 &target_module_id,
3753 "relay_waiter_canceled",
3754 "target_unavailable",
3755 "route.bind relay waiter was canceled before the module responded",
3756 )?])
3757 }
3758 Err(_) => {
3759 reservation.release_and_disarm();
3760 if let Some(opened) = breaker.record_timeout(
3764 self.route_bind_breaker_threshold,
3765 self.route_bind_breaker_cooldown,
3766 ) {
3767 warn!(
3768 module_id = %target_module_id,
3769 consecutive_timeouts = opened.consecutive_timeouts,
3770 cooldown_ms = self.route_bind_breaker_cooldown.as_millis() as u64,
3771 reopened_after_probe = opened.reopened_after_probe,
3772 "route.bind breaker open: refusing route.open for this module without relaying until one probe says it recovered"
3773 );
3774 }
3775 tracing::warn!(
3783 module_id = %target_module_id,
3784 timeout_ms = route_bind_relay_timeout.as_millis() as u64,
3785 "route.bind relay timed out: module did not ack within budget"
3786 );
3787 Ok(vec![self.route_open_refusal_frame(
3788 ctx,
3789 &frame,
3790 &target_module_id,
3791 "relay_timed_out",
3792 "module_timeout",
3793 format!(
3794 "module_id '{target_module_id}' did not answer route.bind within {:?}",
3795 route_bind_relay_timeout
3796 ),
3797 )?])
3798 }
3799 }
3800 }
3801
3802 fn handle_supervisor_spawn_snapshot(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
3803 let response = ClientControlResponse::SupervisorSpawnSnapshot {
3804 snapshot: self.supervisor.spawn_snapshot(),
3805 };
3806 Ok(vec![control_response_body_frame(
3807 &frame,
3808 &response,
3809 "ClientControlResponse::SupervisorSpawnSnapshot",
3810 )?])
3811 }
3812
3813 fn handle_supervisor_spawn_subscribe(
3814 &self,
3815 ctx: &RouteCtx,
3816 frame: Frame,
3817 since: Option<SpawnCursor>,
3818 ) -> Result<Vec<Frame>, RouterError> {
3819 match self.supervisor.subscribe_spawns(
3820 ctx.connection_id,
3821 frame.header.corr,
3822 response_version(&frame),
3823 since,
3824 ctx.egress.clone(),
3825 ) {
3826 Ok(()) => Ok(Vec::new()),
3827 Err(SpawnSubscribeRefusal::ForeignIncarnation { current }) => {
3828 Ok(vec![control_error_body_frame(
3829 &frame,
3830 ErrorBody {
3831 code: "spawn_cursor_incarnation_mismatch".to_string(),
3832 message: "spawn cursor belongs to a different daemon incarnation"
3833 .to_string(),
3834 detail: Some(serde_json::json!({
3835 "current_daemon_incarnation": current
3836 })),
3837 },
3838 )?])
3839 }
3840 Err(SpawnSubscribeRefusal::TooOld { oldest }) => Ok(vec![control_error_body_frame(
3841 &frame,
3842 ErrorBody {
3843 code: "spawn_cursor_too_old".to_string(),
3844 message: "spawn cursor predates the retained event ring".to_string(),
3845 detail: Some(serde_json::json!({
3846 "oldest_retained_cursor": oldest
3847 })),
3848 },
3849 )?]),
3850 Err(SpawnSubscribeRefusal::Frame(error)) => Err(RouterError::backend(
3851 0,
3852 frame.header.corr,
3853 format!("failed to open supervisor spawn subscription: {error}"),
3854 )),
3855 }
3856 }
3857
3858 async fn handle_supervisor_list(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
3859 let generation = self
3860 .registry
3861 .generation()
3862 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
3863 let mut modules = Vec::new();
3864 for module in self.supervisor.list() {
3865 let status = module.status_for_control("list").map_err(|err| {
3866 RouterError::backend(
3867 0,
3868 frame.header.corr,
3869 format!("failed to read supervisor status: {err}"),
3870 )
3871 })?;
3872 let (configured, _) = module.configuration().map_err(|err| {
3873 RouterError::backend(
3874 0,
3875 frame.header.corr,
3876 format!("failed to read module configuration: {err}"),
3877 )
3878 })?;
3879 let image = module.running_image_agreement().await;
3881 let resources = Some(module.child_resource_usage());
3884 let pending_reload = Some(reload_verdict(
3885 &configured.program,
3886 status.spawned_from.as_deref(),
3887 image,
3888 ));
3889 modules.push(SupervisorEntry {
3890 launch_nonce_env: Some(!cfg!(unix)),
3893 module_id: status.module_id,
3894 state: status.state.to_string(),
3895 enabled: status.enabled,
3896 live: status.live,
3897 protocol: status.protocol,
3898 health: status.health.status,
3899 pending_reload,
3900 last_probe_ms: status.health.last_probe_ms,
3901 last_exit_code: status.last_exit.as_ref().and_then(|e| e.code),
3902 last_exit_signal: status.last_exit.as_ref().and_then(|e| e.signal),
3903 last_exit_ms: status.last_exit.as_ref().map(|e| e.at_ms),
3904 last_exit_kind: status.last_exit.as_ref().map(|e| e.kind.into()),
3905 restart_count: Some(status.restart_count),
3906 max_restarts: Some(status.max_restarts),
3907 lifetime_restarts: Some(status.lifetime_restarts),
3908 spawn_generation: Some(status.spawn_generation),
3909 restart_window_secs: Some(status.restart_window.as_secs()),
3910 drain_timeout_ms: Some(status.drain_timeout.as_millis() as u64),
3911 restart_backoff_ms: Some(status.restart_backoff.as_millis() as u64),
3912 restart_max_backoff_ms: Some(status.restart_max_backoff.as_millis() as u64),
3913 resources,
3914 });
3915 }
3916 let response = ClientControlResponse::SupervisorList {
3917 generation,
3918 modules,
3919 };
3920 Ok(vec![control_response_body_frame(
3921 &frame,
3922 &response,
3923 "ClientControlResponse::SupervisorList",
3924 )?])
3925 }
3926
3927 fn handle_supervisor_stderr_tail(
3928 &self,
3929 frame: Frame,
3930 module_id: String,
3931 max_lines: Option<u32>,
3932 max_bytes: Option<u32>,
3933 ) -> Result<Vec<Frame>, RouterError> {
3934 let Some(module) = self.supervisor.get(&module_id) else {
3935 return Ok(vec![control_error_frame(
3936 &frame,
3937 "unknown_module",
3938 format!("module_id '{module_id}' is not supervised"),
3939 )?]);
3940 };
3941
3942 let snapshot = module.stderr_tail(
3943 max_lines.map(|value| value as usize),
3944 max_bytes.map(|value| value as usize),
3945 );
3946
3947 let response = ClientControlResponse::SupervisorStderrTail {
3948 module_id,
3949 tail: StderrTail {
3950 capture: match snapshot.capture {
3951 CaptureState::Captured => StderrCaptureState::Captured,
3952 CaptureState::Incomplete { reason } => {
3953 StderrCaptureState::Incomplete { reason }
3954 }
3955 CaptureState::NotCaptured { reason } => {
3956 StderrCaptureState::NotCaptured { reason }
3957 }
3958 },
3959 entries: snapshot
3960 .entries
3961 .into_iter()
3962 .map(|entry| match entry {
3963 TailEntry::Line {
3964 text,
3965 truncated,
3966 at_ms,
3967 } => StderrTailEntry::Line {
3968 text,
3969 truncated,
3970 at_ms,
3971 },
3972 TailEntry::ProcessStart => StderrTailEntry::ProcessStart,
3973 })
3974 .collect(),
3975 dropped_lines: snapshot.dropped_lines,
3976 },
3977 };
3978 Ok(vec![control_response_body_frame(
3979 &frame,
3980 &response,
3981 "ClientControlResponse::SupervisorStderrTail",
3982 )?])
3983 }
3984
3985 async fn handle_supervisor_terminals(
3986 &self,
3987 frame: Frame,
3988 module_id: String,
3989 ) -> Result<Vec<Frame>, RouterError> {
3990 let Some(module) = self.supervisor.get(&module_id) else {
3991 return Ok(vec![control_error_frame(
3992 &frame,
3993 "unknown_module",
3994 format!("module_id '{module_id}' is not supervised"),
3995 )?]);
3996 };
3997
3998 let terminals = module
4001 .read_durable_terminal_history()
4002 .await
4003 .map_err(|error| {
4004 RouterError::backend(
4005 0,
4006 frame.header.corr,
4007 format!("failed to read terminal history: {error}"),
4008 )
4009 })?;
4010 let response = ClientControlResponse::SupervisorTerminals {
4011 module_id,
4012 terminals,
4013 };
4014 Ok(vec![control_response_body_frame(
4015 &frame,
4016 &response,
4017 "ClientControlResponse::SupervisorTerminals",
4018 )?])
4019 }
4020
4021 fn handle_supervisor_routes(
4022 &self,
4023 frame: Frame,
4024 module_id: Option<String>,
4025 ) -> Result<Vec<Frame>, RouterError> {
4026 let modules = self
4027 .forwarding
4028 .route_census(module_id.as_deref())
4029 .map_err(RouterError::Forwarding)?
4030 .into_iter()
4031 .map(|(module_id, routes)| SupervisorRouteModule {
4032 module_id,
4033 routes: routes
4034 .into_iter()
4035 .map(|route| SupervisorRoute {
4036 consumer: match route.principal {
4037 Principal::Reserved { module_id } => {
4038 SupervisorRouteConsumer::Reserved { module_id }
4039 }
4040 Principal::Direct | Principal::Unverified => {
4041 SupervisorRouteConsumer::Direct {
4042 connection_id: route.goodbye_target.connection_id.get(),
4043 }
4044 }
4045 },
4046 age_ms: Instant::now()
4047 .saturating_duration_since(route.bound_at)
4048 .as_millis()
4049 .try_into()
4050 .unwrap_or(u64::MAX),
4051 draining: route.draining,
4052 drain_reason: route.drain_reason,
4053 })
4054 .collect(),
4055 })
4056 .collect();
4057 let response = ClientControlResponse::SupervisorRoutes { modules };
4058 Ok(vec![control_response_body_frame(
4059 &frame,
4060 &response,
4061 "ClientControlResponse::SupervisorRoutes",
4062 )?])
4063 }
4064
4065 async fn handle_supervisor_provenance(
4066 &self,
4067 frame: Frame,
4068 module_id: Option<String>,
4069 ) -> Result<Vec<Frame>, RouterError> {
4070 let mut selected = if let Some(module_id) = module_id {
4071 let Some(module) = self.supervisor.get(&module_id) else {
4072 return Ok(vec![control_error_frame(
4073 &frame,
4074 "unknown_module",
4075 format!("module_id '{module_id}' is not supervised"),
4076 )?]);
4077 };
4078 vec![module]
4079 } else {
4080 self.supervisor.list()
4081 };
4082
4083 let mut modules = Vec::with_capacity(selected.len());
4084 for module in selected.drain(..) {
4085 let (status, observed_image) = module
4086 .status_and_running_image_agreement()
4087 .await
4088 .map_err(|err| {
4089 RouterError::backend(
4090 0,
4091 frame.header.corr,
4092 format!("failed to read supervisor status: {err}"),
4093 )
4094 })?;
4095 let module_declared = self
4096 .registry
4097 .get_module(&status.module_id)
4098 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
4099 .and_then(|registration| registration.manifest.provenance)
4100 .map(|build| ModuleDeclaredProvenance::Reported { build })
4101 .unwrap_or(ModuleDeclaredProvenance::Unverifiable);
4102 #[cfg(test)]
4103 let running_image = match &self.provenance_probe_override {
4104 Some(result) => result.clone(),
4105 None => observed_image,
4106 };
4107 #[cfg(not(test))]
4108 let running_image = observed_image;
4109 modules.push(SupervisorModuleProvenance {
4110 module_id: status.module_id,
4111 module_declared,
4112 daemon_observed: SupervisorObservedProcess {
4113 pid: status.pid,
4114 spawned_at_ms: status.spawned_at_ms,
4115 spawned_from: status.spawned_from,
4116 running_image,
4117 },
4118 });
4119 }
4120 let daemon = SupervisorDaemonProvenance {
4121 daemon_build: self.daemon_provenance.build.clone(),
4122 daemon_observed: DaemonObservedProcess {
4123 pid: self.daemon_provenance.pid,
4124 started_at_ms: self
4125 .daemon_provenance
4126 .start_clock
4127 .map(|clock| clock.started_at_ms())
4128 .or(self.daemon_provenance.started_at_ms),
4129 running_image: self
4130 .daemon_provenance
4131 .probe
4132 .observe(
4133 self.daemon_provenance.pid,
4134 self.daemon_provenance.executable_path.as_deref(),
4135 self.daemon_provenance.executable_identity,
4136 self.daemon_provenance.process_start_time,
4137 )
4138 .await,
4139 },
4140 };
4141 let response = ClientControlResponse::SupervisorProvenance { daemon, modules };
4142 Ok(vec![control_response_body_frame(
4143 &frame,
4144 &response,
4145 "ClientControlResponse::SupervisorProvenance",
4146 )?])
4147 }
4148
4149 fn handle_supervisor_health(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
4150 self.refresh_capability_requirements();
4151 let generation = self
4152 .registry
4153 .generation()
4154 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
4155 let modules = self
4156 .supervisor
4157 .list()
4158 .into_iter()
4159 .map(|module| {
4160 let status = module.status_for_control("health").map_err(|err| {
4161 RouterError::backend(
4162 0,
4163 frame.header.corr,
4164 format!("failed to read supervisor health: {err}"),
4165 )
4166 })?;
4167 let module_id = status.module_id;
4168 let capability_detail = self
4169 .capability_evaluator
4170 .required_problem_detail(&module_id);
4171 Ok(SupervisorHealthEntry {
4172 module_id,
4173 status: status.health.status,
4174 detail: append_capability_problem_detail(
4175 status.health.detail,
4176 capability_detail,
4177 ),
4178 metrics: status.health.metrics,
4179 consecutive_failures: status.health.consecutive_failures,
4180 late_answer_count: status.health.late_answer_count,
4181 last_late_answer_latency_ms: status.health.last_late_answer_latency_ms,
4182 last_action: status.health.last_action,
4183 last_action_ms: status.health.last_action_ms,
4184 last_probe_ms: status.health.last_probe_ms,
4185 })
4186 })
4187 .collect::<Result<Vec<_>, RouterError>>()?;
4188 let response = ClientControlResponse::SupervisorHealth {
4189 generation,
4190 modules,
4191 };
4192 Ok(vec![control_response_body_frame(
4193 &frame,
4194 &response,
4195 "ClientControlResponse::SupervisorHealth",
4196 )?])
4197 }
4198
4199 async fn handle_supervisor_restart(
4200 &self,
4201 frame: Frame,
4202 module_id: String,
4203 drain_timeout_ms: Option<u64>,
4204 ) -> Result<Vec<Frame>, RouterError> {
4205 let operation_lock = self.supervisor.operation_lock();
4206 let _operation_guard = operation_lock.lock().await;
4207 let Some(module) = self.supervisor.get(&module_id) else {
4208 return Ok(vec![control_error_frame(
4209 &frame,
4210 "unknown_module",
4211 format!("module_id '{module_id}' is not supervised"),
4212 )?]);
4213 };
4214
4215 self.route_outages.mark_operator_action(&module_id);
4216 if let Err(err) = module.restart(drain_timeout_ms).await {
4217 self.route_outages
4218 .operator_action_ended_unrefused(&module_id);
4219 let (code, message) = match err {
4220 crate::supervise::SuperviseError::Disabled { .. } => {
4221 ("module_disabled", err.to_string())
4222 }
4223 crate::supervise::SuperviseError::SwapInProgress { .. } => {
4224 ("swap_in_progress", err.to_string())
4225 }
4226 _ => (
4227 "target_unavailable",
4228 format!("failed to restart module_id '{module_id}': {err}"),
4229 ),
4230 };
4231 return Ok(vec![control_error_frame(&frame, code, message)?]);
4232 }
4233
4234 let response = ClientControlResponse::SupervisorAck {
4235 module_id,
4236 applied: true,
4237 };
4238 Ok(vec![control_response_body_frame(
4239 &frame,
4240 &response,
4241 "ClientControlResponse::SupervisorAck",
4242 )?])
4243 }
4244
4245 async fn handle_supervisor_swap(
4249 &self,
4250 frame: Frame,
4251 module_id: String,
4252 ready_timeout_ms: Option<u64>,
4253 ) -> Result<Vec<Frame>, RouterError> {
4254 let module = {
4261 let operation_lock = self.supervisor.operation_lock();
4262 let _operation_guard = operation_lock.lock().await;
4263 self.supervisor.get(&module_id)
4264 };
4265 let Some(module) = module else {
4266 return Ok(vec![control_error_frame(
4267 &frame,
4268 "unknown_module",
4269 format!("module_id '{module_id}' is not supervised"),
4270 )?]);
4271 };
4272
4273 self.route_outages.mark_operator_action(&module_id);
4274 if let Err(err) = module
4275 .swap(ready_timeout_ms.map(Duration::from_millis))
4276 .await
4277 {
4278 self.route_outages
4279 .operator_action_ended_unrefused(&module_id);
4280 use crate::supervise::SuperviseError;
4281 let message = err.to_string();
4282 let error = match err {
4283 SuperviseError::Disabled { .. } => ErrorBody::new("module_disabled", message),
4284 SuperviseError::SwapRefused { reason, .. } => ErrorBody {
4285 code: "swap_refused".to_string(),
4286 message,
4287 detail: Some(serde_json::json!({ "reason": reason.as_str() })),
4288 },
4289 SuperviseError::SwapFailed {
4290 arm,
4291 candidate_exit,
4292 ..
4293 } => ErrorBody {
4294 code: "swap_failed".to_string(),
4295 message,
4296 detail: Some(serde_json::json!({
4297 "arm": arm.as_str(),
4298 "candidate_exit_code": candidate_exit.as_ref().and_then(|exit| exit.code),
4299 "candidate_exit_signal": candidate_exit.as_ref().and_then(|exit| exit.signal),
4300 })),
4301 },
4302 _ => ErrorBody::new(
4303 "target_unavailable",
4304 format!("failed to swap module_id '{module_id}': {message}"),
4305 ),
4306 };
4307 return Ok(vec![control_error_body_frame(&frame, error)?]);
4308 }
4309 self.route_outages
4313 .operator_action_ended_unrefused(&module_id);
4314
4315 let response = ClientControlResponse::SupervisorAck {
4316 module_id,
4317 applied: true,
4318 };
4319 Ok(vec![control_response_body_frame(
4320 &frame,
4321 &response,
4322 "ClientControlResponse::SupervisorAck",
4323 )?])
4324 }
4325
4326 async fn handle_supervisor_reload(
4327 &self,
4328 frame: Frame,
4329 module_id: String,
4330 ) -> Result<Vec<Frame>, RouterError> {
4331 let operation_lock = self.supervisor.operation_lock();
4332 let _operation_guard = operation_lock.lock().await;
4333 let Some(module) = self.supervisor.get(&module_id) else {
4334 return Ok(vec![control_error_frame(
4335 &frame,
4336 "unknown_module",
4337 format!("module_id '{module_id}' is not supervised"),
4338 )?]);
4339 };
4340
4341 self.route_outages.mark_operator_action(&module_id);
4342 if let Err(err) = module.reload().await {
4343 self.route_outages
4344 .operator_action_ended_unrefused(&module_id);
4345 let (code, message) = match err {
4346 crate::supervise::SuperviseError::Disabled { .. } => {
4347 ("module_disabled", err.to_string())
4348 }
4349 crate::supervise::SuperviseError::SwapInProgress { .. } => {
4350 ("swap_in_progress", err.to_string())
4351 }
4352 _ => (
4353 "reload_failed",
4354 format!("failed to reload module_id '{module_id}': {err}"),
4355 ),
4356 };
4357 return Ok(vec![control_error_frame(&frame, code, message)?]);
4358 }
4359
4360 let response = ClientControlResponse::SupervisorAck {
4361 module_id,
4362 applied: true,
4363 };
4364 Ok(vec![control_response_body_frame(
4365 &frame,
4366 &response,
4367 "ClientControlResponse::SupervisorAck",
4368 )?])
4369 }
4370
4371 async fn handle_supervisor_rescan(
4372 &self,
4373 frame: Frame,
4374 preview: bool,
4375 ) -> Result<Vec<Frame>, RouterError> {
4376 let Some(context) = self.rescan.clone() else {
4377 return Ok(vec![control_error_frame(
4378 &frame,
4379 "rescan_unavailable",
4380 "the daemon was not started with a reloadable config path".to_string(),
4381 )?]);
4382 };
4383
4384 let operation_lock = self.supervisor.operation_lock();
4385 let _operation_guard = operation_lock.lock().await;
4386 let loaded = match crate::daemon_config::load(&context.config_path) {
4387 Ok(config) => config,
4388 Err(err) => {
4389 return Ok(vec![control_error_frame(
4390 &frame,
4391 "invalid_daemon_config",
4392 format!("supervisor rescan rejected daemon config: {err}"),
4393 )?])
4394 }
4395 };
4396 let Some(config) = loaded else {
4404 return Ok(vec![control_error_frame(
4405 &frame,
4406 "invalid_daemon_config",
4407 format!(
4408 "daemon config not found at {}; refusing to rescan (an absent config would \
4409 retire every supervised module)",
4410 context.config_path.display()
4411 ),
4412 )?]);
4413 };
4414 let (
4415 configured_port,
4416 storage_config,
4417 admission_facts_carrier_module_id,
4418 admission_facts_targets,
4419 scope_authority_owners,
4420 modules,
4421 reserved_capabilities,
4422 ) = (
4423 config.port,
4424 config.storage,
4425 config.admission_facts_carrier_module_id,
4426 config.admission_facts_targets,
4427 config.scope_authority_owners,
4428 config.modules,
4429 config.reserved_capabilities,
4430 );
4431
4432 let mut restart_required = Vec::new();
4441 for section in RestartRequiredSection::ALL {
4442 let changed = match section {
4443 RestartRequiredSection::Port => configured_port != context.configured_port,
4444 RestartRequiredSection::Storage => storage_config != context.storage_config,
4445 RestartRequiredSection::AdmissionFactsCarrierModuleId => {
4446 admission_facts_carrier_module_id != context.admission_facts_carrier_module_id
4447 }
4448 RestartRequiredSection::AdmissionFactsTargets => {
4449 admission_facts_targets != context.admission_facts_targets
4450 }
4451 RestartRequiredSection::ScopeAuthorityOwners => {
4452 scope_authority_owners != context.scope_authority_owners
4453 }
4454 };
4455 if changed {
4456 restart_required.push(section.label().to_string());
4457 }
4458 }
4459 if !restart_required.is_empty() {
4460 warn!(
4461 config_path = %context.config_path.display(),
4462 sections = %restart_required.join(", "),
4463 "daemon config changed outside the modules section; restart the daemon to apply those changes"
4464 );
4465 }
4466
4467 for configured in &modules {
4468 if let Err(err) = validate_spec(&configured.module_spec()) {
4469 return Ok(vec![control_error_frame(
4470 &frame,
4471 "invalid_daemon_config",
4472 format!("supervisor rescan rejected daemon config: {err}"),
4473 )?]);
4474 }
4475 }
4476
4477 let configured_capabilities = modules
4478 .iter()
4479 .map(|module| (module.module_id.clone(), module.enabled))
4480 .collect::<Vec<_>>();
4481 let preview_capability_warnings = if preview {
4482 let (_, registrations) = self.runtime_capability_snapshot()?;
4483 let current_modules = self
4484 .supervisor
4485 .list()
4486 .into_iter()
4487 .map(|module| module.module_id().to_string())
4488 .collect::<BTreeSet<_>>();
4489 let resulting_modules = configured_capabilities.clone();
4490 let removed = current_modules
4491 .into_iter()
4492 .filter(|module_id| {
4493 !resulting_modules
4494 .iter()
4495 .any(|(configured_id, _)| configured_id == module_id)
4496 })
4497 .collect::<Vec<_>>();
4498 self.capability_evaluator.preview_removal_warnings(
4499 resulting_modules,
4500 &removed,
4501 ®istrations,
4502 )
4503 } else {
4504 Vec::new()
4505 };
4506 let result = match self
4507 .reconcile_supervised_modules(&context.supervisor, modules, preview)
4508 .await
4509 {
4510 Ok(result) => result,
4511 Err(message) => {
4512 return Ok(vec![control_error_frame(&frame, "rescan_failed", message)?])
4513 }
4514 };
4515 if !preview {
4516 self.capability_evaluator
4517 .configure(configured_capabilities, reserved_capabilities);
4518 self.capability_evaluator.wake_deadline_loop();
4519 self.refresh_capability_requirements();
4520 }
4521 let mut result = result;
4522 result.restart_required = restart_required;
4523 result.capability_warnings = preview_capability_warnings;
4524 let response = ClientControlResponse::SupervisorRescan { result };
4525 Ok(vec![control_response_body_frame(
4526 &frame,
4527 &response,
4528 "ClientControlResponse::SupervisorRescan",
4529 )?])
4530 }
4531
4532 async fn handle_supervisor_release_reserved(
4533 &self,
4534 frame: Frame,
4535 module_id: String,
4536 ) -> Result<Vec<Frame>, RouterError> {
4537 let Some(context) = self.rescan.clone() else {
4538 return Ok(vec![control_error_frame(
4539 &frame,
4540 "release_unavailable",
4541 "reserved-id release requires a daemon started with a reloadable config path",
4542 )?]);
4543 };
4544 let operation_lock = self.supervisor.operation_lock();
4545 let _operation_guard = operation_lock.lock().await;
4546 let loaded = match crate::daemon_config::load(&context.config_path) {
4547 Ok(Some(config)) => config,
4548 Ok(None) => {
4549 return Ok(vec![control_error_frame(
4550 &frame,
4551 "invalid_daemon_config",
4552 format!(
4553 "daemon config not found at {}; refusing to release reserved module_id '{module_id}'",
4554 context.config_path.display()
4555 ),
4556 )?])
4557 }
4558 Err(err) => {
4559 return Ok(vec![control_error_frame(
4560 &frame,
4561 "invalid_daemon_config",
4562 format!("unable to verify reserved-id release against daemon config: {err}"),
4563 )?])
4564 }
4565 };
4566 if loaded
4567 .modules
4568 .iter()
4569 .any(|configured| configured.module_id == module_id)
4570 {
4571 return Ok(vec![control_error_frame(
4572 &frame,
4573 "reserved_module_configured",
4574 format!(
4575 "module_id '{module_id}' remains configured; remove its config entry and rescan before releasing its reserved id"
4576 ),
4577 )?]);
4578 }
4579 if !self.supervisor.release_retained_reserved_gate(&module_id) {
4580 return Ok(vec![control_error_frame(
4581 &frame,
4582 "reserved_gate_not_retained",
4583 format!(
4584 "module_id '{module_id}' has no retired reserved-id gate to release; rescan its removed reserved configuration first"
4585 ),
4586 )?]);
4587 }
4588
4589 let response = ClientControlResponse::SupervisorAck {
4590 module_id,
4591 applied: true,
4592 };
4593 Ok(vec![control_response_body_frame(
4594 &frame,
4595 &response,
4596 "ClientControlResponse::SupervisorAck",
4597 )?])
4598 }
4599
4600 async fn reconcile_supervised_modules(
4609 &self,
4610 supervisor: &Supervisor,
4611 configured_modules: Vec<crate::daemon_config::ConfiguredModule>,
4612 preview: bool,
4613 ) -> Result<SupervisorRescanResult, String> {
4614 let mut current = BTreeMap::new();
4615 for module in self.supervisor.list() {
4616 let (spec, health) = module.configuration().map_err(|err| {
4617 format!(
4618 "failed to read configuration for module_id '{}': {err}",
4619 module.module_id()
4620 )
4621 })?;
4622 let enabled = module
4623 .status()
4624 .map_err(|err| {
4625 format!(
4626 "failed to read status for module_id '{}': {err}",
4627 module.module_id()
4628 )
4629 })?
4630 .enabled;
4631 current.insert(
4632 module.module_id().to_string(),
4633 (module, spec, health, enabled),
4634 );
4635 }
4636 let configured = configured_modules
4637 .into_iter()
4638 .map(|module| (module.module_id.clone(), module))
4639 .collect::<BTreeMap<_, _>>();
4640
4641 let added = configured
4642 .keys()
4643 .filter(|module_id| !current.contains_key(*module_id))
4644 .cloned()
4645 .collect::<Vec<_>>();
4646 let removed = current
4647 .keys()
4648 .filter(|module_id| !configured.contains_key(*module_id))
4649 .cloned()
4650 .collect::<Vec<_>>();
4651 let mut changed_pending_reload = Vec::new();
4652 let mut configuration_changes = BTreeSet::new();
4653 let mut enabled_changes = BTreeSet::new();
4654 let mut unchanged = 0_u32;
4655
4656 for (module_id, configured_module) in &configured {
4657 let Some((_, current_spec, current_health, current_enabled)) = current.get(module_id)
4658 else {
4659 continue;
4660 };
4661 let launch_changed = *current_spec != configured_module.module_spec();
4665 let configuration_changed =
4666 launch_changed || *current_health != configured_module.health;
4667 let enabled_changed = *current_enabled != configured_module.enabled;
4668 if configuration_changed {
4669 configuration_changes.insert(module_id.clone());
4670 }
4671 if launch_changed {
4672 changed_pending_reload.push(module_id.clone());
4673 }
4674 if enabled_changed {
4675 enabled_changes.insert(module_id.clone());
4676 }
4677 if !configuration_changed && !enabled_changed {
4678 unchanged = unchanged.saturating_add(1);
4679 }
4680 }
4681
4682 if preview {
4688 return Ok(SupervisorRescanResult {
4689 added,
4690 removed,
4691 changed_pending_reload,
4692 enabled_changes: enabled_changes.iter().cloned().collect(),
4693 unchanged,
4694 preview: true,
4695 restart_required: Vec::new(),
4699 capability_warnings: Vec::new(),
4700 });
4701 }
4702
4703 for module_id in &removed {
4704 let module = ¤t
4705 .get(module_id)
4706 .expect("removed module came from current supervisor state")
4707 .0;
4708 module.retire().await.map_err(|err| {
4709 format!("failed to retire module_id '{module_id}' during rescan: {err}")
4710 })?;
4711 self.supervisor.record_rescan_removal(module_id);
4745 self.supervisor.retire(module_id);
4746 self.route_outages.forget(module_id);
4747 }
4748
4749 for module_id in configured.keys() {
4750 let Some((module, _, _, _)) = current.get(module_id) else {
4751 continue;
4752 };
4753 let configured_module = configured
4754 .get(module_id)
4755 .expect("configured module id came from configured map");
4756 if configuration_changes.contains(module_id) {
4757 module
4758 .update_configuration(
4759 configured_module.module_spec(),
4760 configured_module.health.clone(),
4761 configured_module.drain_timeout_ms,
4762 )
4763 .await
4764 .map_err(|err| {
4765 format!(
4766 "failed to update module_id '{module_id}' configuration during rescan: {err}"
4767 )
4768 })?;
4769 }
4770 if enabled_changes.contains(module_id) {
4771 self.route_outages.mark_operator_action(module_id);
4774 module
4775 .set_enabled(configured_module.enabled)
4776 .await
4777 .map_err(|err| {
4778 self.route_outages.operator_action_ended_unrefused(module_id);
4779 format!(
4780 "failed to apply module_id '{module_id}' enabled={} during rescan: {err}",
4781 configured_module.enabled
4782 )
4783 })?;
4784 }
4785 }
4786
4787 for module_id in &added {
4788 let configured_module = configured
4789 .get(module_id)
4790 .expect("added module id came from configured map");
4791 supervisor
4792 .supervise_configured_with_health(
4793 configured_module.module_spec(),
4794 configured_module.enabled,
4795 configured_module.health.clone(),
4796 configured_module.drain_timeout_ms,
4797 configured_module.restart,
4798 )
4799 .map_err(|err| {
4800 format!("failed to add module_id '{module_id}' during rescan: {err}")
4801 })?;
4802 }
4803
4804 Ok(SupervisorRescanResult {
4805 added,
4806 removed,
4807 changed_pending_reload,
4808 enabled_changes: enabled_changes.iter().cloned().collect(),
4809 unchanged,
4810 preview: false,
4811 restart_required: Vec::new(),
4814 capability_warnings: Vec::new(),
4815 })
4816 }
4817
4818 async fn handle_supervisor_set_enabled(
4819 &self,
4820 frame: Frame,
4821 module_id: String,
4822 enabled: bool,
4823 ) -> Result<Vec<Frame>, RouterError> {
4824 let operation_lock = self.supervisor.operation_lock();
4825 let _operation_guard = operation_lock.lock().await;
4826 let Some(module) = self.supervisor.get(&module_id) else {
4827 return Ok(vec![control_error_frame(
4828 &frame,
4829 "unknown_module",
4830 format!("module_id '{module_id}' is not supervised"),
4831 )?]);
4832 };
4833
4834 self.route_outages.mark_operator_action(&module_id);
4837 let applied = match module.set_enabled(enabled).await {
4838 Ok(applied) => applied,
4839 Err(err) => {
4840 self.route_outages
4841 .operator_action_ended_unrefused(&module_id);
4842 return Ok(vec![control_error_frame(
4843 &frame,
4844 "target_unavailable",
4845 format!("failed to set module_id '{module_id}' enabled={enabled}: {err}"),
4846 )?]);
4847 }
4848 };
4849 if !applied {
4850 self.route_outages
4853 .operator_action_ended_unrefused(&module_id);
4854 }
4855
4856 self.capability_evaluator.wake_deadline_loop();
4857 self.refresh_capability_requirements();
4858 let response = ClientControlResponse::SupervisorAck { module_id, applied };
4859 Ok(vec![control_response_body_frame(
4860 &frame,
4861 &response,
4862 "ClientControlResponse::SupervisorAck",
4863 )?])
4864 }
4865
4866 async fn handle_supervisor_health_probe(
4867 &self,
4868 frame: Frame,
4869 module_id: String,
4870 ) -> Result<Vec<Frame>, RouterError> {
4871 self.refresh_capability_requirements();
4872 let Some(registration) = self
4873 .registry
4874 .get_module(&module_id)
4875 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
4876 else {
4877 return Ok(vec![control_error_frame(
4878 &frame,
4879 "unknown_module",
4880 format!("module_id '{module_id}' is not registered"),
4881 )?]);
4882 };
4883
4884 if !module_registration_grants_op(®istration.control_ops, MODULE_CONTROL_OP_HEALTH_CHECK)
4897 {
4898 return Ok(vec![control_error_frame(
4899 &frame,
4900 "health_not_advertised",
4901 format!("module_id '{module_id}' did not advertise health.check"),
4902 )?]);
4903 }
4904
4905 let deadline = Instant::now() + self.health_probe_timeout;
4906 let pending = match self.forwarding.begin_module_control_rpc_for(
4907 &module_id,
4908 MODULE_CONTROL_OP_HEALTH_CHECK,
4909 deadline,
4910 ) {
4911 Ok(pending) => pending,
4912 Err(err) => {
4913 return Ok(vec![control_error_frame(
4914 &frame,
4915 forwarding_error_code(&err),
4916 err.to_string(),
4917 )?])
4918 }
4919 };
4920
4921 let PendingModuleControlRpc {
4922 endpoint,
4923 module_sink,
4924 negotiated_ver,
4925 corr: probe_corr,
4926 receiver,
4927 } = pending;
4928 let mut guard =
4929 ModuleControlRpcGuard::new(Arc::clone(&self.forwarding), endpoint, probe_corr);
4930 let probe_body =
4931 serde_json::to_vec(&ModuleControlRequest::HealthCheck {}).map_err(|err| {
4932 RouterError::backend(
4933 0,
4934 frame.header.corr,
4935 format!("failed to encode health.check request: {err}"),
4936 )
4937 })?;
4938 let probe_frame = Frame::build_with_version(
4939 negotiated_ver,
4940 FrameType::Request,
4941 control_flags(),
4942 0,
4943 0,
4944 probe_corr,
4945 probe_body,
4946 )
4947 .map_err(RouterError::FrameBuild)?;
4948
4949 if let Err(err) = module_sink.send(probe_frame).await {
4950 return Ok(vec![control_error_frame(
4951 &frame,
4952 "target_unavailable",
4953 err.to_string(),
4954 )?]);
4955 }
4956
4957 match timeout_at(deadline, receiver).await {
4958 Ok(Ok(ModuleControlRpcOutcome::Response(response))) => {
4959 guard.disarm();
4960 let Some(report) = response.health_report() else {
4961 return Ok(vec![control_error_frame(
4962 &frame,
4963 "invalid_control_body",
4964 "health.check RPC returned a non-health response",
4965 )?]);
4966 };
4967 let HealthReport {
4972 status,
4973 detail,
4974 metrics,
4975 } = report;
4976 let capability_detail = self
4977 .capability_evaluator
4978 .required_problem_detail(&module_id);
4979 let response = ClientControlResponse::SupervisorHealthProbe {
4980 module_id,
4981 status,
4982 detail: append_capability_problem_detail(detail, capability_detail),
4983 metrics,
4984 };
4985 Ok(vec![control_response_body_frame(
4986 &frame,
4987 &response,
4988 "ClientControlResponse::SupervisorHealthProbe",
4989 )?])
4990 }
4991 Ok(Ok(ModuleControlRpcOutcome::Rejected(body))) => {
4992 guard.disarm();
4993 Ok(vec![control_error_body_frame(&frame, body)?])
4994 }
4995 Ok(Ok(ModuleControlRpcOutcome::ModuleGone(message))) => {
4996 guard.disarm();
4997 Ok(vec![control_error_frame(
4998 &frame,
4999 "target_unavailable",
5000 message,
5001 )?])
5002 }
5003 Ok(Ok(ModuleControlRpcOutcome::MalformedResponse(message))) => {
5004 guard.disarm();
5005 Ok(vec![control_error_frame(
5006 &frame,
5007 "invalid_control_body",
5008 message,
5009 )?])
5010 }
5011 Ok(Ok(ModuleControlRpcOutcome::UnexpectedOp { expected, actual })) => {
5012 guard.disarm();
5013 Ok(vec![control_error_frame(
5014 &frame,
5015 "invalid_control_body",
5016 format!("expected module-control op '{expected}', got '{actual}'"),
5017 )?])
5018 }
5019 Ok(Ok(ModuleControlRpcOutcome::DeadlineElapsed)) => {
5020 guard.disarm();
5021 Ok(vec![control_error_frame(
5022 &frame,
5023 "module_timeout",
5024 format!(
5025 "module_id '{module_id}' answered health.check after {:?}",
5026 self.health_probe_timeout
5027 ),
5028 )?])
5029 }
5030 Ok(Err(_)) => Ok(vec![control_error_frame(
5031 &frame,
5032 "target_unavailable",
5033 "health.check waiter was canceled before the module responded",
5034 )?]),
5035 Err(_) => Ok(vec![control_error_frame(
5036 &frame,
5037 "module_timeout",
5038 format!(
5039 "module_id '{module_id}' did not answer health.check within {:?}",
5040 self.health_probe_timeout
5041 ),
5042 )?]),
5043 }
5044 }
5045
5046 fn supervisor_status(
5047 &self,
5048 module_id: &str,
5049 corr: u64,
5050 ) -> Result<Option<(crate::supervise::ModuleStatus, bool)>, RouterError> {
5051 self.supervisor
5052 .get(module_id)
5053 .map(|module| {
5054 let warming = module.is_warming_for_control("status").map_err(|err| {
5055 RouterError::backend(
5056 0,
5057 corr,
5058 format!(
5059 "failed to read supervisor warming state for module_id '{module_id}': {err}"
5060 ),
5061 )
5062 })?;
5063 module.status_for_control("status").map_err(|err| {
5064 RouterError::backend(
5065 0,
5066 corr,
5067 format!(
5068 "failed to read supervisor status for module_id '{module_id}': {err}"
5069 ),
5070 )
5071 }).map(|status| (status, warming))
5072 })
5073 .transpose()
5074 }
5075
5076 fn guard_module_control_op(
5077 &self,
5078 frame: &Frame,
5079 module_id: &str,
5080 op: &str,
5081 ) -> Result<Option<Frame>, RouterError> {
5082 if self.module_grants_op(module_id, op, frame.header.corr)? {
5083 return Ok(None);
5084 }
5085
5086 Ok(Some(control_error_frame(
5087 frame,
5088 "op_not_allowed",
5089 format!("module_id '{module_id}' did not grant control op '{op}'"),
5090 )?))
5091 }
5092
5093 fn module_grants_op(&self, module_id: &str, op: &str, corr: u64) -> Result<bool, RouterError> {
5094 let Some(registration) = self
5095 .registry
5096 .get_module(module_id)
5097 .map_err(|err| RouterError::backend(0, corr, err.to_string()))?
5098 else {
5099 return Ok(false);
5100 };
5101 Ok(module_registration_grants_op(®istration.control_ops, op))
5102 }
5103
5104 fn handle_status_update(
5105 &self,
5106 endpoint: ModuleEndpointId,
5107 frame: Frame,
5108 ) -> Result<Vec<Frame>, RouterError> {
5109 let update = match serde_json::from_slice::<ModuleControlPush>(&frame.body) {
5110 Ok(update) => update,
5111 Err(err) => {
5112 if is_known_module_push_op(&frame.body) {
5117 return Ok(vec![control_error_frame(
5118 &frame,
5119 "invalid_control_body",
5120 format!("malformed module control push body: {err}"),
5121 )?]);
5122 }
5123 return Ok(Vec::new());
5124 }
5125 };
5126
5127 match update {
5128 ModuleControlPush::RouteStatus {
5129 route_channel,
5130 route_epoch,
5131 status,
5132 } => {
5133 self.forwarding
5134 .cache_status(endpoint, route_channel, route_epoch, status)
5135 .map_err(RouterError::Forwarding)?;
5136 }
5137 }
5138 Ok(Vec::new())
5139 }
5140
5141 fn handle_route_poll(
5142 &self,
5143 ctx: &RouteCtx,
5144 frame: Frame,
5145 route_channel: u16,
5146 route_epoch: u32,
5147 kind: PollKind,
5148 ) -> Result<Vec<Frame>, RouterError> {
5149 let snapshot = self
5150 .forwarding
5151 .route_poll_snapshot(ctx.connection_id, route_channel, route_epoch)
5152 .map_err(RouterError::Forwarding)?;
5153 let response = match (kind, snapshot) {
5154 (PollKind::Status, RoutePollSnapshot::Bound { status, .. }) => {
5155 ClientControlResponse::RoutePoll {
5156 route_channel,
5157 route_epoch,
5158 status,
5159 live: None,
5160 }
5161 }
5162 (PollKind::Status, RoutePollSnapshot::Absent) => ClientControlResponse::RoutePoll {
5163 route_channel,
5164 route_epoch,
5165 status: None,
5166 live: None,
5167 },
5168 (PollKind::Liveness, RoutePollSnapshot::Bound { module_id, .. }) => {
5169 let live = self
5195 .process_liveness
5196 .as_ref()
5197 .and_then(|source| source.process_live(&module_id))
5198 .unwrap_or(true);
5199 ClientControlResponse::RoutePoll {
5200 route_channel,
5201 route_epoch,
5202 status: None,
5203 live: Some(live),
5204 }
5205 }
5206 (PollKind::Liveness, RoutePollSnapshot::Absent) => ClientControlResponse::RoutePoll {
5207 route_channel,
5208 route_epoch,
5209 status: None,
5210 live: Some(false),
5211 },
5212 };
5213
5214 Ok(vec![control_response_body_frame(
5215 &frame,
5216 &response,
5217 "ClientControlResponse::RoutePoll",
5218 )?])
5219 }
5220
5221 pub(crate) fn observe_module_control_completion(
5222 &self,
5223 completion: ModuleControlRpcCompletion,
5224 ) -> bool {
5225 match completion {
5226 ModuleControlRpcCompletion::Unknown => false,
5227 ModuleControlRpcCompletion::Settled => true,
5228 ModuleControlRpcCompletion::LateHealthAnswer { module_id, latency } => {
5229 let latency_ms = latency.as_millis().min(u128::from(u64::MAX)) as u64;
5230 info!(
5231 module_id = %module_id,
5232 latency_ms,
5233 "late health.check answer proves the module is alive"
5234 );
5235 match self
5236 .supervisor
5237 .record_late_health_answer(&module_id, latency_ms)
5238 {
5239 Ok(true) => {}
5240 Ok(false) => debug!(
5241 module_id = %module_id,
5242 latency_ms,
5243 "late health.check answer has no active supervisor snapshot"
5244 ),
5245 Err(err) => warn!(
5246 module_id = %module_id,
5247 latency_ms,
5248 error = %err,
5249 "failed to record late health.check answer"
5250 ),
5251 }
5252 true
5253 }
5254 }
5255 }
5256
5257 fn refuse_to_end_module_connection_for_a_client(
5278 &self,
5279 module_connection_id: ConnectionId,
5280 corr: u64,
5281 err: ForwardingError,
5282 ) -> Result<(), RouterError> {
5283 if let ForwardingError::ConnectionClosing { connection_id } = err {
5284 if connection_id != module_connection_id {
5285 warn!(
5286 module_connection_id = module_connection_id.get(),
5287 client_connection_id = connection_id.get(),
5288 corr,
5289 "dropping a route.bind response for a closing client; the module connection keeps serving"
5290 );
5291 return Ok(());
5292 }
5293 }
5294 Err(RouterError::Forwarding(err))
5295 }
5296
5297 fn handle_module_relay_response(
5298 &self,
5299 connection_id: ConnectionId,
5300 frame: Frame,
5301 ) -> Result<Vec<Frame>, RouterError> {
5302 let mut secondary_error = None;
5303 let outcome = match frame.header.ty {
5304 FrameType::Response => match serde_json::from_slice::<ControlOpProbe>(&frame.body) {
5305 Ok(probe) if probe.op == "route.bind" => {
5306 match serde_json::from_slice::<ModuleControlResponse>(&frame.body) {
5307 Ok(ModuleControlResponse::RouteBindAck {}) => {
5308 RouteBindRelayOutcome::Accepted
5309 }
5310 Ok(other) => {
5311 let message =
5312 format!("route.bind response carried unexpected body: {other:?}");
5313 secondary_error = Some(control_error_frame(
5314 &frame,
5315 "invalid_control_body",
5316 message.clone(),
5317 )?);
5318 RouteBindRelayOutcome::ModuleGone(message)
5319 }
5320 Err(err) => {
5321 let message = format!("malformed route.bind response body: {err}");
5322 secondary_error = Some(control_error_frame(
5323 &frame,
5324 "invalid_control_body",
5325 message.clone(),
5326 )?);
5327 RouteBindRelayOutcome::ModuleGone(message)
5328 }
5329 }
5330 }
5331 Ok(probe) => {
5332 let outcome = match serde_json::from_slice::<ModuleControlResponse>(&frame.body)
5333 {
5334 Ok(response) => ModuleControlRpcOutcome::Response(response),
5335 Err(err) => ModuleControlRpcOutcome::MalformedResponse(format!(
5336 "malformed {} response body: {err}",
5337 probe.op
5338 )),
5339 };
5340 let completion = self
5341 .forwarding
5342 .complete_module_control_rpc(
5343 connection_id,
5344 frame.header.corr,
5345 Some(&probe.op),
5346 outcome,
5347 )
5348 .map_err(RouterError::Forwarding)?;
5349 if !self.observe_module_control_completion(completion) {
5350 debug!(
5351 connection_id = connection_id.get(),
5352 corr = frame.header.corr,
5353 op = %probe.op,
5354 "dropping late or unknown module-control RPC response"
5355 );
5356 }
5357 return Ok(Vec::new());
5358 }
5359 Err(err) => {
5360 if let Some(expected_op) = self
5361 .forwarding
5362 .pending_module_control_op(connection_id, frame.header.corr)
5363 .map_err(RouterError::Forwarding)?
5364 {
5365 let completion = self
5366 .forwarding
5367 .complete_module_control_rpc(
5368 connection_id,
5369 frame.header.corr,
5370 None,
5371 ModuleControlRpcOutcome::MalformedResponse(format!(
5372 "malformed {expected_op} response body: {err}"
5373 )),
5374 )
5375 .map_err(RouterError::Forwarding)?;
5376 if !self.observe_module_control_completion(completion) {
5377 debug!(
5378 connection_id = connection_id.get(),
5379 corr = frame.header.corr,
5380 "dropping late malformed module-control RPC response"
5381 );
5382 }
5383 return Ok(Vec::new());
5384 }
5385 let message = format!("malformed route.bind response body: {err}");
5386 secondary_error = Some(control_error_frame(
5387 &frame,
5388 "invalid_control_body",
5389 message.clone(),
5390 )?);
5391 RouteBindRelayOutcome::ModuleGone(message)
5392 }
5393 },
5394 FrameType::Error => {
5395 if self
5396 .forwarding
5397 .pending_module_control_op(connection_id, frame.header.corr)
5398 .map_err(RouterError::Forwarding)?
5399 .is_some()
5400 {
5401 let outcome = match serde_json::from_slice::<ErrorBody>(&frame.body) {
5402 Ok(body) => ModuleControlRpcOutcome::Rejected(body),
5403 Err(err) => ModuleControlRpcOutcome::MalformedResponse(format!(
5404 "malformed module-control ERROR body: {err}"
5405 )),
5406 };
5407 let completion = self
5408 .forwarding
5409 .complete_module_control_rpc(
5410 connection_id,
5411 frame.header.corr,
5412 None,
5413 outcome,
5414 )
5415 .map_err(RouterError::Forwarding)?;
5416 if !self.observe_module_control_completion(completion) {
5417 debug!(
5418 connection_id = connection_id.get(),
5419 corr = frame.header.corr,
5420 "dropping late or unknown module-control RPC error"
5421 );
5422 }
5423 return Ok(Vec::new());
5424 }
5425 match serde_json::from_slice::<ErrorBody>(&frame.body) {
5426 Ok(body) => RouteBindRelayOutcome::Rejected(body),
5427 Err(err) => {
5428 let message = format!("malformed route.bind ERROR body: {err}");
5429 secondary_error = Some(control_error_frame(
5430 &frame,
5431 "invalid_control_body",
5432 message.clone(),
5433 )?);
5434 RouteBindRelayOutcome::ModuleGone(message)
5435 }
5436 }
5437 }
5438 ty => {
5439 return Ok(vec![control_error_frame(
5440 &frame,
5441 "unsupported_control_frame",
5442 format!("unsupported module channel-0 frame {ty:?}"),
5443 )?])
5444 }
5445 };
5446
5447 let settled =
5448 self.forwarding
5449 .complete_pending_relay(connection_id, frame.header.corr, outcome);
5450 let completion = match settled {
5451 Ok(completion) => completion,
5452 Err(err) => {
5453 self.refuse_to_end_module_connection_for_a_client(
5454 connection_id,
5455 frame.header.corr,
5456 err,
5457 )?;
5458 return Ok(secondary_error.into_iter().collect());
5459 }
5460 };
5461 if let Some(target) = completion.abandoned.as_ref() {
5462 send_goodbye_target_best_effort(&self.counters, target, "late accepted route.bind");
5463 }
5464 if !completion.settled {
5465 debug!(
5466 connection_id = connection_id.get(),
5467 corr = frame.header.corr,
5468 frame_type = ?frame.header.ty,
5469 "dropping late or unknown route.bind relay response"
5470 );
5471 }
5472 Ok(secondary_error.into_iter().collect())
5473 }
5474
5475 fn handle_goodbye(&self, connection_id: ConnectionId) -> Result<Vec<Frame>, RouterError> {
5476 debug!(connection_id = connection_id.get(), "handling GOODBYE");
5477 self.cleanup_connection(connection_id)
5481 .map_err(|err| RouterError::backend(0, 0, err.to_string()))?;
5482 Ok(Vec::new())
5483 }
5484}
5485
5486impl Default for ControlHandler {
5487 fn default() -> Self {
5488 Self::new(Arc::new(Registry::default()))
5489 }
5490}
5491
5492impl crate::supervise::SwapPromotionObserver for ControlHandler {
5493 fn swap_promoted(&self, registration: &crate::registry::ModuleRegistration) {
5494 self.apply_registration_capabilities(registration);
5495 }
5496}
5497
5498fn capability_requirement_status(status: RequirementStatus) -> CapabilityRequirementStatus {
5499 CapabilityRequirementStatus {
5500 consumer: status.consumer,
5501 capability: status.capability,
5502 need: match status.need {
5503 subc_protocol::manifest::CapabilityNeed::Required => "required".to_string(),
5504 subc_protocol::manifest::CapabilityNeed::Optional => "optional".to_string(),
5505 },
5506 verdict: status.verdict.as_str().to_string(),
5507 episode_seq: status.episode_seq,
5508 config_satisfiable: status.config_satisfiable,
5509 runtime_available: status.runtime_available,
5510 detail: status.detail,
5511 }
5512}
5513
5514fn append_capability_problem_detail(
5515 detail: Option<String>,
5516 capability_detail: Option<String>,
5517) -> Option<String> {
5518 match (detail, capability_detail) {
5519 (Some(detail), Some(capability_detail)) => Some(format!("{detail}; {capability_detail}")),
5520 (Some(detail), None) => Some(detail),
5521 (None, Some(capability_detail)) => Some(capability_detail),
5522 (None, None) => None,
5523 }
5524}
5525
5526fn subc_ops() -> Vec<String> {
5527 SUBC_CONTROL_OPS
5528 .iter()
5529 .map(|op| (*op).to_string())
5530 .collect()
5531}
5532
5533fn module_subc_ops() -> Vec<String> {
5534 SUBC_CONTROL_OPS
5535 .iter()
5536 .chain(MODULE_TO_SUBC_CONTROL_OPS.iter())
5537 .map(|op| (*op).to_string())
5538 .collect()
5539}
5540
5541#[cfg(test)]
5542fn module_baseline_control_ops() -> Vec<String> {
5543 MODULE_BASELINE_CONTROL_OPS
5544 .iter()
5545 .map(|op| (*op).to_string())
5546 .collect()
5547}
5548
5549fn effective_module_control_ops(declared: Option<Vec<String>>) -> Vec<String> {
5550 let mut seen = HashSet::new();
5551 let mut effective = Vec::new();
5552 for op in MODULE_BASELINE_CONTROL_OPS {
5553 if seen.insert((*op).to_string()) {
5554 effective.push((*op).to_string());
5555 }
5556 }
5557 for op in declared.unwrap_or_default() {
5558 if seen.insert(op.clone()) {
5559 effective.push(op);
5560 }
5561 }
5562 effective
5563}
5564
5565fn module_registration_grants_op(control_ops: &[String], op: &str) -> bool {
5566 MODULE_BASELINE_CONTROL_OPS.contains(&op) || control_ops.iter().any(|granted| granted == op)
5567}
5568
5569fn target_module_id(target: &RouteTarget) -> &str {
5570 match target {
5571 RouteTarget::ToolProvider { module_id }
5572 | RouteTarget::ManagementSurface { module_id }
5573 | RouteTarget::InternalService { module_id, .. } => module_id,
5574 }
5575}
5576
5577fn target_has_required_role(target: &RouteTarget, roles: &[ProviderRole]) -> bool {
5578 roles.iter().any(|role| match (target, role) {
5579 (RouteTarget::ToolProvider { .. }, ProviderRole::ToolProvider { .. }) => true,
5580 (RouteTarget::ManagementSurface { .. }, ProviderRole::ManagementSurface { .. }) => true,
5581 (
5582 RouteTarget::InternalService { service_id, .. },
5583 ProviderRole::InternalService {
5584 service_id: provided,
5585 ..
5586 },
5587 ) => service_id == provided,
5588 _ => false,
5589 })
5590}
5591
5592fn is_routable_role(role: &ProviderRole) -> bool {
5593 matches!(
5594 role,
5595 ProviderRole::ToolProvider { .. }
5596 | ProviderRole::ManagementSurface { .. }
5597 | ProviderRole::InternalService { .. }
5598 )
5599}
5600
5601#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5602enum ControlRequestBodyError {
5603 UnknownOp,
5604 InvalidBody,
5605}
5606
5607#[derive(Debug, Deserialize)]
5608struct ControlOpProbe {
5609 op: String,
5610}
5611
5612const MODULE_PUSH_OPS: &[&str] = &["route.status"];
5615
5616fn is_known_module_push_op(body: &[u8]) -> bool {
5617 serde_json::from_slice::<ControlOpProbe>(body)
5618 .map(|probe| MODULE_PUSH_OPS.contains(&probe.op.as_str()))
5619 .unwrap_or(false)
5620}
5621
5622fn is_known_module_request_op(body: &[u8]) -> bool {
5623 serde_json::from_slice::<ControlOpProbe>(body)
5624 .map(|probe| is_module_to_subc_op(&probe.op))
5625 .unwrap_or(false)
5626}
5627
5628fn is_module_to_subc_op(op: &str) -> bool {
5629 MODULE_TO_SUBC_CONTROL_OPS.contains(&op) || MODULE_TO_SUBC_UNADVERTISED_OPS.contains(&op)
5630}
5631
5632fn log_control_dispatch_arrival(op: &'static str, connection_id: ConnectionId, corr: u64) {
5633 debug!(
5634 op = %op,
5635 connection_id = connection_id.get(),
5636 corr,
5637 "control dispatch"
5638 );
5639}
5640
5641fn log_slow_control_dispatch(
5642 dispatch_started_at: Option<StdInstant>,
5643 op: &'static str,
5644 connection_id: ConnectionId,
5645 corr: u64,
5646) {
5647 let Some(dispatch_started_at) = dispatch_started_at else {
5648 return;
5649 };
5650 let elapsed = dispatch_started_at.elapsed();
5651 if elapsed >= SLOW_CONTROL_DISPATCH_THRESHOLD {
5652 warn!(
5653 op = %op,
5654 connection_id = connection_id.get(),
5655 corr,
5656 elapsed_ms = elapsed.as_millis() as u64,
5657 "slow control dispatch"
5658 );
5659 }
5660}
5661
5662fn client_control_request_op(request: &ClientControlRequest) -> &'static str {
5663 match request {
5664 ClientControlRequest::ServerDescribe {} => ops::SERVER_DESCRIBE,
5665 ClientControlRequest::SupervisorProvenance { .. } => ops::SUPERVISOR_PROVENANCE,
5666 ClientControlRequest::CatalogList { .. } => ops::CATALOG_LIST,
5667 ClientControlRequest::RouteOpen { .. } => ops::ROUTE_OPEN,
5668 ClientControlRequest::RoutePoll { .. } => ops::ROUTE_POLL,
5669 ClientControlRequest::SupervisorList {} => ops::SUPERVISOR_LIST,
5670 ClientControlRequest::SupervisorSpawnSnapshot {} => ops::SUPERVISOR_SPAWN_SNAPSHOT,
5671 ClientControlRequest::SupervisorSpawnSubscribe { .. } => ops::SUPERVISOR_SPAWN_SUBSCRIBE,
5672 ClientControlRequest::SupervisorRestart { .. } => ops::SUPERVISOR_RESTART,
5673 ClientControlRequest::SupervisorSwap { .. } => ops::SUPERVISOR_SWAP,
5674 ClientControlRequest::SupervisorReload { .. } => ops::SUPERVISOR_RELOAD,
5675 ClientControlRequest::SupervisorRescan { .. } => ops::SUPERVISOR_RESCAN,
5676 ClientControlRequest::SupervisorReleaseReserved { .. } => ops::SUPERVISOR_RELEASE_RESERVED,
5677 ClientControlRequest::SupervisorSetEnabled { .. } => ops::SUPERVISOR_SET_ENABLED,
5678 ClientControlRequest::SupervisorHealthProbe { .. } => ops::SUPERVISOR_HEALTH_PROBE,
5679 ClientControlRequest::SupervisorHealth {} => ops::SUPERVISOR_HEALTH,
5680 ClientControlRequest::SupervisorRoutes { .. } => ops::SUPERVISOR_ROUTES,
5681 ClientControlRequest::SupervisorStderrTail { .. } => ops::SUPERVISOR_STDERR_TAIL,
5682 ClientControlRequest::SupervisorTerminals { .. } => ops::SUPERVISOR_TERMINALS,
5683 }
5684}
5685
5686fn module_control_request_op(request: &ModuleControlRequestFromModule) -> &'static str {
5687 match request {
5688 ModuleControlRequestFromModule::CatalogUpdate { .. } => MODULE_TO_SUBC_OP_CATALOG_UPDATE,
5689 ModuleControlRequestFromModule::LiveRoots {} => "supervisor.live_roots",
5690 ModuleControlRequestFromModule::ScopeSync { .. } => SCOPE_SYNC_OP,
5691 ModuleControlRequestFromModule::ScopeDescribe { .. } => SCOPE_DESCRIBE_OP,
5692 }
5693}
5694
5695fn parse_client_control_request(
5696 body: &[u8],
5697) -> Result<ClientControlRequest, (serde_json::Error, ControlRequestBodyError)> {
5698 serde_json::from_slice::<ClientControlRequest>(body).map_err(|err| {
5699 let classification = match serde_json::from_slice::<ControlOpProbe>(body) {
5700 Ok(probe) if SUBC_CONTROL_OPS.contains(&probe.op.as_str()) => {
5701 ControlRequestBodyError::InvalidBody
5702 }
5703 Ok(_) => ControlRequestBodyError::UnknownOp,
5704 Err(_) => ControlRequestBodyError::InvalidBody,
5705 };
5706 (err, classification)
5707 })
5708}
5709
5710fn parse_module_control_request_from_module(
5711 body: &[u8],
5712) -> Result<ModuleControlRequestFromModule, (serde_json::Error, ControlRequestBodyError)> {
5713 serde_json::from_slice::<ModuleControlRequestFromModule>(body).map_err(|err| {
5714 let classification = match serde_json::from_slice::<ControlOpProbe>(body) {
5715 Ok(probe) if is_module_to_subc_op(&probe.op) => ControlRequestBodyError::InvalidBody,
5716 Ok(_) => ControlRequestBodyError::UnknownOp,
5717 Err(_) => ControlRequestBodyError::InvalidBody,
5718 };
5719 (err, classification)
5720 })
5721}
5722
5723#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
5724enum ProviderRoleKind {
5725 ToolProvider,
5726 PipelineStage,
5727 ManagementSurface,
5728 InternalService,
5729}
5730
5731fn provider_role_kind(role: &ProviderRole) -> ProviderRoleKind {
5732 match role {
5733 ProviderRole::ToolProvider { .. } => ProviderRoleKind::ToolProvider,
5734 ProviderRole::PipelineStage { .. } => ProviderRoleKind::PipelineStage,
5735 ProviderRole::ManagementSurface { .. } => ProviderRoleKind::ManagementSurface,
5736 ProviderRole::InternalService { .. } => ProviderRoleKind::InternalService,
5737 }
5738}
5739
5740fn provider_role_kind_set(roles: &[ProviderRole]) -> BTreeSet<ProviderRoleKind> {
5741 roles.iter().map(provider_role_kind).collect()
5742}
5743
5744const MAX_LOGGED_REFUSED_SCOPE_RECORDS: usize = 8;
5747
5748#[derive(Debug, Default, PartialEq, Eq)]
5750struct ScopeOutcomeCounts {
5751 created: usize,
5752 replaced: usize,
5753 updated: usize,
5754 unchanged: usize,
5755 refused: usize,
5756}
5757
5758impl ScopeOutcomeCounts {
5759 fn of(results: &[ScopeRecordResult]) -> Self {
5760 let mut counts = Self::default();
5761 for result in results {
5762 let slot = match result.outcome {
5763 ScopeRecordOutcome::Created => &mut counts.created,
5764 ScopeRecordOutcome::Replaced => &mut counts.replaced,
5765 ScopeRecordOutcome::Updated => &mut counts.updated,
5766 ScopeRecordOutcome::Unchanged => &mut counts.unchanged,
5767 ScopeRecordOutcome::Refused => &mut counts.refused,
5768 };
5769 *slot += 1;
5770 }
5771 counts
5772 }
5773}
5774
5775#[cfg(test)]
5776mod scope_outcome_count_tests {
5777 use super::*;
5778
5779 fn result(outcome: ScopeRecordOutcome) -> ScopeRecordResult {
5780 ScopeRecordResult {
5781 scope_ref: "r".to_string(),
5782 scope_epoch: 1,
5783 outcome,
5784 code: None,
5785 message: None,
5786 version: None,
5787 parent_state: None,
5788 }
5789 }
5790
5791 #[test]
5794 fn every_outcome_is_counted_in_its_own_field() {
5795 let results = [
5796 result(ScopeRecordOutcome::Created),
5797 result(ScopeRecordOutcome::Created),
5798 result(ScopeRecordOutcome::Replaced),
5799 result(ScopeRecordOutcome::Updated),
5800 result(ScopeRecordOutcome::Unchanged),
5801 result(ScopeRecordOutcome::Refused),
5802 result(ScopeRecordOutcome::Refused),
5803 result(ScopeRecordOutcome::Refused),
5804 ];
5805 assert_eq!(
5806 ScopeOutcomeCounts::of(&results),
5807 ScopeOutcomeCounts {
5808 created: 2,
5809 replaced: 1,
5810 updated: 1,
5811 unchanged: 1,
5812 refused: 3,
5813 }
5814 );
5815 }
5816}
5817
5818fn capability_census_trigger(
5822 old: Option<&CapabilityDeclarations>,
5823 new: Option<&CapabilityDeclarations>,
5824) -> bool {
5825 let old_provides = old
5826 .map(|capabilities| capabilities.provides.iter().collect::<HashSet<_>>())
5827 .unwrap_or_default();
5828 let old_denies = old
5829 .map(|capabilities| capabilities.must_never_reach.iter().collect::<HashSet<_>>())
5830 .unwrap_or_default();
5831 let new = new.cloned().unwrap_or(CapabilityDeclarations {
5832 provides: Vec::new(),
5833 requires: Vec::new(),
5834 must_never_reach: Vec::new(),
5835 });
5836
5837 new.provides
5838 .iter()
5839 .any(|capability| !old_provides.contains(capability))
5840 || new
5841 .must_never_reach
5842 .iter()
5843 .any(|capability| !old_denies.contains(capability))
5844}
5845
5846fn denied_capability<'a>(
5849 opening_manifest: &'a ModuleManifest,
5850 target_manifest: &ModuleManifest,
5851) -> Option<&'a str> {
5852 let opening_capabilities = opening_manifest.capabilities.as_ref()?;
5853 let target_capabilities = target_manifest.capabilities.as_ref()?;
5854 opening_capabilities
5855 .must_never_reach
5856 .iter()
5857 .find(|denied| {
5858 target_capabilities
5859 .provides
5860 .iter()
5861 .any(|provided| provided == *denied)
5862 })
5863 .map(String::as_str)
5864}
5865
5866fn catalog_update_frozen_field_message(
5867 registered: &ModuleManifest,
5868 provides: &[ProviderRole],
5869) -> Option<String> {
5870 let old_has_provides = !registered.provides.is_empty();
5871 let new_has_provides = !provides.is_empty();
5872 if old_has_provides != new_has_provides {
5873 return Some(format!(
5874 "catalog.update cannot change module '{}' between supervision-only and routable; routability is fixed at HELLO",
5875 registered.module_id
5876 ));
5877 }
5878
5879 if provider_role_kind_set(®istered.provides) != provider_role_kind_set(provides) {
5880 return Some(format!(
5881 "catalog.update cannot change provider role kinds for module '{}'; role kinds are fixed at HELLO",
5882 registered.module_id
5883 ));
5884 }
5885
5886 let registered_concurrency = manifest_concurrency(registered);
5887 let mut candidate = registered.clone();
5888 candidate.provides = provides.to_vec();
5889 let candidate_concurrency = manifest_concurrency(&candidate);
5890 if candidate_concurrency != registered_concurrency {
5891 return Some(format!(
5892 "catalog.update cannot change module '{}' concurrency from {:?} to {:?}; concurrency is fixed at HELLO",
5893 registered.module_id, registered_concurrency, candidate_concurrency
5894 ));
5895 }
5896
5897 None
5900}
5901
5902fn manifest_provides_routable_role(manifest: &ModuleManifest) -> bool {
5903 manifest.provides.iter().any(is_routable_role)
5904}
5905
5906fn manifest_concurrency(manifest: &ModuleManifest) -> Concurrency {
5912 manifest
5913 .provides
5914 .iter()
5915 .find_map(|provider| match provider {
5916 ProviderRole::ToolProvider { concurrency, .. }
5917 | ProviderRole::ManagementSurface { concurrency, .. } => Some(concurrency.clone()),
5918 ProviderRole::PipelineStage { .. } | ProviderRole::InternalService { .. } => None,
5919 })
5920 .unwrap_or(Concurrency::ModuleManaged)
5921}
5922
5923fn manifest_concurrency_was_defaulted(raw_hello: &[u8], manifest: &ModuleManifest) -> bool {
5930 let has_management_surface = manifest
5931 .provides
5932 .iter()
5933 .any(|provider| matches!(provider, ProviderRole::ManagementSurface { .. }));
5934 if !has_management_surface {
5935 return false;
5936 }
5937 let Ok(raw) = serde_json::from_slice::<serde_json::Value>(raw_hello) else {
5938 return false;
5939 };
5940 let Some(provides) = raw
5941 .get("manifest")
5942 .and_then(|manifest| manifest.get("provides"))
5943 .and_then(serde_json::Value::as_array)
5944 else {
5945 return false;
5946 };
5947 provides.iter().any(|role| {
5952 role.get("role").and_then(serde_json::Value::as_str) == Some("management_surface")
5953 && role.get("concurrency").is_none()
5954 })
5955}
5956
5957fn negotiate_version(peer_version: u8) -> Result<u8, String> {
5958 if peer_version != PROTOCOL_VERSION {
5959 return Err(format!(
5960 "protocol_ver {peer_version} is unsupported; this daemon requires exactly {PROTOCOL_VERSION}"
5961 ));
5962 }
5963 Ok(PROTOCOL_VERSION)
5964}
5965
5966fn pong(frame: &Frame) -> Result<Frame, RouterError> {
5967 Frame::build_with_version(
5968 response_version(frame),
5969 FrameType::Pong,
5970 frame.header.flags,
5971 0,
5972 0,
5973 frame.header.corr,
5974 Vec::new(),
5975 )
5976 .map_err(RouterError::FrameBuild)
5977}
5978
5979fn control_error_frame(
5980 frame: &Frame,
5981 code: &'static str,
5982 message: impl Into<String>,
5983) -> Result<Frame, RouterError> {
5984 control_error_body_frame(
5985 frame,
5986 ErrorBody {
5987 code: code.to_string(),
5988 message: message.into(),
5989 detail: None,
5990 },
5991 )
5992}
5993
5994fn control_error_body_frame(frame: &Frame, error: ErrorBody) -> Result<Frame, RouterError> {
5995 let body = serde_json::to_vec(&error).map_err(|err| {
5996 RouterError::backend(
5997 0,
5998 frame.header.corr,
5999 format!("failed to encode control ERROR: {err}"),
6000 )
6001 })?;
6002
6003 Frame::build_with_version(
6004 response_version(frame),
6005 FrameType::Error,
6006 control_flags(),
6007 0,
6008 0,
6009 frame.header.corr,
6010 body,
6011 )
6012 .map_err(RouterError::FrameBuild)
6013}
6014
6015fn control_response_body_frame<T: Serialize>(
6016 frame: &Frame,
6017 reply: &T,
6018 label: &'static str,
6019) -> Result<Frame, RouterError> {
6020 let body = serde_json::to_vec(reply).map_err(|err| {
6021 RouterError::backend(
6022 0,
6023 frame.header.corr,
6024 format!("failed to encode {label}: {err}"),
6025 )
6026 })?;
6027
6028 Frame::build_with_version(
6029 response_version(frame),
6030 FrameType::Response,
6031 control_flags(),
6032 0,
6033 0,
6034 frame.header.corr,
6035 body,
6036 )
6037 .map_err(RouterError::FrameBuild)
6038}
6039
6040fn forwarding_error_code(err: &ForwardingError) -> &'static str {
6070 match err {
6071 ForwardingError::ConnectionRoleConflict { .. } => "invalid_request",
6072 ForwardingError::NoModuleConnection => "target_unavailable",
6073 ForwardingError::ModuleReloading { .. } => "module_reloading",
6074 ForwardingError::ClientRouteChannelExhausted { .. }
6075 | ForwardingError::ModuleRouteChannelExhausted { .. } => "route_limit",
6076 ForwardingError::StaleModuleEndpoint
6077 | ForwardingError::UnknownReservation { .. }
6078 | ForwardingError::ConnectionClosing { .. }
6079 | ForwardingError::ClientEgressClosed { .. }
6080 | ForwardingError::ModuleEgressUnavailable { .. } => "target_unavailable",
6081 ForwardingError::CandidateSlotOccupied { .. } => "duplicate_module_id",
6084 ForwardingError::RelayCorrelationExhausted
6085 | ForwardingError::RouteOpenBuild(_)
6086 | ForwardingError::Poisoned => "forwarding_error",
6087 }
6088}
6089
6090fn response_version(frame: &Frame) -> u8 {
6091 if (MIN_SUPPORTED_VERSION..=PROTOCOL_VERSION).contains(&frame.header.ver) {
6092 frame.header.ver
6093 } else {
6094 PROTOCOL_VERSION
6095 }
6096}
6097
6098fn control_flags() -> Flags {
6099 Flags::new(false, Priority::Passive, false)
6100}
6101
6102fn send_goodbye_target_best_effort(
6107 counters: &DaemonCounters,
6108 target: &GoodbyeTarget,
6109 context: &'static str,
6110) {
6111 let Ok(frame) = Frame::build_with_version(
6112 target.negotiated_ver,
6113 FrameType::Goodbye,
6114 control_flags(),
6115 target.channel,
6116 target.epoch,
6117 0,
6118 Vec::new(),
6119 ) else {
6120 return;
6121 };
6122 crate::forwarding::send_module_route_goodbye(
6123 counters,
6124 &target.sink,
6125 frame,
6126 target.module_id.as_deref(),
6127 context,
6128 );
6129}
6130
6131pub(crate) fn send_route_control_pushes(
6132 forwarding: &ForwardingTable,
6133 routes: Vec<EndpointRoute>,
6134 push: ClientControlPush,
6135) {
6136 let mut targets: Vec<(GoodbyeTarget, Vec<u16>)> = Vec::new();
6137 for route in routes {
6138 let target = route.goodbye_target;
6139 if let Some((existing, channels)) = targets
6140 .iter_mut()
6141 .find(|(existing, _)| existing.connection_id == target.connection_id)
6142 {
6143 debug_assert_eq!(
6144 existing.negotiated_ver, target.negotiated_ver,
6145 "one connection cannot negotiate multiple frame versions"
6146 );
6147 if !channels.contains(&target.channel) {
6148 channels.push(target.channel);
6149 }
6150 continue;
6151 }
6152 let channel = target.channel;
6153 targets.push((target, vec![channel]));
6154 }
6155 for (target, mut channels) in targets {
6156 channels.sort_unstable();
6157 let mut push = push.clone();
6158 match &mut push {
6159 ClientControlPush::RouteClosing {
6160 channels: covered, ..
6161 }
6162 | ClientControlPush::RouteClosed {
6163 channels: covered, ..
6164 } => *covered = channels,
6165 }
6166 let body = match serde_json::to_vec(&push) {
6167 Ok(body) => body,
6168 Err(err) => {
6169 warn!(error = %err, "failed to serialize route lifecycle control PUSH");
6170 continue;
6171 }
6172 };
6173 let frame = match Frame::build_with_version(
6174 target.negotiated_ver,
6175 FrameType::Push,
6176 control_flags(),
6177 0,
6178 0,
6179 0,
6180 body.clone(),
6181 ) {
6182 Ok(frame) => frame,
6183 Err(err) => {
6184 warn!(
6185 route_channel = target.channel,
6186 error = %err,
6187 "failed to build route lifecycle control PUSH frame"
6188 );
6189 continue;
6190 }
6191 };
6192 if let Err(err) = target.sink.try_send(frame) {
6193 if target.close_on_delivery_failure() {
6194 warn!(
6195 target_connection_id = target.connection_id.get(),
6196 route_channel = target.channel,
6197 error = %err,
6198 "route lifecycle control PUSH was not delivered to client; closing target connection"
6199 );
6200 let _ = forwarding.escalate_client_delivery_failure(
6201 target.connection_id,
6202 target.channel,
6203 target.epoch,
6204 CloseReason::new(
6205 "route_lifecycle_push_delivery_failed",
6206 format!(
6207 "failed to enqueue route lifecycle control PUSH for channel {}: {err}",
6208 target.channel
6209 ),
6210 ),
6211 crate::forwarding::UndeliveredFrame {
6212 module_id: target.module_id.as_deref(),
6213 sink: &target.sink,
6214 },
6215 );
6216 }
6217 }
6218 }
6219}
6220
6221#[cfg(test)]
6222mod tests {
6223 #[cfg(unix)]
6224 #[tokio::test]
6225 async fn rescan_health_only_is_live_but_launch_edits_need_reload() {
6226 let dir = subc_test_support::TestTempDir::new("rescan-live-health");
6227 let path = dir.join("subc.jsonc");
6228 std::fs::write(&path, serde_json::json!({"version":1,"modules":{"stock":{
6229 "program":"/bin/sleep","args":["60"],"protocol":"none",
6230 "env":{"XDG_DATA_HOME":dir.path(),"XDG_RUNTIME_DIR":dir.path(),"XDG_CONFIG_HOME":dir.path()}
6231 }}}).to_string()).unwrap();
6232 let mut configured = crate::daemon_config::load(&path)
6233 .unwrap()
6234 .unwrap()
6235 .modules
6236 .pop()
6237 .unwrap();
6238 let registry = std::sync::Arc::new(crate::Registry::default());
6239 let handle = crate::SupervisorHandle::new();
6240 let supervisor = crate::Supervisor::new(registry.clone(), crate::RestartPolicy::default())
6241 .with_handle(handle.clone());
6242 let module = supervisor
6243 .supervise_configured_with_health(
6244 configured.module_spec(),
6245 true,
6246 configured.health.clone(),
6247 None,
6248 configured.restart,
6249 )
6250 .unwrap();
6251 let handler = super::ControlHandler::new(registry).with_supervisor(handle);
6252 let before = module.status().unwrap().pid;
6253 configured.health.http = Some("http://127.0.0.1:1/healthz".into());
6254 configured.health.cadence = std::time::Duration::from_secs(3600);
6255 let health_only = handler
6256 .reconcile_supervised_modules(&supervisor, vec![configured.clone()], false)
6257 .await
6258 .unwrap();
6259 assert!(
6260 health_only.changed_pending_reload.is_empty(),
6261 "health policy is already applied live"
6262 );
6263 assert_eq!(module.status().unwrap().pid, before);
6264 assert_eq!(
6265 module.configuration().unwrap().1.http,
6266 configured.health.http
6267 );
6268 configured.args = vec!["61".into()];
6269 let launch = handler
6270 .reconcile_supervised_modules(&supervisor, vec![configured], false)
6271 .await
6272 .unwrap();
6273 assert_eq!(launch.changed_pending_reload, ["stock"]);
6274 assert_eq!(
6275 module.status().unwrap().pid,
6276 before,
6277 "a launch edit is stored until reload"
6278 );
6279 module.drain().await.unwrap();
6280 }
6281 use std::{
6282 collections::BTreeMap,
6283 fmt,
6284 path::PathBuf,
6285 sync::{Arc, Mutex},
6286 time::Duration,
6287 };
6288 use subc_test_support::TestTempDir;
6289
6290 use serde_json::{json, Value};
6291 use subc_protocol::{
6292 manifest::{
6293 Concurrency, ExecutionMode, IdentityScope, ManagementOperation,
6294 ManagementOperationKind, ObservabilityKind, ObservabilitySurface, ProviderRole, Tool,
6295 },
6296 session::HealthStatus,
6297 FrameType,
6298 };
6299
6300 use super::*;
6301 use crate::{
6302 forwarding::{DataRoute, DataRouteState},
6303 registry::ChannelState,
6304 router::FrameSink,
6305 stderr_tail::DEFAULT_MAX_LINE_BYTES,
6306 supervise::{ModuleSpec, ModuleState, RestartPolicy, Supervisor, SupervisorHandle},
6307 RouteCtx, Router,
6308 };
6309 use tokio::{
6310 sync::mpsc,
6311 time::{sleep, Instant},
6312 };
6313 use tracing::{
6314 field::{Field, Visit},
6315 Event, Subscriber,
6316 };
6317 use tracing_subscriber::{layer::Context, prelude::*, Layer};
6318
6319 fn fake_aft_stub_path() -> PathBuf {
6338 let mut path = std::env::current_exe().expect("current_exe available in tests");
6339 path.pop(); path.pop(); path.push(if cfg!(windows) {
6342 "fake-aft-stub.exe"
6343 } else {
6344 "fake-aft-stub"
6345 });
6346 assert!(
6347 path.exists(),
6348 "fake-aft-stub not built at {}: run `cargo test -p subc-core` (which builds \
6349 [[bin]] targets) rather than `cargo test -p subc-core --lib` (which does not)",
6350 path.display()
6351 );
6352 path
6353 }
6354
6355 fn client_retries(code: &str) -> bool {
6360 subc_protocol::error_codes::is_retryable_route_open(code)
6361 }
6362
6363 #[test]
6374 fn retryability_of_forwarding_codes_matches_the_failure() {
6375 let transient = [
6378 ForwardingError::NoModuleConnection,
6379 ForwardingError::ModuleReloading {
6380 module_id: "m".into(),
6381 },
6382 ForwardingError::StaleModuleEndpoint,
6383 ForwardingError::UnknownReservation {
6384 client_channel: 1,
6385 module_channel: 1,
6386 },
6387 ForwardingError::ConnectionClosing {
6388 connection_id: ConnectionId::new(1),
6389 },
6390 ForwardingError::ClientEgressClosed {
6391 connection_id: ConnectionId::new(1),
6392 },
6393 ForwardingError::ModuleEgressUnavailable {
6394 connection_id: ConnectionId::new(1),
6395 },
6396 ];
6397 for err in transient {
6398 let code = forwarding_error_code(&err);
6399 assert!(
6400 client_retries(code),
6401 "{err:?} is transient but publishes {code:?}, which clients treat as permanent"
6402 );
6403 }
6404
6405 let permanent = [
6410 ForwardingError::ConnectionRoleConflict {
6411 connection_id: ConnectionId::new(1),
6412 },
6413 ForwardingError::ClientRouteChannelExhausted {
6414 connection_id: ConnectionId::new(1),
6415 },
6416 ForwardingError::ModuleRouteChannelExhausted {
6417 endpoint: ModuleEndpointId {
6418 connection_id: ConnectionId::new(1),
6419 generation: 1,
6420 },
6421 },
6422 ForwardingError::RelayCorrelationExhausted,
6423 ForwardingError::RouteOpenBuild("x".into()),
6424 ForwardingError::Poisoned,
6425 ];
6426 for err in permanent {
6427 let code = forwarding_error_code(&err);
6428 assert!(
6429 !client_retries(code),
6430 "{err:?} cannot be fixed by retrying but publishes {code:?}, which clients retry"
6431 );
6432 }
6433 }
6434
6435 #[tokio::test]
6463 async fn an_unattested_caller_is_never_stamped_as_a_supervised_module() {
6464 let handler = ControlHandler::default();
6465 let frame =
6466 Frame::build(FrameType::Request, control_flags(), 0, 0, 900, Vec::new()).unwrap();
6467
6468 let stamped = handler.route_open_principal(&frame, None).unwrap().unwrap();
6472 assert_eq!(
6473 stamped,
6474 Principal::Direct,
6475 "a caller that proved nothing must not be stamped as a supervised module"
6476 );
6477
6478 let forged = handler
6483 .route_open_principal(
6484 &frame,
6485 Some(ConsumerIdentity {
6486 module_id: "aft".to_string(),
6487 launch_nonce: "not-a-real-nonce".to_string(),
6488 }),
6489 )
6490 .unwrap();
6491 let refusal = forged.expect_err("an unmatched launch nonce must not yield a principal");
6492 assert_eq!(parse_error(&refusal)["code"], "bad_consumer_identity");
6493 }
6494
6495 #[test]
6512 fn a_wire_body_actually_yields_the_consumer_identity_the_daemon_stamps_from() {
6513 let body = br#"{"op":"route.open","target":{"kind":"tool_provider","module_id":"m"},"identity":{"session":"s","project_root":"/p","harness":"h"},"consumer_identity":{"module_id":"aft","launch_nonce":"n"}}"#;
6514 let parsed: ClientControlRequest = serde_json::from_slice(body).unwrap();
6515 let ClientControlRequest::RouteOpen {
6516 consumer_identity, ..
6517 } = parsed
6518 else {
6519 panic!("route.open body must parse as RouteOpen");
6520 };
6521 assert_eq!(
6522 consumer_identity,
6523 Some(ConsumerIdentity {
6524 module_id: "aft".to_string(),
6525 launch_nonce: "n".to_string(),
6526 }),
6527 "the wire field name must reach the value route_open_principal reads"
6528 );
6529 }
6530
6531 fn manifest(module_id: &str, protocol_ver: u8) -> ModuleManifest {
6532 ModuleManifest::builder(module_id, "0.1.0")
6533 .protocol_ver(protocol_ver)
6534 .provides(vec![ProviderRole::ToolProvider {
6535 tools: vec![Tool {
6536 name: "read".to_string(),
6537 description: None,
6538 execution_mode: ExecutionMode::Pure,
6539 schema: json!({"type": "object"}),
6540 }],
6541 identity_scope: vec![IdentityScope::Project, IdentityScope::Session],
6542 concurrency: Concurrency::ModuleManaged,
6543 emits_push: true,
6544 sub_supervises: true,
6545 }])
6546 .build()
6547 }
6548
6549 fn hello_frame(module_id: &str, protocol_ver: u8, corr: u64) -> Frame {
6550 hello_frame_with_control_ops(module_id, protocol_ver, corr, None)
6551 }
6552
6553 fn hello_frame_with_control_ops(
6554 module_id: &str,
6555 protocol_ver: u8,
6556 corr: u64,
6557 control_ops: Option<Vec<String>>,
6558 ) -> Frame {
6559 hello_frame_full(module_id, protocol_ver, corr, control_ops, None)
6560 }
6561
6562 fn hello_frame_with_nonce(
6563 module_id: &str,
6564 protocol_ver: u8,
6565 corr: u64,
6566 launch_nonce: Option<&str>,
6567 ) -> Frame {
6568 hello_frame_full(
6569 module_id,
6570 protocol_ver,
6571 corr,
6572 None,
6573 launch_nonce.map(ToOwned::to_owned),
6574 )
6575 }
6576
6577 fn hello_frame_full(
6578 module_id: &str,
6579 protocol_ver: u8,
6580 corr: u64,
6581 control_ops: Option<Vec<String>>,
6582 launch_nonce: Option<String>,
6583 ) -> Frame {
6584 let body = serde_json::to_vec(&ModuleHelloBody {
6585 manifest: manifest(module_id, protocol_ver),
6586 protocol_ver,
6587 control_ops,
6588 launch_nonce,
6589 })
6590 .unwrap();
6591 Frame::build(FrameType::Hello, control_flags(), 0, 0, corr, body).unwrap()
6592 }
6593
6594 fn non_routable_hello_frame_with_control_ops(
6595 module_id: &str,
6596 corr: u64,
6597 control_ops: Option<Vec<String>>,
6598 ) -> Frame {
6599 let mut manifest = manifest(module_id, PROTOCOL_VERSION);
6600 manifest.provides.clear();
6601 let body = serde_json::to_vec(&ModuleHelloBody {
6602 manifest,
6603 protocol_ver: PROTOCOL_VERSION,
6604 control_ops,
6605 launch_nonce: None,
6606 })
6607 .unwrap();
6608 Frame::build(FrameType::Hello, control_flags(), 0, 0, corr, body).unwrap()
6609 }
6610
6611 fn capability_grammar_hello_frame(
6612 capabilities: Value,
6613 runtime_computed: Option<Value>,
6614 corr: u64,
6615 ) -> Frame {
6616 let mut body = serde_json::to_value(ModuleHelloBody {
6617 manifest: manifest("capability-grammar-test", PROTOCOL_VERSION),
6618 protocol_ver: PROTOCOL_VERSION,
6619 control_ops: None,
6620 launch_nonce: None,
6621 })
6622 .expect("HELLO body serializes");
6623 body["manifest"]["capabilities"] = capabilities;
6624 if let Some(runtime_computed) = runtime_computed {
6625 body["runtime_computed"] = runtime_computed;
6626 }
6627 Frame::build(
6628 FrameType::Hello,
6629 control_flags(),
6630 0,
6631 0,
6632 corr,
6633 serde_json::to_vec(&body).expect("HELLO body reserializes"),
6634 )
6635 .expect("HELLO frame builds")
6636 }
6637
6638 fn channel_request(channel: u16, corr: u64) -> Frame {
6639 Frame::build(
6640 FrameType::Request,
6641 Flags::new(true, Priority::Interactive, false),
6642 channel,
6643 0,
6644 corr,
6645 b"opaque".to_vec(),
6646 )
6647 .unwrap()
6648 }
6649
6650 fn route_ctx(
6651 connection_id: ConnectionId,
6652 ) -> (RouteCtx, mpsc::Receiver<crate::router::OutboundFrame>) {
6653 let (tx, rx) = mpsc::channel(8);
6654 (
6655 RouteCtx {
6656 connection_id,
6657 egress: FrameSink::new(tx),
6658 },
6659 rx,
6660 )
6661 }
6662
6663 fn parse_ack(frame: &Frame) -> ModuleHelloAckBody {
6664 serde_json::from_slice(&frame.body).unwrap()
6665 }
6666
6667 async fn hello_via_sink(
6672 handler: &ControlHandler,
6673 ctx: &RouteCtx,
6674 rx: &mut mpsc::Receiver<crate::router::OutboundFrame>,
6675 hello: Frame,
6676 ) -> Frame {
6677 let replies = handler.handle_control_frame(ctx, hello).await.unwrap();
6678 assert!(
6679 replies.is_empty(),
6680 "a registered HELLO replies with nothing; its ack is already queued: {replies:?}"
6681 );
6682 let ack = rx
6683 .try_recv()
6684 .expect("HELLO_ACK is queued on the module sink")
6685 .frame;
6686 assert_eq!(ack.header.ty, FrameType::HelloAck);
6687 ack
6688 }
6689
6690 fn parse_error(frame: &Frame) -> Value {
6691 serde_json::from_slice(&frame.body).unwrap()
6692 }
6693
6694 fn parse_route_poll(frame: &Frame) -> ClientControlResponse {
6695 serde_json::from_slice(&frame.body).unwrap()
6696 }
6697
6698 fn route_poll_frame(corr: u64, kind: PollKind, route_channel: u16) -> Frame {
6699 let body = serde_json::to_vec(&ClientControlRequest::RoutePoll {
6700 route_channel,
6701 route_epoch: 0,
6702 kind,
6703 })
6704 .unwrap();
6705 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
6706 }
6707
6708 fn supervisor_health_probe_frame(corr: u64, module_id: &str) -> Frame {
6709 let body = serde_json::to_vec(&ClientControlRequest::SupervisorHealthProbe {
6710 module_id: module_id.to_string(),
6711 })
6712 .unwrap();
6713 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
6714 }
6715
6716 fn route_open_frame(corr: u64, module_id: &str, project_root: TestTempDir) -> Frame {
6717 route_open_frame_with_consumer_capabilities(corr, module_id, project_root, None)
6718 }
6719
6720 fn route_open_frame_with_consumer_capabilities(
6721 corr: u64,
6722 module_id: &str,
6723 project_root: TestTempDir,
6724 consumer_capabilities: Option<Vec<String>>,
6725 ) -> Frame {
6726 let body = serde_json::to_vec(&ClientControlRequest::RouteOpen {
6727 target: RouteTarget::ToolProvider {
6728 module_id: module_id.to_string(),
6729 },
6730 identity: BindIdentity::new(
6731 project_root.path().to_path_buf(),
6732 "unit".to_string(),
6733 "session".to_string(),
6734 ),
6735 consumer_identity: None,
6736 consumer_capabilities,
6737 role_versions: None,
6738 admission_facts: None,
6739 scope: None,
6740 })
6741 .unwrap();
6742 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
6743 }
6744
6745 fn route_open_frame_with_role_versions(
6746 corr: u64,
6747 module_id: &str,
6748 project_root: TestTempDir,
6749 role_versions: Option<BTreeMap<String, String>>,
6750 ) -> Frame {
6751 let body = serde_json::to_vec(&ClientControlRequest::RouteOpen {
6752 target: RouteTarget::ToolProvider {
6753 module_id: module_id.to_string(),
6754 },
6755 identity: BindIdentity::new(
6756 project_root.path().to_path_buf(),
6757 "unit".to_string(),
6758 format!("session-{corr}"),
6759 ),
6760 consumer_identity: None,
6761 consumer_capabilities: None,
6762 role_versions,
6763 admission_facts: None,
6764 scope: None,
6765 })
6766 .unwrap();
6767 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
6768 }
6769
6770 fn role_versions(entries: &[(&str, &str)]) -> BTreeMap<String, String> {
6771 entries
6772 .iter()
6773 .map(|(role, version)| (role.to_string(), version.to_string()))
6774 .collect()
6775 }
6776
6777 fn route_open_frame_with_admission_facts(
6778 corr: u64,
6779 module_id: &str,
6780 project_root: TestTempDir,
6781 consumer_identity: Option<subc_control::ConsumerIdentity>,
6782 facts: Option<Value>,
6783 ) -> Frame {
6784 let body = serde_json::to_vec(&ClientControlRequest::RouteOpen {
6785 target: RouteTarget::ToolProvider {
6786 module_id: module_id.to_string(),
6787 },
6788 identity: BindIdentity::new(
6789 project_root.path().to_path_buf(),
6790 "unit".to_string(),
6791 format!("session-{corr}"),
6792 ),
6793 consumer_identity,
6794 consumer_capabilities: None,
6795 role_versions: None,
6796 admission_facts: facts,
6797 scope: None,
6798 })
6799 .unwrap();
6800 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
6801 }
6802
6803 #[derive(Clone, Default)]
6804 struct EventCapture {
6805 events: Arc<Mutex<Vec<CapturedEvent>>>,
6806 }
6807
6808 #[derive(Clone, Debug)]
6809 struct CapturedEvent {
6810 target: String,
6811 level: tracing::Level,
6812 fields: BTreeMap<String, String>,
6813 }
6814
6815 impl EventCapture {
6816 fn events(&self) -> Vec<CapturedEvent> {
6817 self.events.lock().unwrap().clone()
6818 }
6819 }
6820
6821 impl<S> Layer<S> for EventCapture
6822 where
6823 S: Subscriber,
6824 {
6825 fn on_event(&self, event: &Event<'_>, _context: Context<'_, S>) {
6826 let mut visitor = EventFieldVisitor::default();
6827 event.record(&mut visitor);
6828 self.events.lock().unwrap().push(CapturedEvent {
6829 target: event.metadata().target().to_string(),
6830 level: *event.metadata().level(),
6831 fields: visitor.fields,
6832 });
6833 }
6834 }
6835
6836 #[derive(Default)]
6837 struct EventFieldVisitor {
6838 fields: BTreeMap<String, String>,
6839 }
6840
6841 impl Visit for EventFieldVisitor {
6842 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
6843 self.fields
6844 .insert(field.name().to_string(), format!("{value:?}"));
6845 }
6846 }
6847
6848 fn health_response(corr: u64, status: HealthStatus) -> Frame {
6849 let body = serde_json::to_vec(&ModuleControlResponse::HealthCheck {
6850 status,
6851 detail: Some("warming".to_string()),
6852 metrics: Some(json!({"queue_depth": 3})),
6853 })
6854 .unwrap();
6855 Frame::build(FrameType::Response, control_flags(), 0, 0, corr, body).unwrap()
6856 }
6857
6858 fn route_bind_ack(corr: u64) -> Frame {
6859 let body = serde_json::to_vec(&ModuleControlResponse::RouteBindAck {}).unwrap();
6860 Frame::build(FrameType::Response, control_flags(), 0, 0, corr, body).unwrap()
6861 }
6862
6863 fn unique_project_root(label: &str) -> TestTempDir {
6864 TestTempDir::new(label)
6865 }
6866
6867 fn assert_route_poll_liveness(frame: &Frame, expected_live: bool) {
6868 match parse_route_poll(frame) {
6869 ClientControlResponse::RoutePoll {
6870 status: None,
6871 live: Some(live),
6872 ..
6873 } => assert_eq!(live, expected_live),
6874 other => panic!("unexpected route.poll response: {other:?}"),
6875 }
6876 }
6877
6878 fn bind_liveness_route(
6879 registry: &Registry,
6880 forwarding: &ForwardingTable,
6881 module_id: &str,
6882 ) -> (RouteCtx, u16, u32) {
6883 let module_connection = ConnectionId::new(101);
6884 let client_connection = ConnectionId::new(202);
6885 let registration = registry
6886 .register_with_control_ops(
6887 manifest(module_id, PROTOCOL_VERSION),
6888 PROTOCOL_VERSION,
6889 module_connection,
6890 module_baseline_control_ops(),
6891 )
6892 .unwrap();
6893 let (module_tx, _module_rx) = mpsc::channel(8);
6894 let endpoint = forwarding
6895 .register_module_connection(
6896 module_connection,
6897 module_id.to_string(),
6898 PROTOCOL_VERSION,
6899 manifest_concurrency(®istration.manifest),
6900 FrameSink::new(module_tx),
6901 )
6902 .unwrap();
6903 let (client_ctx, _client_rx) = route_ctx(client_connection);
6904 let pending = forwarding
6905 .begin_route_bind_relay_for_test(
6906 client_connection,
6907 client_ctx.egress.clone(),
6908 1,
6909 module_id,
6910 )
6911 .unwrap();
6912 assert_eq!(pending.endpoint, endpoint);
6913 let route_channel = pending.client_channel;
6914 let route_epoch = pending.client_epoch;
6915 forwarding
6916 .complete_pending_relay(
6917 module_connection,
6918 pending.corr,
6919 RouteBindRelayOutcome::Accepted,
6920 )
6921 .unwrap();
6922 (client_ctx, route_channel, route_epoch)
6923 }
6924
6925 struct FakeProcessLiveness {
6926 live: Option<bool>,
6927 }
6928
6929 impl ModuleProcessLiveness for FakeProcessLiveness {
6930 fn process_live(&self, _module_id: &str) -> Option<bool> {
6931 self.live
6932 }
6933 }
6934
6935 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6936 async fn supervisor_stderr_tail_converts_a_real_truncated_ring_entry_to_prefix_only_wire_data()
6937 {
6938 let registry = Arc::new(Registry::default());
6939 let supervisor_handle = SupervisorHandle::new();
6940 let supervisor = Supervisor::new_for_test(
6941 Arc::clone(®istry),
6942 RestartPolicy::new(1, Duration::from_millis(10)),
6943 )
6944 .with_handle(supervisor_handle.clone());
6945 let source_line = format!("config error: {}", "x".repeat(DEFAULT_MAX_LINE_BYTES));
6946 let module = supervisor
6947 .spawn(ModuleSpec {
6948 module_id: "stderr-tail-wire".to_string(),
6949 program: fake_aft_stub_path(),
6950 args: Vec::new(),
6951 env: vec![
6952 ("FAKE_AFT_STDERR_LINE".to_string(), source_line.clone()),
6953 ("FAKE_AFT_EXIT_CODE".to_string(), "1".to_string()),
6954 ],
6955 reserved: false,
6956 reserved_prefixes: Vec::new(),
6957 protocol: ModuleProtocol::Subc,
6958 overlap: Default::default(),
6959 })
6960 .unwrap();
6961
6962 let deadline = Instant::now() + Duration::from_secs(5);
6963 loop {
6964 let tail = module.stderr_tail(None, None);
6965 if tail
6966 .entries
6967 .iter()
6968 .any(|entry| matches!(entry, TailEntry::ProcessStart))
6969 && tail.entries.iter().any(|entry| {
6970 matches!(
6971 entry,
6972 TailEntry::Line {
6973 truncated: true,
6974 ..
6975 }
6976 )
6977 })
6978 {
6979 break;
6980 }
6981 assert!(
6982 Instant::now() < deadline,
6983 "module did not produce a truncated line and restart boundary: {tail:?}"
6984 );
6985 sleep(Duration::from_millis(10)).await;
6986 }
6987
6988 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
6989 let request = ClientControlRequest::SupervisorStderrTail {
6990 module_id: "stderr-tail-wire".to_string(),
6991 max_lines: None,
6992 max_bytes: None,
6993 };
6994 let frame = Frame::build(
6995 FrameType::Request,
6996 control_flags(),
6997 0,
6998 0,
6999 1,
7000 serde_json::to_vec(&request).unwrap(),
7001 )
7002 .unwrap();
7003 let (ctx, _egress) = route_ctx(ConnectionId::new(1));
7004 let responses = handler.handle_control_frame(&ctx, frame).await.unwrap();
7005 let ClientControlResponse::SupervisorStderrTail { tail, .. } =
7006 serde_json::from_slice(&responses[0].body).unwrap()
7007 else {
7008 panic!("expected supervisor.stderr_tail response");
7009 };
7010
7011 assert!(
7012 tail.entries
7013 .iter()
7014 .any(|entry| matches!(entry, StderrTailEntry::ProcessStart)),
7015 "the control response lost the restart boundary"
7016 );
7017 let Some(StderrTailEntry::Line {
7018 text,
7019 truncated,
7020 at_ms,
7021 }) = tail.entries.iter().find(|entry| {
7022 matches!(
7023 entry,
7024 StderrTailEntry::Line {
7025 truncated: true,
7026 ..
7027 }
7028 )
7029 })
7030 else {
7031 panic!("the control response lost the truncated line");
7032 };
7033 assert_eq!(text, &source_line[..DEFAULT_MAX_LINE_BYTES]);
7034 assert!(*truncated);
7035 assert!(
7036 at_ms.is_some(),
7037 "the control response lost the line's capture time"
7038 );
7039 }
7040
7041 #[tokio::test(flavor = "current_thread")]
7046 async fn supervisor_terminals_reads_the_journal_off_the_runtime_worker() {
7047 let dir = TestTempDir::new("terminals-off-worker");
7048 let journal_path = dir.join("terminals.jsonl");
7049 let registry = Arc::new(Registry::default());
7050 let supervisor_handle = SupervisorHandle::new();
7051 let supervisor =
7052 Supervisor::new_for_test(Arc::clone(®istry), RestartPolicy::new(1, Duration::ZERO))
7053 .with_handle(supervisor_handle.clone())
7054 .with_terminal_journal(journal_path.clone(), "off-worker-daemon".to_string());
7055 let module = supervisor
7056 .spawn(ModuleSpec {
7057 module_id: "terminal-off-worker".to_string(),
7058 program: fake_aft_stub_path(),
7059 args: Vec::new(),
7060 env: vec![("FAKE_AFT_EXIT_CODE".to_string(), "23".to_string())],
7061 reserved: false,
7062 reserved_prefixes: Vec::new(),
7063 protocol: ModuleProtocol::Subc,
7064 overlap: Default::default(),
7065 })
7066 .unwrap();
7067 let deadline = Instant::now() + Duration::from_secs(5);
7068 while module.terminal_history().entries.len() != 2 {
7069 assert!(Instant::now() < deadline, "module did not record two exits");
7070 sleep(Duration::from_millis(10)).await;
7071 }
7072
7073 let (started, release) = crate::terminal_journal::read_pause::install(&journal_path);
7074 let handler =
7075 Arc::new(ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle));
7076 let frame = Frame::build(
7077 FrameType::Request,
7078 control_flags(),
7079 0,
7080 0,
7081 1,
7082 serde_json::to_vec(&ClientControlRequest::SupervisorTerminals {
7083 module_id: "terminal-off-worker".to_string(),
7084 })
7085 .unwrap(),
7086 )
7087 .unwrap();
7088 let (ctx, _egress) = route_ctx(ConnectionId::new(1));
7089 let spawned_at = std::time::Instant::now();
7090 let read = tokio::spawn({
7091 let handler = Arc::clone(&handler);
7092 async move { handler.handle_control_frame(&ctx, frame).await }
7093 });
7094 tokio::task::spawn_blocking(move || started.recv_timeout(Duration::from_secs(5)))
7097 .await
7098 .unwrap()
7099 .expect("the history read reached its pause");
7100 let elapsed = spawned_at.elapsed();
7101 assert!(
7102 elapsed < Duration::from_secs(2) && !read.is_finished(),
7103 "this task could not run while the history read was paused \
7104 (resumed after {elapsed:?}, read finished: {})",
7105 read.is_finished()
7106 );
7107
7108 drop(release);
7109 let responses = read.await.unwrap().unwrap();
7110 let response: ClientControlResponse = serde_json::from_slice(&responses[0].body).unwrap();
7111 let ClientControlResponse::SupervisorTerminals { terminals, .. } = response else {
7112 panic!("expected supervisor.terminals response");
7113 };
7114 assert_eq!(terminals.entries.len(), 2);
7115 assert_eq!(terminals.journal_skipped_lines, 0);
7116 assert_eq!(terminals.journal_read_errors, 0);
7117 }
7118
7119 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
7120 async fn supervisor_terminals_golden_is_generated_through_the_real_handler() {
7121 let registry = Arc::new(Registry::default());
7122 let supervisor_handle = SupervisorHandle::new();
7123 let supervisor =
7124 Supervisor::new_for_test(Arc::clone(®istry), RestartPolicy::new(1, Duration::ZERO))
7125 .with_handle(supervisor_handle.clone());
7126 let module = supervisor
7127 .spawn(ModuleSpec {
7128 module_id: "terminal-golden".to_string(),
7129 program: fake_aft_stub_path(),
7130 args: Vec::new(),
7131 env: vec![("FAKE_AFT_EXIT_CODE".to_string(), "23".to_string())],
7132 reserved: false,
7133 reserved_prefixes: Vec::new(),
7134 protocol: ModuleProtocol::Subc,
7135 overlap: Default::default(),
7136 })
7137 .unwrap();
7138
7139 let deadline = Instant::now() + Duration::from_secs(5);
7140 while module.terminal_history().entries.len() != 2 {
7141 assert!(
7142 Instant::now() < deadline,
7143 "module did not retain two terminal exits: {:?}",
7144 module.terminal_history()
7145 );
7146 sleep(Duration::from_millis(10)).await;
7147 }
7148
7149 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
7150 let request = ClientControlRequest::SupervisorTerminals {
7151 module_id: "terminal-golden".to_string(),
7152 };
7153 let frame = Frame::build(
7154 FrameType::Request,
7155 control_flags(),
7156 0,
7157 0,
7158 1,
7159 serde_json::to_vec(&request).unwrap(),
7160 )
7161 .unwrap();
7162 let (ctx, _egress) = route_ctx(ConnectionId::new(1));
7163 let responses = handler.handle_control_frame(&ctx, frame).await.unwrap();
7164 let response: ClientControlResponse = serde_json::from_slice(&responses[0].body).unwrap();
7165 let ClientControlResponse::SupervisorTerminals { terminals, .. } = &response else {
7166 panic!("expected supervisor.terminals response");
7167 };
7168 assert_eq!(terminals.entries.len(), 2);
7169 assert_eq!(terminals.dropped, 0);
7170
7171 let mut rendered = serde_json::to_value(response).unwrap();
7172 rendered["daemon_started_at_ms"] = json!(1_700_000_000_000u64);
7175 for (index, entry) in rendered["entries"]
7176 .as_array_mut()
7177 .expect("terminal response entries array")
7178 .iter_mut()
7179 .enumerate()
7180 {
7181 entry["at_ms"] = json!(1_700_000_000_001u64 + index as u64);
7182 }
7183
7184 let golden_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7185 .join("../subc-control/tests/golden/client_control_response_supervisor_terminals.json");
7186 let serialized = serde_json::to_string_pretty(&rendered).unwrap() + "\n";
7187 if std::env::var_os("UPDATE_GOLDEN").is_some() {
7188 std::fs::write(&golden_path, &serialized).unwrap();
7189 }
7190 let expected: Value =
7191 serde_json::from_str(&std::fs::read_to_string(&golden_path).unwrap()).unwrap();
7192 assert_eq!(rendered, expected);
7193 }
7194
7195 #[test]
7196 fn hello_registers_manifest_and_returns_ack() {
7197 let registry = Arc::new(Registry::default());
7198 let handler = ControlHandler::new(Arc::clone(®istry));
7199 let conn = ConnectionId::new(1);
7200
7201 let responses = handler
7202 .handle_control(conn, hello_frame("aft", PROTOCOL_VERSION, 7))
7203 .unwrap();
7204
7205 assert_eq!(responses.len(), 1);
7206 assert_eq!(responses[0].header.ty, FrameType::HelloAck);
7207 assert_eq!(responses[0].header.channel, 0);
7208 assert_eq!(responses[0].header.corr, 7);
7209 let ack = parse_ack(&responses[0]);
7210 assert_eq!(ack.negotiated_ver, PROTOCOL_VERSION);
7211 assert!(ack
7212 .subc_capabilities
7213 .contains(&CAP_MANIFEST_REGISTRATION.to_string()));
7214 assert!(ack.subc_ops.contains(&ops::SUPERVISOR_LIST.to_string()));
7215 assert!(ack.subc_ops.contains(&ops::SUPERVISOR_RESTART.to_string()));
7216 assert!(ack
7217 .subc_ops
7218 .contains(&ops::SUPERVISOR_SET_ENABLED.to_string()));
7219 assert!(ack
7220 .subc_ops
7221 .contains(&MODULE_TO_SUBC_OP_CATALOG_UPDATE.to_string()));
7222
7223 let registration = registry.get_module("aft").unwrap().unwrap();
7224 assert_eq!(registration.negotiated_ver, PROTOCOL_VERSION);
7225 assert_eq!(registration.state, ChannelState::Active);
7226 assert_eq!(registration.connection_id, conn);
7227 assert_eq!(registration.control_ops, module_baseline_control_ops());
7228 }
7229
7230 #[test]
7231 fn capability_grammar_refusals_name_the_field_and_leave_no_catalog_entry() {
7232 let invalid_identifiers = [
7233 ("case_change", "credentials-Provider/v1"),
7234 ("leading_zero", "credentials-provider/v01"),
7235 ("trailing_hyphen", "credentials-provider-/v1"),
7236 ("consecutive_hyphens", "credentials--provider/v1"),
7237 ("uppercase", "Credentials-provider/v1"),
7238 ("missing_v", "credentials-provider/1"),
7239 ("whitespace", "credentials provider/v1"),
7240 ("zero_version", "credentials-provider/v0"),
7241 ("out_of_range_version", "credentials-provider/v4294967296"),
7242 (
7243 "overlength_name",
7244 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/v1",
7245 ),
7246 ];
7247 let mut cases = invalid_identifiers
7248 .into_iter()
7249 .map(|(name, identifier)| {
7250 (
7251 format!("identifier_{name}"),
7252 "capabilities.provides[0]".to_string(),
7253 identifier.to_string(),
7254 json!({ "provides": [identifier] }),
7255 None,
7256 )
7257 })
7258 .collect::<Vec<_>>();
7259 cases.extend([
7260 (
7261 "unknown_need".to_string(),
7262 "capabilities.requires[0].need".to_string(),
7263 "deferred".to_string(),
7264 json!({ "requires": [{ "capability": "credentials-provider/v1", "need": "deferred" }] }),
7265 None,
7266 ),
7267 (
7268 "duplicate_provides".to_string(),
7269 "capabilities.provides[1]".to_string(),
7270 "credentials-provider/v1".to_string(),
7271 json!({ "provides": ["credentials-provider/v1", "credentials-provider/v1"] }),
7272 None,
7273 ),
7274 (
7275 "duplicate_must_never_reach".to_string(),
7276 "capabilities.must_never_reach[1]".to_string(),
7277 "credentials-provider/v1".to_string(),
7278 json!({ "must_never_reach": ["credentials-provider/v1", "credentials-provider/v1"] }),
7279 None,
7280 ),
7281 (
7282 "duplicate_requires_same_need".to_string(),
7283 "capabilities.requires[1]".to_string(),
7284 "credentials-provider/v1".to_string(),
7285 json!({ "requires": [
7286 { "capability": "credentials-provider/v1", "need": "required" },
7287 { "capability": "credentials-provider/v1", "need": "required" }
7288 ] }),
7289 None,
7290 ),
7291 (
7292 "duplicate_requires_conflicting_need".to_string(),
7293 "capabilities.requires[1]".to_string(),
7294 "credentials-provider/v1".to_string(),
7295 json!({ "requires": [
7296 { "capability": "credentials-provider/v1", "need": "required" },
7297 { "capability": "credentials-provider/v1", "need": "optional" }
7298 ] }),
7299 None,
7300 ),
7301 (
7302 "capabilities_root_pointer".to_string(),
7303 "runtime_computed[0]".to_string(),
7304 "/capabilities".to_string(),
7305 json!({}),
7306 Some(json!(["/capabilities"])),
7307 ),
7308 (
7309 "capabilities_descendant_pointer".to_string(),
7310 "runtime_computed[0]".to_string(),
7311 "/capabilities/provides".to_string(),
7312 json!({}),
7313 Some(json!(["/capabilities/provides"])),
7314 ),
7315 (
7316 "malformed_pointer_without_leading_slash".to_string(),
7317 "runtime_computed[0]".to_string(),
7318 "capabilities".to_string(),
7319 json!({}),
7320 Some(json!(["capabilities"])),
7321 ),
7322 (
7323 "malformed_pointer_escape".to_string(),
7324 "runtime_computed[0]".to_string(),
7325 "/roles/~2/tools".to_string(),
7326 json!({}),
7327 Some(json!(["/roles/~2/tools"])),
7328 ),
7329 (
7330 "unknown_capabilities_field".to_string(),
7331 "capabilities.future".to_string(),
7332 "<array>".to_string(),
7333 json!({ "future": [] }),
7334 None,
7335 ),
7336 ]);
7337
7338 for (index, (name, field, value, capabilities, runtime_computed)) in
7339 cases.into_iter().enumerate()
7340 {
7341 let registry = Arc::new(Registry::default());
7342 let handler = ControlHandler::new(Arc::clone(®istry));
7343 let response = handler
7344 .handle_control(
7345 ConnectionId::new((index + 1) as u64),
7346 capability_grammar_hello_frame(
7347 capabilities,
7348 runtime_computed,
7349 index as u64 + 1,
7350 ),
7351 )
7352 .expect("invalid HELLO returns a refusal");
7353
7354 assert_eq!(response.len(), 1, "{name} must emit one refusal");
7355 let error = parse_error(&response[0]);
7356 assert_eq!(error["code"], "invalid_capability_grammar", "{name}");
7357 let message = error["message"]
7358 .as_str()
7359 .expect("error message is a string");
7360 assert!(
7361 message.contains(&field),
7362 "{name}: field missing from {message}"
7363 );
7364 assert!(
7365 message.contains(&value),
7366 "{name}: value missing from {message}"
7367 );
7368 assert_eq!(
7369 registry
7370 .active_registration_count()
7371 .expect("registry reads"),
7372 0,
7373 "{name}: refused HELLO must not create a catalog entry"
7374 );
7375 }
7376 }
7377
7378 #[test]
7379 fn legal_runtime_pointer_and_capabilities_are_mirrored_in_catalog_list() {
7380 let registry = Arc::new(Registry::default());
7381 let handler = ControlHandler::new(Arc::clone(®istry));
7382 let capabilities = json!({
7383 "provides": ["credentials-provider/v1"],
7384 "requires": [{ "capability": "context-transform/v1", "need": "optional" }],
7385 "must_never_reach": ["federation-transport/v1"]
7386 });
7387 let response = handler
7388 .handle_control(
7389 ConnectionId::new(99),
7390 capability_grammar_hello_frame(
7391 capabilities.clone(),
7392 Some(json!(["/roles/0/tools"])),
7393 99,
7394 ),
7395 )
7396 .expect("valid HELLO registers");
7397 assert_eq!(response[0].header.ty, FrameType::HelloAck);
7398
7399 let request = Frame::build(
7400 FrameType::Request,
7401 control_flags(),
7402 0,
7403 0,
7404 100,
7405 serde_json::to_vec(&ClientControlRequest::CatalogList { module_id: None })
7406 .expect("catalog request serializes"),
7407 )
7408 .expect("catalog request frame builds");
7409 let response = handler
7410 .handle_catalog_list(request, None)
7411 .expect("catalog list succeeds");
7412 let ClientControlResponse::CatalogList { modules, .. } =
7413 serde_json::from_slice(&response[0].body).expect("catalog response decodes")
7414 else {
7415 panic!("catalog request must return catalog.list");
7416 };
7417 assert_eq!(modules.len(), 1);
7418 assert_eq!(
7419 serde_json::to_value(&modules[0].capabilities).expect("catalog capabilities serialize"),
7420 capabilities
7421 );
7422 }
7423
7424 #[test]
7425 fn catalog_list_mirrors_management_operation_description() {
7426 let registry = Arc::new(Registry::default());
7427 let handler = ControlHandler::new(Arc::clone(®istry));
7428 let description = "List managed records and return their identifiers and metadata.";
7429 let mut manifest = manifest("described-management", PROTOCOL_VERSION);
7430 manifest.provides = vec![ProviderRole::ManagementSurface {
7431 operations: vec![ManagementOperation {
7432 name: "records.list".to_string(),
7433 kind: ManagementOperationKind::Query,
7434 description: Some(description.to_string()),
7435 }],
7436 config_schema: json!({"type": "object"}),
7437 observability: vec![ObservabilitySurface {
7438 name: "records.stats".to_string(),
7439 kind: ObservabilityKind::Snapshot,
7440 }],
7441 identity_scope: vec![IdentityScope::Project],
7442 concurrency: Concurrency::ModuleManaged,
7443 }];
7444 registry
7445 .register_with_control_ops(
7446 manifest,
7447 PROTOCOL_VERSION,
7448 ConnectionId::new(99),
7449 Vec::new(),
7450 )
7451 .expect("described management manifest registers");
7452
7453 let request = Frame::build(
7454 FrameType::Request,
7455 control_flags(),
7456 0,
7457 0,
7458 100,
7459 serde_json::to_vec(&ClientControlRequest::CatalogList { module_id: None })
7460 .expect("catalog request serializes"),
7461 )
7462 .expect("catalog request frame builds");
7463 let response = handler
7464 .handle_catalog_list(request, None)
7465 .expect("catalog list succeeds");
7466 let body: Value = serde_json::from_slice(&response[0].body).expect("catalog response JSON");
7467 assert_eq!(
7468 body["modules"][0]["roles"][0]["operations"][0]["description"], description,
7469 "catalog.list must preserve the declared operation description verbatim"
7470 );
7471 }
7472
7473 #[test]
7474 fn reserved_capability_refusal_mutation_proof_leaves_no_catalog_entry() {
7475 let registry = Arc::new(Registry::default());
7476 let handler = ControlHandler::new(Arc::clone(®istry)).with_capability_config(
7477 [("vault".to_string(), true), ("squatter".to_string(), true)],
7478 BTreeMap::from([("credentials-provider/v1".to_string(), "vault".to_string())]),
7479 );
7480 let mut squatter = manifest("squatter", PROTOCOL_VERSION);
7481 squatter.capabilities = Some(subc_protocol::manifest::CapabilityDeclarations {
7482 provides: vec!["credentials-provider/v1".to_string()],
7483 requires: Vec::new(),
7484 must_never_reach: Vec::new(),
7485 });
7486 let frame = Frame::build(
7487 FrameType::Hello,
7488 control_flags(),
7489 0,
7490 0,
7491 77,
7492 serde_json::to_vec(&ModuleHelloBody {
7493 manifest: squatter,
7494 protocol_ver: PROTOCOL_VERSION,
7495 control_ops: None,
7496 launch_nonce: None,
7497 })
7498 .expect("HELLO serializes"),
7499 )
7500 .expect("HELLO frame builds");
7501 let response = handler
7502 .handle_control(ConnectionId::new(77), frame)
7503 .expect("reserved claim receives a typed refusal");
7504 assert_eq!(parse_error(&response[0])["code"], "reserved_capability");
7505 assert_eq!(
7506 registry
7507 .active_registration_count()
7508 .expect("registry reads"),
7509 0,
7510 "a reserved capability refusal must not leave a catalog entry"
7511 );
7512 }
7513
7514 #[test]
7515 fn stale_relay_settlement_cannot_release_the_half_open_probe() {
7516 for settlement in ["timeout", "inconclusive", "drop"] {
7517 let breakers = RouteBindBreakers::default();
7518 let RouteBindAdmission::Admitted {
7519 guard: mut old,
7520 probe: false,
7521 } = breakers.admit("prov")
7522 else {
7523 panic!("ordinary relay admitted")
7524 };
7525 let RouteBindAdmission::Admitted {
7526 guard: mut opener, ..
7527 } = breakers.admit("prov")
7528 else {
7529 panic!("second relay admitted")
7530 };
7531 assert!(
7532 !opener
7533 .record_timeout(1, Duration::ZERO)
7534 .unwrap()
7535 .reopened_after_probe
7536 );
7537 let RouteBindAdmission::Admitted {
7538 guard: mut probe,
7539 probe: true,
7540 } = breakers.admit("prov")
7541 else {
7542 panic!("one cooldown probe admitted")
7543 };
7544 match settlement {
7545 "timeout" => assert!(
7546 !old.record_timeout(1, Duration::ZERO)
7547 .unwrap()
7548 .reopened_after_probe
7549 ),
7550 "inconclusive" => old.record_inconclusive(),
7551 "drop" => drop(old),
7552 _ => unreachable!(),
7553 }
7554 assert!(
7555 matches!(
7556 breakers.admit("prov"),
7557 RouteBindAdmission::Refused {
7558 probe_in_flight: true,
7559 ..
7560 }
7561 ),
7562 "{settlement} of a pre-open relay cannot release the real probe"
7563 );
7564 assert!(
7565 probe
7566 .record_timeout(1, Duration::ZERO)
7567 .unwrap()
7568 .reopened_after_probe
7569 );
7570 assert!(matches!(
7571 breakers.admit("prov"),
7572 RouteBindAdmission::Admitted { probe: true, .. }
7573 ));
7574 }
7575 let breakers = RouteBindBreakers::default();
7576 let admit = || match breakers.admit("prov") {
7577 RouteBindAdmission::Admitted { guard, .. } => guard,
7578 _ => panic!("relay admitted"),
7579 };
7580 admit().record_timeout(1, Duration::ZERO);
7581 let mut old_probe = admit();
7582 breakers.reset_for_new_module_connection("prov");
7583 admit().record_timeout(1, Duration::ZERO);
7584 let _new_probe = admit();
7585 old_probe.record_inconclusive();
7586 assert!(matches!(
7587 breakers.admit("prov"),
7588 RouteBindAdmission::Refused {
7589 probe_in_flight: true,
7590 ..
7591 }
7592 ));
7593 }
7594
7595 #[tokio::test]
7596 async fn catalog_update_refuses_reserved_capabilities_for_active_and_candidate() {
7597 for candidate in [false, true] {
7598 let registry = Arc::new(Registry::default());
7599 let handler = ControlHandler::new(Arc::clone(®istry)).with_capability_config(
7600 [("vault".to_string(), true), ("squatter".to_string(), true)],
7601 BTreeMap::from([("credentials-provider/v1".to_string(), "vault".to_string())]),
7602 );
7603 let conn = ConnectionId::new(77);
7604 let (ctx, mut rx) = route_ctx(conn);
7605 let initial = capability_manifest("squatter", &[], &[]);
7606 if candidate {
7607 registry
7608 .register_candidate_with_control_ops(
7609 initial.clone(),
7610 PROTOCOL_VERSION,
7611 conn,
7612 module_baseline_control_ops(),
7613 )
7614 .unwrap();
7615 handler
7616 .forwarding
7617 .register_candidate_module_connection(
7618 conn,
7619 "squatter".to_string(),
7620 PROTOCOL_VERSION,
7621 manifest_concurrency(&initial),
7622 ctx.egress.clone(),
7623 )
7624 .unwrap();
7625 } else {
7626 hello_via_sink(
7627 &handler,
7628 &ctx,
7629 &mut rx,
7630 hello_frame_with_manifest(initial.clone(), 1),
7631 )
7632 .await;
7633 }
7634 let response = handler
7635 .handle_control_frame(
7636 &ctx,
7637 catalog_update_with_capabilities_frame(
7638 2,
7639 capability_manifest("squatter", &["credentials-provider/v1"], &[])
7640 .capabilities
7641 .unwrap(),
7642 ),
7643 )
7644 .await
7645 .unwrap();
7646 assert_eq!(parse_error(&response[0])["code"], "reserved_capability");
7647 assert_eq!(
7648 registry
7649 .get_module_by_connection(conn)
7650 .unwrap()
7651 .unwrap()
7652 .manifest,
7653 initial
7654 );
7655 }
7656 }
7657
7658 #[test]
7659 fn server_describe_surfaces_required_capability_verdict_fields() {
7660 let registry = Arc::new(Registry::default());
7661 let handler = ControlHandler::new(Arc::clone(®istry)).with_capability_config(
7662 [
7663 ("consumer".to_string(), true),
7664 ("provider".to_string(), false),
7665 ],
7666 BTreeMap::new(),
7667 );
7668 let mut consumer = manifest("consumer", PROTOCOL_VERSION);
7669 consumer.capabilities = Some(subc_protocol::manifest::CapabilityDeclarations {
7670 provides: Vec::new(),
7671 requires: vec![subc_protocol::manifest::CapabilityRequirement {
7672 capability: "credentials-provider/v1".to_string(),
7673 need: subc_protocol::manifest::CapabilityNeed::Required,
7674 }],
7675 must_never_reach: Vec::new(),
7676 });
7677 let hello = Frame::build(
7678 FrameType::Hello,
7679 control_flags(),
7680 0,
7681 0,
7682 78,
7683 serde_json::to_vec(&ModuleHelloBody {
7684 manifest: consumer,
7685 protocol_ver: PROTOCOL_VERSION,
7686 control_ops: None,
7687 launch_nonce: None,
7688 })
7689 .expect("HELLO serializes"),
7690 )
7691 .expect("HELLO frame builds");
7692 handler
7693 .handle_control(ConnectionId::new(78), hello)
7694 .expect("consumer registers");
7695 let describe = Frame::build(
7696 FrameType::Request,
7697 control_flags(),
7698 0,
7699 0,
7700 79,
7701 serde_json::to_vec(&ClientControlRequest::ServerDescribe {})
7702 .expect("request serializes"),
7703 )
7704 .expect("describe frame builds");
7705 let response = handler
7706 .handle_server_describe(describe)
7707 .expect("server.describe succeeds");
7708 let rendered: Value = serde_json::from_slice(&response[0].body).expect("response JSON");
7709 let requirement = &rendered["capability_requirements"][0];
7710 assert_eq!(requirement["consumer"], "consumer");
7711 assert_eq!(requirement["verdict"], "never_provided");
7712 assert_eq!(requirement["episode_seq"], 1);
7713 assert_eq!(requirement["config_satisfiable"], false);
7714 assert_eq!(requirement["runtime_available"], false);
7715 assert!(requirement["detail"]
7716 .as_str()
7717 .expect("detail string")
7718 .contains("credentials-provider/v1"));
7719 }
7720
7721 #[test]
7722 fn catalog_list_omits_capabilities_for_legacy_manifest() {
7723 let registry = Arc::new(Registry::default());
7724 let handler = ControlHandler::new(Arc::clone(®istry));
7725 let hello = handler
7726 .handle_control(
7727 ConnectionId::new(101),
7728 hello_frame("legacy-capability-manifest", PROTOCOL_VERSION, 101),
7729 )
7730 .expect("legacy HELLO registers");
7731 assert_eq!(hello[0].header.ty, FrameType::HelloAck);
7732
7733 let request = Frame::build(
7734 FrameType::Request,
7735 control_flags(),
7736 0,
7737 0,
7738 102,
7739 serde_json::to_vec(&ClientControlRequest::CatalogList { module_id: None })
7740 .expect("catalog request serializes"),
7741 )
7742 .expect("catalog request frame builds");
7743 let response = handler
7744 .handle_catalog_list(request, None)
7745 .expect("catalog list succeeds");
7746 let body: Value = serde_json::from_slice(&response[0].body).expect("catalog response JSON");
7747 assert!(
7748 body["modules"][0].get("capabilities").is_none(),
7749 "legacy manifest must retain an absent capabilities field on catalog.list"
7750 );
7751 }
7752
7753 #[test]
7754 fn hello_ack_omits_storage_when_no_storage_config() {
7755 let registry = Arc::new(Registry::default());
7756 let handler = ControlHandler::new(Arc::clone(®istry));
7757 let responses = handler
7758 .handle_control(
7759 ConnectionId::new(1),
7760 hello_frame("aft", PROTOCOL_VERSION, 7),
7761 )
7762 .unwrap();
7763 let ack = parse_ack(&responses[0]);
7764 assert_eq!(ack.storage, None, "no storage config -> no descriptor");
7765 assert_eq!(ack.machine_id, None, "no machine id configured -> no field");
7766 }
7767
7768 #[tokio::test]
7769 async fn hello_ack_and_server_describe_carry_the_configured_machine_id() {
7770 let id = crate::machine_id::MachineId::parse("0123456789abcdef0123456789abcdef").unwrap();
7771 let registry = Arc::new(Registry::default());
7772 let handler = ControlHandler::new(Arc::clone(®istry)).with_machine_id(Some(id.clone()));
7773 let responses = handler
7774 .handle_control(
7775 ConnectionId::new(1),
7776 hello_frame("aft", PROTOCOL_VERSION, 7),
7777 )
7778 .unwrap();
7779 let ack = parse_ack(&responses[0]);
7780 assert_eq!(ack.machine_id.as_deref(), Some(id.as_str()));
7781
7782 let described = handler
7783 .handle_control_frame(
7784 &route_ctx(ConnectionId::new(2)).0,
7785 Frame::build(
7786 FrameType::Request,
7787 control_flags(),
7788 0,
7789 0,
7790 9,
7791 serde_json::to_vec(&ClientControlRequest::ServerDescribe {}).unwrap(),
7792 )
7793 .unwrap(),
7794 )
7795 .await
7796 .unwrap();
7797 let ClientControlResponse::ServerDescribe { machine_id, .. } =
7798 serde_json::from_slice(&described[0].body).unwrap()
7799 else {
7800 panic!("server.describe answered with another shape");
7801 };
7802 assert_eq!(machine_id.as_deref(), Some(id.as_str()));
7803 }
7804
7805 #[test]
7806 fn hello_ack_delivers_resolved_storage_descriptor_per_module() {
7807 let registry = Arc::new(Registry::default());
7810 let handler = ControlHandler::new(Arc::clone(®istry)).with_storage_config(Some(
7811 crate::daemon_config::StorageConfig::Sqlite {
7812 data_home: std::path::PathBuf::from("/data"),
7813 },
7814 ));
7815
7816 let responses = handler
7817 .handle_control(
7818 ConnectionId::new(1),
7819 hello_frame("alfonso-routing", PROTOCOL_VERSION, 7),
7820 )
7821 .unwrap();
7822 let ack = parse_ack(&responses[0]);
7823 assert_eq!(
7824 ack.storage,
7825 Some(serde_json::json!({
7826 "module_id": "alfonso-routing",
7827 "storage_namespace": "default",
7828 "isolation": { "kind": "module" },
7829 "backend": {
7830 "backend": "sqlite",
7831 "path": "/data/cortexkit/alfonso-routing/store.db"
7832 }
7833 })),
7834 "the delivered descriptor is the module's own sqlite store path"
7835 );
7836 }
7837
7838 #[test]
7839 fn hello_control_ops_none_is_baseline_and_guard_rejects_synthetic_gated_op() {
7840 let registry = Arc::new(Registry::default());
7841 let handler = ControlHandler::new(Arc::clone(®istry));
7842 let conn = ConnectionId::new(1);
7843 let responses = handler
7844 .handle_control(
7845 conn,
7846 hello_frame_with_control_ops("aft", PROTOCOL_VERSION, 7, None),
7847 )
7848 .unwrap();
7849 assert_eq!(responses[0].header.ty, FrameType::HelloAck);
7850 let registration = registry.get_module("aft").unwrap().unwrap();
7851 assert_eq!(registration.control_ops, module_baseline_control_ops());
7852
7853 let frame =
7854 Frame::build(FrameType::Request, control_flags(), 0, 0, 77, Vec::new()).unwrap();
7855 assert!(handler
7856 .guard_module_control_op(&frame, "aft", "route.bind")
7857 .unwrap()
7858 .is_none());
7859 let error = handler
7860 .guard_module_control_op(&frame, "aft", "test.synthetic")
7861 .unwrap()
7862 .expect("synthetic ungranted op should be rejected");
7863 assert_eq!(error.header.ty, FrameType::Error);
7864 assert_eq!(parse_error(&error)["code"], "op_not_allowed");
7865 }
7866
7867 #[test]
7868 fn hello_control_ops_some_adds_optional_grants() {
7869 let registry = Arc::new(Registry::default());
7870 let handler = ControlHandler::new(Arc::clone(®istry));
7871 handler
7872 .handle_control(
7873 ConnectionId::new(1),
7874 hello_frame_with_control_ops(
7875 "aft",
7876 PROTOCOL_VERSION,
7877 7,
7878 Some(vec![
7879 "future.synthetic".to_string(),
7880 "route.bind".to_string(),
7881 ]),
7882 ),
7883 )
7884 .unwrap();
7885 let registration = registry.get_module("aft").unwrap().unwrap();
7886 assert_eq!(
7887 registration.control_ops,
7888 vec![
7889 "route.bind".to_string(),
7890 "route.status".to_string(),
7891 "future.synthetic".to_string(),
7892 ]
7893 );
7894 let frame =
7895 Frame::build(FrameType::Request, control_flags(), 0, 0, 78, Vec::new()).unwrap();
7896 assert!(handler
7897 .guard_module_control_op(&frame, "aft", "future.synthetic")
7898 .unwrap()
7899 .is_none());
7900 }
7901
7902 #[tokio::test]
7903 async fn health_probe_refuses_unadvertised_module_without_sending_frame() {
7904 let registry = Arc::new(Registry::default());
7905 let forwarding = Arc::new(ForwardingTable::default());
7906 let handler =
7907 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
7908 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(10));
7909 hello_via_sink(
7910 &handler,
7911 &module_ctx,
7912 &mut module_rx,
7913 hello_frame_with_control_ops("aft", PROTOCOL_VERSION, 7, None),
7914 )
7915 .await;
7916
7917 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(20));
7918 let responses = handler
7919 .handle_control_frame(&client_ctx, supervisor_health_probe_frame(77, "aft"))
7920 .await
7921 .unwrap();
7922 assert_eq!(responses.len(), 1);
7923 assert_eq!(responses[0].header.ty, FrameType::Error);
7924 assert_eq!(parse_error(&responses[0])["code"], "health_not_advertised");
7925 assert!(module_rx.try_recv().is_err());
7926 }
7927
7928 #[tokio::test]
7929 async fn health_probe_demuxes_while_route_bind_relay_is_in_flight() {
7930 let registry = Arc::new(Registry::default());
7931 let forwarding = Arc::new(ForwardingTable::default());
7932 let handler =
7933 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
7934 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(30));
7935 hello_via_sink(
7936 &handler,
7937 &module_ctx,
7938 &mut module_rx,
7939 hello_frame_with_control_ops(
7940 "aft",
7941 PROTOCOL_VERSION,
7942 7,
7943 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
7944 ),
7945 )
7946 .await;
7947
7948 let project_root = unique_project_root("demux");
7949 let (route_client_ctx, mut route_client_rx) = route_ctx(ConnectionId::new(31));
7950 let route_handler = handler.clone();
7951 let route_task = tokio::spawn(async move {
7952 route_handler
7953 .handle_control_frame(
7954 &route_client_ctx,
7955 route_open_frame(100, "aft", project_root),
7956 )
7957 .await
7958 .unwrap()
7959 });
7960 let bind_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
7961 .await
7962 .unwrap()
7963 .unwrap();
7964 assert!(matches!(
7965 serde_json::from_slice::<ModuleControlRequest>(&bind_frame.body).unwrap(),
7966 ModuleControlRequest::RouteBind { .. }
7967 ));
7968
7969 let (health_client_ctx, _health_client_rx) = route_ctx(ConnectionId::new(32));
7970 let health_handler = handler.clone();
7971 let health_task = tokio::spawn(async move {
7972 health_handler
7973 .handle_control_frame(
7974 &health_client_ctx,
7975 supervisor_health_probe_frame(101, "aft"),
7976 )
7977 .await
7978 .unwrap()
7979 });
7980 let health_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
7981 .await
7982 .unwrap()
7983 .unwrap();
7984 assert_eq!(
7985 serde_json::from_slice::<ModuleControlRequest>(&health_frame.body).unwrap(),
7986 ModuleControlRequest::HealthCheck {}
7987 );
7988
7989 handler
7990 .handle_control_frame(
7991 &module_ctx,
7992 health_response(health_frame.header.corr, HealthStatus::Degraded),
7993 )
7994 .await
7995 .unwrap();
7996 let health_response = health_task.await.unwrap();
7997 assert_eq!(health_response.len(), 1);
7998 match serde_json::from_slice::<ClientControlResponse>(&health_response[0].body).unwrap() {
7999 ClientControlResponse::SupervisorHealthProbe {
8000 module_id,
8001 status,
8002 detail,
8003 metrics,
8004 } => {
8005 assert_eq!(module_id, "aft");
8006 assert_eq!(status, HealthStatus::Degraded);
8007 assert_eq!(detail.as_deref(), Some("warming"));
8008 assert_eq!(metrics, Some(json!({"queue_depth": 3})));
8009 }
8010 other => panic!("unexpected health response: {other:?}"),
8011 }
8012
8013 handler
8014 .handle_control_frame(&module_ctx, route_bind_ack(bind_frame.header.corr))
8015 .await
8016 .unwrap();
8017 let route_response = route_task.await.unwrap();
8018 assert!(route_response.is_empty());
8019 let published = route_client_rx.recv().await.unwrap();
8020 assert!(matches!(
8021 serde_json::from_slice::<ClientControlResponse>(&published.body).unwrap(),
8022 ClientControlResponse::RouteOpen { .. }
8023 ));
8024 }
8025
8026 async fn relay_route_open(
8031 handler: &ControlHandler,
8032 client_connection: ConnectionId,
8033 client_egress: &FrameSink,
8034 module_rx: &mut mpsc::Receiver<crate::router::OutboundFrame>,
8035 corr: u64,
8036 module_id: &str,
8037 project_root_label: &str,
8038 ) -> (tokio::task::JoinHandle<Vec<Frame>>, Frame) {
8039 let ctx = RouteCtx {
8040 connection_id: client_connection,
8041 egress: client_egress.clone(),
8042 };
8043 let handler = handler.clone();
8044 let project_root = unique_project_root(project_root_label);
8045 let module_id = module_id.to_string();
8046 let dispatch = tracing::dispatcher::get_default(|dispatch| dispatch.clone());
8047 let task = tokio::spawn(async move {
8048 let _guard = tracing::dispatcher::set_default(&dispatch);
8049 handler
8050 .handle_control_frame(&ctx, route_open_frame(corr, &module_id, project_root))
8051 .await
8052 .unwrap()
8053 });
8054 let bind = tokio::time::timeout(Duration::from_secs(2), module_rx.recv())
8055 .await
8056 .expect("module receives the relayed route.bind")
8057 .expect("module egress is open");
8058 (task, bind.frame)
8059 }
8060
8061 fn route_bind_channel(frame: &Frame) -> (u16, u32) {
8062 match serde_json::from_slice::<ModuleControlRequest>(&frame.body).unwrap() {
8063 ModuleControlRequest::RouteBind {
8064 route_channel,
8065 epoch,
8066 ..
8067 } => (route_channel, epoch),
8068 other => panic!("expected a route.bind request, got {other:?}"),
8069 }
8070 }
8071
8072 fn published_route(frame: &Frame) -> (u16, u32) {
8073 match serde_json::from_slice::<ClientControlResponse>(&frame.body).unwrap() {
8074 ClientControlResponse::RouteOpen {
8075 route_channel,
8076 route_epoch,
8077 } => (route_channel, route_epoch),
8078 other => panic!("expected a route.open response, got {other:?}"),
8079 }
8080 }
8081
8082 #[tokio::test]
8102 async fn late_bind_ack_for_a_closing_client_keeps_the_module_connection_serving() {
8103 let registry = Arc::new(Registry::default());
8104 let forwarding = Arc::new(ForwardingTable::default());
8105 let handler =
8106 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
8107
8108 let module_connection = ConnectionId::new(30);
8109 let (module_ctx, mut module_rx) = route_ctx(module_connection);
8110 hello_via_sink(
8111 &handler,
8112 &module_ctx,
8113 &mut module_rx,
8114 hello_frame("aft", PROTOCOL_VERSION, 7),
8115 )
8116 .await;
8117
8118 let dying_client = ConnectionId::new(31);
8119 let (dying_ctx, mut dying_rx) = route_ctx(dying_client);
8120
8121 let (first_task, first_bind) = relay_route_open(
8124 &handler,
8125 dying_client,
8126 &dying_ctx.egress,
8127 &mut module_rx,
8128 100,
8129 "aft",
8130 "closing-first",
8131 )
8132 .await;
8133 handler
8134 .handle_control_frame(&module_ctx, route_bind_ack(first_bind.header.corr))
8135 .await
8136 .unwrap();
8137 assert!(first_task.await.unwrap().is_empty());
8138 let (first_channel, first_epoch) = published_route(&dying_rx.recv().await.unwrap());
8139
8140 let (second_task, second_bind) = relay_route_open(
8142 &handler,
8143 dying_client,
8144 &dying_ctx.egress,
8145 &mut module_rx,
8146 101,
8147 "aft",
8148 "closing-second",
8149 )
8150 .await;
8151 let (abandoned_channel, abandoned_epoch) = route_bind_channel(&second_bind);
8152
8153 assert!(forwarding
8156 .escalate_client_delivery_failure(
8157 dying_client,
8158 first_channel,
8159 first_epoch,
8160 CloseReason::new(
8161 "module_to_client_delivery_failed",
8162 "client egress refused a module frame",
8163 ),
8164 crate::forwarding::UndeliveredFrame {
8165 module_id: None,
8166 sink: &dying_ctx.egress,
8167 },
8168 )
8169 .unwrap());
8170 assert!(!dying_ctx.egress.is_closed());
8171
8172 let ack = handler
8174 .handle_control_frame(&module_ctx, route_bind_ack(second_bind.header.corr))
8175 .await;
8176 let module_loop_error = ack.as_ref().err().map(ToString::to_string);
8177 if module_loop_error.is_some() {
8178 handler.cleanup_connection(module_connection).unwrap();
8182 }
8183 let post_ack_module_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
8187 .await
8188 .ok()
8189 .flatten();
8190
8191 assert!(
8193 registry
8194 .get_module_by_connection(module_connection)
8195 .unwrap()
8196 .is_some(),
8197 "one client's closing connection ended the shared module connection: \
8198 {module_loop_error:?}"
8199 );
8200 let cotenant = ConnectionId::new(32);
8202 let (cotenant_ctx, mut cotenant_rx) = route_ctx(cotenant);
8203 let (cotenant_task, cotenant_bind) = relay_route_open(
8204 &handler,
8205 cotenant,
8206 &cotenant_ctx.egress,
8207 &mut module_rx,
8208 102,
8209 "aft",
8210 "closing-cotenant",
8211 )
8212 .await;
8213 handler
8214 .handle_control_frame(&module_ctx, route_bind_ack(cotenant_bind.header.corr))
8215 .await
8216 .unwrap();
8217 assert!(cotenant_task.await.unwrap().is_empty());
8218 let (cotenant_channel, cotenant_epoch) =
8219 published_route(&cotenant_rx.recv().await.unwrap());
8220 assert!(matches!(
8221 forwarding
8222 .lookup_data_route(cotenant, cotenant_channel, cotenant_epoch)
8223 .unwrap(),
8224 DataRoute::Client(DataRouteState::Bound(_))
8225 ));
8226
8227 let goodbye = post_ack_module_frame
8230 .expect("module receives a GOODBYE for the abandoned route channel");
8231 assert_eq!(goodbye.header.ty, FrameType::Goodbye);
8232 assert_eq!(goodbye.header.channel, abandoned_channel);
8233 assert_eq!(goodbye.header.epoch, abandoned_epoch);
8234
8235 assert!(dying_rx.try_recv().is_err());
8239 let second_response = second_task.await.unwrap();
8240 assert_eq!(second_response.len(), 1);
8241 assert_eq!(
8242 parse_error(&second_response[0])["code"],
8243 "target_unavailable"
8244 );
8245 }
8246
8247 #[test]
8254 fn only_the_modules_own_closing_connection_ends_the_module_loop() {
8255 let handler = ControlHandler::default();
8256 let module_connection = ConnectionId::new(30);
8257 let client_connection = ConnectionId::new(31);
8258
8259 handler
8260 .refuse_to_end_module_connection_for_a_client(
8261 module_connection,
8262 77,
8263 ForwardingError::ConnectionClosing {
8264 connection_id: client_connection,
8265 },
8266 )
8267 .expect("a closing client must never end the module connection");
8268
8269 assert!(matches!(
8270 handler.refuse_to_end_module_connection_for_a_client(
8271 module_connection,
8272 78,
8273 ForwardingError::ConnectionClosing {
8274 connection_id: module_connection,
8275 },
8276 ),
8277 Err(RouterError::Forwarding(ForwardingError::ConnectionClosing {
8278 connection_id
8279 })) if connection_id == module_connection
8280 ));
8281 assert!(matches!(
8282 handler.refuse_to_end_module_connection_for_a_client(
8283 module_connection,
8284 79,
8285 ForwardingError::Poisoned,
8286 ),
8287 Err(RouterError::Forwarding(ForwardingError::Poisoned))
8288 ));
8289 assert!(matches!(
8290 handler.refuse_to_end_module_connection_for_a_client(
8291 module_connection,
8292 80,
8293 ForwardingError::StaleModuleEndpoint,
8294 ),
8295 Err(RouterError::Forwarding(
8296 ForwardingError::StaleModuleEndpoint
8297 ))
8298 ));
8299 }
8300
8301 #[tokio::test]
8314 async fn route_open_refuses_consumer_identity_that_fails_spawn_attestation() {
8315 let registry = Arc::new(Registry::default());
8316 let forwarding = Arc::new(ForwardingTable::default());
8317 let supervisor = SupervisorHandle::new();
8318 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
8319 let handler =
8320 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
8321 .with_supervisor(supervisor);
8322
8323 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(90));
8324 hello_via_sink(
8325 &handler,
8326 &target_ctx,
8327 &mut target_rx,
8328 hello_frame("target", PROTOCOL_VERSION, 1),
8329 )
8330 .await;
8331
8332 let wrong_nonce = handler
8336 .handle_control_frame(
8337 &route_ctx(ConnectionId::new(91)).0,
8338 route_open_frame_with_admission_facts(
8339 20,
8340 "target",
8341 unique_project_root("admission-facts"),
8342 Some(subc_control::ConsumerIdentity {
8343 module_id: "fed".to_string(),
8344 launch_nonce: "not-the-real-nonce".to_string(),
8345 }),
8346 None,
8347 ),
8348 )
8349 .await
8350 .unwrap();
8351 assert_eq!(
8352 parse_error(&wrong_nonce[0])["code"],
8353 "bad_consumer_identity",
8354 "a mismatched launch nonce must be refused, not stamped Reserved"
8355 );
8356
8357 let never_spawned = handler
8361 .handle_control_frame(
8362 &route_ctx(ConnectionId::new(92)).0,
8363 route_open_frame_with_admission_facts(
8364 21,
8365 "target",
8366 unique_project_root("admission-facts"),
8367 Some(subc_control::ConsumerIdentity {
8368 module_id: "never-spawned".to_string(),
8369 launch_nonce: "any-nonce".to_string(),
8370 }),
8371 None,
8372 ),
8373 )
8374 .await
8375 .unwrap();
8376 assert_eq!(
8377 parse_error(&never_spawned[0])["code"],
8378 "bad_consumer_identity",
8379 "an unspawned module_id must be refused rather than accepted for lack of a record"
8380 );
8381 }
8382
8383 #[tokio::test]
8406 async fn route_open_stamps_reserved_for_a_correctly_attested_consumer() {
8407 let registry = Arc::new(Registry::default());
8408 let forwarding = Arc::new(ForwardingTable::default());
8409 let supervisor = SupervisorHandle::new();
8410 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
8411 let handler =
8412 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
8413 .with_supervisor(supervisor);
8414
8415 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(95));
8416 hello_via_sink(
8417 &handler,
8418 &target_ctx,
8419 &mut target_rx,
8420 hello_frame("target", PROTOCOL_VERSION, 1),
8421 )
8422 .await;
8423
8424 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(96));
8425 let route_handler = handler.clone();
8426 let route_task = tokio::spawn(async move {
8427 route_handler
8428 .handle_control_frame(
8429 &client_ctx,
8430 route_open_frame_with_admission_facts(
8431 30,
8432 "target",
8433 unique_project_root("admission-facts"),
8434 Some(subc_control::ConsumerIdentity {
8435 module_id: "fed".to_string(),
8436 launch_nonce: "fed-nonce".to_string(),
8437 }),
8438 None,
8439 ),
8440 )
8441 .await
8442 .unwrap()
8443 });
8444
8445 let bind_frame = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
8452 .await
8453 .expect("no route.bind within 5s: the consumer-identity guard refused a correctly attested consumer")
8454 .expect("module control channel closed before route.bind");
8455 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
8456 let ModuleControlRequest::RouteBind { principal, .. } = bind else {
8457 panic!("expected route.bind")
8458 };
8459 assert_eq!(
8460 principal,
8461 Some(Principal::Reserved {
8462 module_id: "fed".to_string()
8463 }),
8464 "a correctly attested consumer must be stamped Reserved for its own id"
8465 );
8466
8467 handler
8468 .handle_control_frame(&target_ctx, route_bind_ack(bind_frame.header.corr))
8469 .await
8470 .unwrap();
8471 assert!(route_task.await.unwrap().is_empty());
8472 assert!(
8473 matches!(
8474 serde_json::from_slice::<ClientControlResponse>(
8475 &client_rx.recv().await.unwrap().body
8476 )
8477 .unwrap(),
8478 ClientControlResponse::RouteOpen { .. }
8479 ),
8480 "the route must actually open, not merely avoid an error"
8481 );
8482 }
8483
8484 #[tokio::test(start_paused = true)]
8485 async fn supervisor_routes_serializes_live_draining_bindings_from_the_real_handler() {
8486 let registry = Arc::new(Registry::default());
8487 let forwarding = Arc::new(ForwardingTable::default());
8488 let supervisor = SupervisorHandle::new();
8489 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
8490 let handler =
8491 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
8492 .with_supervisor(supervisor);
8493
8494 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(101));
8495 hello_via_sink(
8496 &handler,
8497 &target_ctx,
8498 &mut target_rx,
8499 hello_frame("target", PROTOCOL_VERSION, 1),
8500 )
8501 .await;
8502
8503 let (direct_ctx, mut direct_rx) = route_ctx(ConnectionId::new(102));
8504 let direct_handler = handler.clone();
8505 let direct_open = tokio::spawn(async move {
8506 direct_handler
8507 .handle_control_frame(
8508 &direct_ctx,
8509 route_open_frame(2, "target", unique_project_root("route-census-direct")),
8510 )
8511 .await
8512 .unwrap()
8513 });
8514 let direct_bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
8515 .await
8516 .expect("no direct route.bind within 5s")
8517 .expect("target control channel closed before direct route.bind");
8518 handler
8519 .handle_control_frame(&target_ctx, route_bind_ack(direct_bind.header.corr))
8520 .await
8521 .unwrap();
8522 assert!(direct_open.await.unwrap().is_empty());
8523 let _ = direct_rx.recv().await.unwrap();
8524
8525 let (reserved_ctx, mut reserved_rx) = route_ctx(ConnectionId::new(103));
8526 let reserved_handler = handler.clone();
8527 let reserved_open = tokio::spawn(async move {
8528 reserved_handler
8529 .handle_control_frame(
8530 &reserved_ctx,
8531 route_open_frame_with_admission_facts(
8532 3,
8533 "target",
8534 unique_project_root("admission-facts"),
8535 Some(ConsumerIdentity {
8536 module_id: "fed".to_string(),
8537 launch_nonce: "fed-nonce".to_string(),
8538 }),
8539 None,
8540 ),
8541 )
8542 .await
8543 .unwrap()
8544 });
8545 let reserved_bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
8546 .await
8547 .expect("no reserved route.bind within 5s")
8548 .expect("target control channel closed before reserved route.bind");
8549 handler
8550 .handle_control_frame(&target_ctx, route_bind_ack(reserved_bind.header.corr))
8551 .await
8552 .unwrap();
8553 assert!(reserved_open.await.unwrap().is_empty());
8554 let _ = reserved_rx.recv().await.unwrap();
8555
8556 forwarding
8557 .begin_module_drain("target", subc_control::RouteCloseReason::Reload)
8558 .unwrap();
8559 let (census_ctx, _census_rx) = route_ctx(ConnectionId::new(104));
8560 let census_body = serde_json::to_vec(&ClientControlRequest::SupervisorRoutes {
8561 module_id: Some("target".to_string()),
8562 })
8563 .unwrap();
8564 let census_frame =
8565 Frame::build(FrameType::Request, control_flags(), 0, 0, 4, census_body).unwrap();
8566 let response = handler
8567 .handle_control_frame(&census_ctx, census_frame)
8568 .await
8569 .unwrap()
8570 .pop()
8571 .unwrap();
8572 let actual: Value = serde_json::from_slice(&response.body).unwrap();
8573 let decoded: ClientControlResponse = serde_json::from_value(actual.clone()).unwrap();
8574 assert!(matches!(
8575 decoded,
8576 ClientControlResponse::SupervisorRoutes { .. }
8577 ));
8578 let routes = actual["modules"][0]["routes"].as_array().unwrap();
8579 assert_eq!(routes.len(), 2);
8580 assert!(routes.iter().all(|route| route["draining"] == true));
8581 assert!(
8584 routes.iter().all(|route| route["drain_reason"] == "reload"),
8585 "draining routes must name the drain's reason: {routes:?}"
8586 );
8587 assert!(routes.iter().any(|route| {
8588 route["consumer"] == serde_json::json!({"kind": "direct", "connection_id": 102})
8589 }));
8590 assert!(routes.iter().any(|route| {
8591 route["consumer"] == serde_json::json!({"kind": "reserved", "module_id": "fed"})
8592 }));
8593
8594 let golden_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8595 .join("../subc-control/tests/golden/client_control_response_supervisor_routes.json");
8596 if std::env::var_os("UPDATE_GOLDEN").is_some() {
8597 std::fs::write(
8598 &golden_path,
8599 format!("{}\n", serde_json::to_string_pretty(&actual).unwrap()),
8600 )
8601 .unwrap();
8602 }
8603 let expected: Value =
8604 serde_json::from_str(&std::fs::read_to_string(golden_path).unwrap()).unwrap();
8605 assert_eq!(actual, expected);
8606 }
8607
8608 async fn query_live_roots(
8609 handler: &ControlHandler,
8610 module_ctx: &RouteCtx,
8611 ) -> ModuleControlResponseToModule {
8612 let body = serde_json::to_vec(&ModuleControlRequestFromModule::LiveRoots {}).unwrap();
8613 let frame = Frame::build(FrameType::Request, control_flags(), 0, 0, 900, body).unwrap();
8614 let response = handler
8615 .handle_control_frame(module_ctx, frame)
8616 .await
8617 .unwrap()
8618 .pop()
8619 .unwrap();
8620 serde_json::from_slice(&response.body).unwrap()
8621 }
8622
8623 #[tokio::test(start_paused = true)]
8624 async fn supervisor_live_roots_root_known_arm_counts_bound_and_pending_from_real_handler() {
8625 let registry = Arc::new(Registry::default());
8626 let forwarding = Arc::new(ForwardingTable::default());
8627 let handler = ControlHandler::with_forwarding(registry, forwarding);
8628 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(301));
8629 hello_via_sink(
8630 &handler,
8631 &target_ctx,
8632 &mut target_rx,
8633 hello_frame("target", PROTOCOL_VERSION, 1),
8634 )
8635 .await;
8636 let root = unique_project_root("live-roots-known");
8637 let path = ProjectRootId::from_path_allowing_missing(root.path())
8638 .unwrap()
8639 .as_path()
8640 .to_path_buf();
8641 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(302));
8642 let open_handler = handler.clone();
8643 let opened = tokio::spawn(async move {
8644 open_handler
8645 .handle_control_frame(&client_ctx, route_open_frame(2, "target", root))
8646 .await
8647 .unwrap()
8648 });
8649 let bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
8650 .await
8651 .unwrap()
8652 .unwrap();
8653 handler
8654 .handle_control_frame(&target_ctx, route_bind_ack(bind.header.corr))
8655 .await
8656 .unwrap();
8657 assert!(opened.await.unwrap().is_empty());
8658 let _ = client_rx.recv().await.unwrap();
8659
8660 let root = unique_project_root("live-roots-pending");
8661 let pending_path = ProjectRootId::from_path_allowing_missing(root.path())
8662 .unwrap()
8663 .as_path()
8664 .to_path_buf();
8665 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(303));
8666 let open_handler = handler.clone();
8667 let pending = tokio::spawn(async move {
8668 open_handler
8669 .handle_control_frame(&client_ctx, route_open_frame(3, "target", root))
8670 .await
8671 .unwrap()
8672 });
8673 let pending_bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
8674 .await
8675 .unwrap()
8676 .unwrap();
8677 let actual = query_live_roots(&handler, &target_ctx).await;
8678 let ModuleControlResponseToModule::LiveRoots {
8679 roots,
8680 unknown_root_bindings,
8681 total_bindings,
8682 } = actual
8683 else {
8684 panic!("expected live roots")
8685 };
8686 assert_eq!(total_bindings, 2, "root-known arm must count live routes");
8687 assert_eq!(unknown_root_bindings, 0);
8688 assert_eq!(
8689 roots.len(),
8690 2,
8691 "root-known arm must retain each canonical root"
8692 );
8693 assert_eq!(
8694 total_bindings,
8695 roots.iter().map(|r| r.bound + r.pending).sum::<u64>() + unknown_root_bindings
8696 );
8697 let counts = roots
8698 .iter()
8699 .map(|root| (root.project_root.clone(), root.bound, root.pending))
8700 .collect::<Vec<_>>();
8701 let mut expected = vec![(path, 1, 0), (pending_path, 0, 1)];
8702 expected.sort_by(|a, b| a.0.cmp(&b.0));
8703 assert_eq!(
8704 counts, expected,
8705 "roots must sort by path and count pending separately"
8706 );
8707 handler
8708 .handle_control_frame(&target_ctx, route_bind_ack(pending_bind.header.corr))
8709 .await
8710 .unwrap();
8711 assert!(pending.await.unwrap().is_empty());
8712 }
8713
8714 #[tokio::test(start_paused = true)]
8715 async fn supervisor_live_roots_unknown_root_arm_is_not_no_bindings() {
8716 let forwarding = Arc::new(ForwardingTable::default());
8717 let handler =
8718 ControlHandler::with_forwarding(Arc::new(Registry::default()), Arc::clone(&forwarding));
8719 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(311));
8720 hello_via_sink(
8721 &handler,
8722 &target_ctx,
8723 &mut target_rx,
8724 hello_frame("target", PROTOCOL_VERSION, 1),
8725 )
8726 .await;
8727 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(312));
8728 let pending = forwarding
8729 .begin_route_bind_relay_for_test(
8730 client_ctx.connection_id,
8731 client_ctx.egress.clone(),
8732 2,
8733 "target",
8734 )
8735 .unwrap();
8736 forwarding
8737 .complete_pending_relay(
8738 target_ctx.connection_id,
8739 pending.corr,
8740 RouteBindRelayOutcome::Accepted,
8741 )
8742 .unwrap();
8743 let actual = query_live_roots(&handler, &target_ctx).await;
8744 let ModuleControlResponseToModule::LiveRoots {
8745 roots,
8746 unknown_root_bindings,
8747 total_bindings,
8748 } = actual
8749 else {
8750 panic!("expected live roots")
8751 };
8752 assert!(roots.is_empty(), "unknown-root arm must not invent a root");
8753 assert_eq!(
8754 unknown_root_bindings, 1,
8755 "unknown-root arm must not read as no bindings"
8756 );
8757 assert_eq!(total_bindings, 1, "unknown-root arm has a live binding");
8758 assert_eq!(
8759 total_bindings,
8760 roots.iter().map(|r| r.bound + r.pending).sum::<u64>() + unknown_root_bindings
8761 );
8762 }
8763
8764 #[tokio::test(start_paused = true)]
8772 async fn hello_ack_reaches_the_module_before_a_route_bind_raced_into_the_reply_gap() {
8773 let forwarding = Arc::new(ForwardingTable::default());
8774 let handler =
8775 ControlHandler::with_forwarding(Arc::new(Registry::default()), Arc::clone(&forwarding));
8776 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(341));
8777 let replies = handler
8778 .handle_control_frame(&module_ctx, hello_frame("raced", PROTOCOL_VERSION, 7))
8779 .await
8780 .unwrap();
8781 let queued_by_hello = module_rx.len();
8782
8783 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(342));
8784 let open_handler = handler.clone();
8785 let open = tokio::spawn(async move {
8786 open_handler
8787 .handle_control_frame(
8788 &client_ctx,
8789 route_open_frame(2, "raced", unique_project_root("hello-ack-race")),
8790 )
8791 .await
8792 .unwrap()
8793 });
8794 let mut spins = 0;
8796 while module_rx.len() == queued_by_hello {
8797 spins += 1;
8798 assert!(spins < 10_000, "route.open never queued a route.bind");
8799 tokio::task::yield_now().await;
8800 }
8801
8802 for reply in replies {
8804 module_ctx.egress.send(reply).await.unwrap();
8805 }
8806
8807 let first = module_rx.recv().await.unwrap().frame;
8808 assert_eq!(
8809 first.header.ty,
8810 FrameType::HelloAck,
8811 "the first frame a registering module reads must be its HELLO_ACK"
8812 );
8813 assert_eq!(first.header.corr, 7);
8814 let second = module_rx.recv().await.unwrap().frame;
8815 assert_eq!(second.header.ty, FrameType::Request);
8816 assert!(
8817 matches!(
8818 serde_json::from_slice::<ModuleControlRequest>(&second.body).unwrap(),
8819 ModuleControlRequest::RouteBind { .. }
8820 ),
8821 "the route.bind follows the ack"
8822 );
8823 assert!(module_rx.try_recv().is_err(), "nothing else was queued");
8824
8825 handler
8826 .handle_control_frame(&module_ctx, route_bind_ack(second.header.corr))
8827 .await
8828 .unwrap();
8829 assert!(open.await.unwrap().is_empty());
8830 let _ = client_rx.recv().await.unwrap();
8831 }
8832
8833 #[tokio::test(start_paused = true)]
8834 async fn supervisor_live_roots_cross_module_scope_uses_requesting_connection() {
8835 let handler = ControlHandler::with_forwarding(
8836 Arc::new(Registry::default()),
8837 Arc::new(ForwardingTable::default()),
8838 );
8839 let (first_ctx, mut first_rx) = route_ctx(ConnectionId::new(315));
8840 let (second_ctx, mut second_rx) = route_ctx(ConnectionId::new(316));
8841 hello_via_sink(
8842 &handler,
8843 &first_ctx,
8844 &mut first_rx,
8845 hello_frame("first", PROTOCOL_VERSION, 1),
8846 )
8847 .await;
8848 hello_via_sink(
8849 &handler,
8850 &second_ctx,
8851 &mut second_rx,
8852 hello_frame("second", PROTOCOL_VERSION, 2),
8853 )
8854 .await;
8855 let root = unique_project_root("second-only");
8856 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(317));
8857 let cloned = handler.clone();
8858 let open = tokio::spawn(async move {
8859 cloned
8860 .handle_control_frame(&client_ctx, route_open_frame(3, "second", root))
8861 .await
8862 .unwrap()
8863 });
8864 let bind = tokio::time::timeout(Duration::from_secs(5), second_rx.recv())
8865 .await
8866 .unwrap()
8867 .unwrap();
8868 let first = query_live_roots(&handler, &first_ctx).await;
8869 let second = query_live_roots(&handler, &second_ctx).await;
8870 assert!(
8871 matches!(
8872 first,
8873 ModuleControlResponseToModule::LiveRoots {
8874 total_bindings: 0,
8875 ..
8876 }
8877 ),
8878 "cross-module scope must not expose another module's roots"
8879 );
8880 assert!(
8881 matches!(
8882 second,
8883 ModuleControlResponseToModule::LiveRoots {
8884 total_bindings: 1,
8885 ..
8886 }
8887 ),
8888 "second module must see its pending route"
8889 );
8890 handler
8891 .handle_control_frame(&second_ctx, route_bind_ack(bind.header.corr))
8892 .await
8893 .unwrap();
8894 assert!(open.await.unwrap().is_empty());
8895 }
8896
8897 #[tokio::test(start_paused = true)]
8898 async fn supervisor_live_roots_no_bindings_arm_is_empty() {
8899 let handler = ControlHandler::with_forwarding(
8900 Arc::new(Registry::default()),
8901 Arc::new(ForwardingTable::default()),
8902 );
8903 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(321));
8904 hello_via_sink(
8905 &handler,
8906 &target_ctx,
8907 &mut target_rx,
8908 hello_frame("target", PROTOCOL_VERSION, 1),
8909 )
8910 .await;
8911 let actual = query_live_roots(&handler, &target_ctx).await;
8912 let ModuleControlResponseToModule::LiveRoots {
8913 roots,
8914 unknown_root_bindings,
8915 total_bindings,
8916 } = actual
8917 else {
8918 panic!("expected live roots")
8919 };
8920 assert!(roots.is_empty());
8921 assert_eq!(unknown_root_bindings, 0);
8922 assert_eq!(total_bindings, 0);
8923 assert_eq!(
8924 total_bindings,
8925 roots.iter().map(|r| r.bound + r.pending).sum::<u64>() + unknown_root_bindings
8926 );
8927 }
8928
8929 fn fed_admission_facts_vectors() -> Vec<(String, Value)> {
8936 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8937 .join("tests/fixtures/fed/admission-facts-emit.jsonl");
8938 let text = std::fs::read_to_string(&path)
8939 .unwrap_or_else(|err| panic!("vendored fed corpus unreadable at {path:?}: {err}"));
8940 let vectors: Vec<(String, Value)> = text
8941 .lines()
8942 .filter(|line| !line.trim().is_empty())
8943 .map(|line| {
8944 let entry: Value = serde_json::from_str(line).expect("corpus line must be JSON");
8945 let id = entry["corpus_id"]
8946 .as_str()
8947 .expect("every vector carries a corpus_id")
8948 .to_string();
8949 (id, entry["package"].clone())
8950 })
8951 .collect();
8952 assert_eq!(
8956 vectors.len(),
8957 3,
8958 "vendored fed corpus changed size; re-sync from subc-federation"
8959 );
8960
8961 const COMMONLY_MODELLED: [&str; 3] = ["schema", "verified_class", "org"];
8976 let richest = vectors
8977 .iter()
8978 .filter_map(|(_, package)| package.as_object())
8979 .map(|object| {
8980 object
8981 .keys()
8982 .filter(|key| !COMMONLY_MODELLED.contains(&key.as_str()))
8983 .count()
8984 })
8985 .max()
8986 .unwrap_or(0);
8987 assert!(
8988 richest >= 2,
8989 "vendored corpus no longer carries a package with unmodelled fields, \
8990 so the relay test can no longer distinguish a verbatim relay from a lossy one"
8991 );
8992
8993 vectors
8994 }
8995
8996 #[tokio::test]
9004 async fn admission_facts_relay_carries_vendored_packages_verbatim() {
9005 for (corpus_id, package) in fed_admission_facts_vectors() {
9006 let registry = Arc::new(Registry::default());
9007 let forwarding = Arc::new(ForwardingTable::default());
9008 let supervisor = SupervisorHandle::new();
9009 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
9010 let handler =
9011 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9012 .with_supervisor(supervisor)
9013 .with_admission_facts_config(
9014 Some("fed".to_string()),
9015 Some(vec!["target".to_string()]),
9016 );
9017
9018 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(90));
9019 hello_via_sink(
9020 &handler,
9021 &target_ctx,
9022 &mut target_rx,
9023 hello_frame("target", PROTOCOL_VERSION, 1),
9024 )
9025 .await;
9026
9027 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(91));
9028 let route_handler = handler.clone();
9029 let expected = package.clone();
9030 let route_task = tokio::spawn(async move {
9031 route_handler
9032 .handle_control_frame(
9033 &client_ctx,
9034 route_open_frame_with_admission_facts(
9035 20,
9036 "target",
9037 unique_project_root("admission-facts"),
9038 Some(subc_control::ConsumerIdentity {
9039 module_id: "fed".to_string(),
9040 launch_nonce: "fed-nonce".to_string(),
9041 }),
9042 Some(package),
9043 ),
9044 )
9045 .await
9046 .unwrap()
9047 });
9048
9049 let bind_frame = target_rx.recv().await.unwrap();
9050 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
9051 let ModuleControlRequest::RouteBind {
9052 admission_facts, ..
9053 } = bind
9054 else {
9055 panic!("{corpus_id}: expected route.bind")
9056 };
9057 assert_eq!(
9058 admission_facts,
9059 Some(expected),
9060 "{corpus_id}: relay must not add, drop or reshape any field"
9061 );
9062
9063 handler
9064 .handle_control_frame(&target_ctx, route_bind_ack(bind_frame.header.corr))
9065 .await
9066 .unwrap();
9067 route_task.await.unwrap();
9068 }
9069 }
9070
9071 #[tokio::test]
9072 async fn admission_facts_gate_checks_carrier_target_and_precedence() {
9073 let registry = Arc::new(Registry::default());
9074 let forwarding = Arc::new(ForwardingTable::default());
9075 let supervisor = SupervisorHandle::new();
9076 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
9077 supervisor.set_spawn_nonce("other", "other-nonce".to_string());
9078 let handler =
9079 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9080 .with_supervisor(supervisor)
9081 .with_admission_facts_config(
9082 Some("fed".to_string()),
9083 Some(vec!["target".to_string()]),
9084 );
9085
9086 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(70));
9087 hello_via_sink(
9088 &handler,
9089 &target_ctx,
9090 &mut target_rx,
9091 hello_frame("target", PROTOCOL_VERSION, 1),
9092 )
9093 .await;
9094 let (other_ctx, mut other_rx) = route_ctx(ConnectionId::new(71));
9095 hello_via_sink(
9096 &handler,
9097 &other_ctx,
9098 &mut other_rx,
9099 hello_frame("other", PROTOCOL_VERSION, 2),
9100 )
9101 .await;
9102
9103 let facts = json!({"schema": 1, "verified_class": "member", "org": "01H"});
9104 let expected_facts = facts.clone();
9105 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(72));
9106 let route_handler = handler.clone();
9107 let route_task = tokio::spawn(async move {
9108 route_handler
9109 .handle_control_frame(
9110 &client_ctx,
9111 route_open_frame_with_admission_facts(
9112 10,
9113 "target",
9114 unique_project_root("admission-facts"),
9115 Some(subc_control::ConsumerIdentity {
9116 module_id: "fed".to_string(),
9117 launch_nonce: "fed-nonce".to_string(),
9118 }),
9119 Some(facts.clone()),
9120 ),
9121 )
9122 .await
9123 .unwrap()
9124 });
9125 let bind_frame = target_rx.recv().await.unwrap();
9126 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
9127 let ModuleControlRequest::RouteBind {
9128 admission_facts, ..
9129 } = bind
9130 else {
9131 panic!("expected route.bind")
9132 };
9133 assert_eq!(admission_facts, Some(expected_facts));
9134 handler
9135 .handle_control_frame(&target_ctx, route_bind_ack(bind_frame.header.corr))
9136 .await
9137 .unwrap();
9138 assert!(route_task.await.unwrap().is_empty());
9139 assert!(matches!(
9140 serde_json::from_slice::<ClientControlResponse>(&client_rx.recv().await.unwrap().body)
9141 .unwrap(),
9142 ClientControlResponse::RouteOpen { .. }
9143 ));
9144
9145 let direct = handler
9146 .handle_control_frame(
9147 &route_ctx(ConnectionId::new(73)).0,
9148 route_open_frame_with_admission_facts(
9149 11,
9150 "target",
9151 unique_project_root("admission-facts"),
9152 None,
9153 Some(json!({"x": 1})),
9154 ),
9155 )
9156 .await
9157 .unwrap();
9158 assert_eq!(
9159 parse_error(&direct[0])["code"],
9160 "admission_facts_not_permitted"
9161 );
9162
9163 let different_reserved = handler
9164 .handle_control_frame(
9165 &route_ctx(ConnectionId::new(77)).0,
9166 route_open_frame_with_admission_facts(
9167 15,
9168 "target",
9169 unique_project_root("admission-facts"),
9170 Some(subc_control::ConsumerIdentity {
9171 module_id: "other".to_string(),
9172 launch_nonce: "other-nonce".to_string(),
9173 }),
9174 Some(json!({"x": 1})),
9175 ),
9176 )
9177 .await
9178 .unwrap();
9179 assert_eq!(
9180 parse_error(&different_reserved[0])["code"],
9181 "admission_facts_not_permitted"
9182 );
9183
9184 let other_target = handler
9185 .handle_control_frame(
9186 &route_ctx(ConnectionId::new(74)).0,
9187 route_open_frame_with_admission_facts(
9188 12,
9189 "other",
9190 unique_project_root("admission-facts"),
9191 Some(subc_control::ConsumerIdentity {
9192 module_id: "fed".to_string(),
9193 launch_nonce: "fed-nonce".to_string(),
9194 }),
9195 Some(json!({"x": 1})),
9196 ),
9197 )
9198 .await
9199 .unwrap();
9200 assert_eq!(
9201 parse_error(&other_target[0])["code"],
9202 "admission_facts_target_not_allowed"
9203 );
9204
9205 let nonexistent = handler
9206 .handle_control_frame(
9207 &route_ctx(ConnectionId::new(75)).0,
9208 route_open_frame_with_admission_facts(
9209 13,
9210 "missing",
9211 unique_project_root("admission-facts"),
9212 None,
9213 Some(json!({"x": 1})),
9214 ),
9215 )
9216 .await
9217 .unwrap();
9218 assert_eq!(parse_error(&nonexistent[0])["code"], "unknown_module");
9219
9220 let described = handler
9221 .handle_control_frame(
9222 &route_ctx(ConnectionId::new(76)).0,
9223 Frame::build(
9224 FrameType::Request,
9225 control_flags(),
9226 0,
9227 0,
9228 14,
9229 serde_json::to_vec(&ClientControlRequest::ServerDescribe {}).unwrap(),
9230 )
9231 .unwrap(),
9232 )
9233 .await
9234 .unwrap();
9235 let ClientControlResponse::ServerDescribe { capabilities, .. } =
9236 serde_json::from_slice(&described[0].body).unwrap()
9237 else {
9238 panic!("expected server.describe response")
9239 };
9240 assert!(capabilities
9241 .iter()
9242 .any(|cap| cap == "admission_facts_relay_v1"));
9243 }
9244
9245 #[tokio::test]
9246 async fn admission_facts_without_configured_carrier_are_rejected() {
9247 let registry = Arc::new(Registry::default());
9248 let forwarding = Arc::new(ForwardingTable::default());
9249 let handler = ControlHandler::with_forwarding(registry, forwarding);
9250 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(78));
9251 hello_via_sink(
9252 &handler,
9253 &target_ctx,
9254 &mut target_rx,
9255 hello_frame("target", PROTOCOL_VERSION, 1),
9256 )
9257 .await;
9258
9259 let responses = handler
9260 .handle_control_frame(
9261 &route_ctx(ConnectionId::new(79)).0,
9262 route_open_frame_with_admission_facts(
9263 16,
9264 "target",
9265 unique_project_root("admission-facts"),
9266 None,
9267 Some(json!({"x": 1})),
9268 ),
9269 )
9270 .await
9271 .unwrap();
9272 assert_eq!(
9273 parse_error(&responses[0])["code"],
9274 "admission_facts_not_permitted"
9275 );
9276 }
9277
9278 #[tokio::test]
9279 async fn route_open_relays_consumer_capabilities_verbatim() {
9280 let registry = Arc::new(Registry::default());
9281 let forwarding = Arc::new(ForwardingTable::default());
9282 let handler =
9283 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
9284 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(37));
9285 hello_via_sink(
9286 &handler,
9287 &module_ctx,
9288 &mut module_rx,
9289 hello_frame("aft", PROTOCOL_VERSION, 7),
9290 )
9291 .await;
9292
9293 let expected = vec!["elicitation".to_string(), "roots".to_string()];
9294 let expected_for_request = expected.clone();
9295 let project_root = unique_project_root("consumer-capabilities-present");
9296 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(38));
9297 let route_handler = handler.clone();
9298 let route_task = tokio::spawn(async move {
9299 route_handler
9300 .handle_control_frame(
9301 &client_ctx,
9302 route_open_frame_with_consumer_capabilities(
9303 401,
9304 "aft",
9305 project_root,
9306 Some(expected_for_request),
9307 ),
9308 )
9309 .await
9310 .unwrap()
9311 });
9312 let bind_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
9313 .await
9314 .unwrap()
9315 .unwrap();
9316 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
9317 let ModuleControlRequest::RouteBind {
9318 consumer_capabilities,
9319 ..
9320 } = bind
9321 else {
9322 panic!("expected route.bind request, got {bind:?}");
9323 };
9324 assert_eq!(consumer_capabilities, Some(expected.clone()));
9325
9326 handler
9327 .handle_control_frame(&module_ctx, route_bind_ack(bind_frame.header.corr))
9328 .await
9329 .unwrap();
9330 let route_response = route_task.await.unwrap();
9331 assert!(route_response.is_empty());
9332 let published = client_rx.recv().await.unwrap();
9333 assert!(matches!(
9334 serde_json::from_slice::<ClientControlResponse>(&published.body).unwrap(),
9335 ClientControlResponse::RouteOpen { .. }
9336 ));
9337 }
9338
9339 #[tokio::test]
9340 async fn route_open_without_consumer_capabilities_relays_none() {
9341 let registry = Arc::new(Registry::default());
9342 let forwarding = Arc::new(ForwardingTable::default());
9343 let handler =
9344 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
9345 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(39));
9346 hello_via_sink(
9347 &handler,
9348 &module_ctx,
9349 &mut module_rx,
9350 hello_frame("aft", PROTOCOL_VERSION, 7),
9351 )
9352 .await;
9353
9354 let project_root = unique_project_root("consumer-capabilities-absent");
9355 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(40));
9356 let route_handler = handler.clone();
9357 let route_task = tokio::spawn(async move {
9358 route_handler
9359 .handle_control_frame(&client_ctx, route_open_frame(402, "aft", project_root))
9360 .await
9361 .unwrap()
9362 });
9363 let bind_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
9364 .await
9365 .unwrap()
9366 .unwrap();
9367 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
9368 let ModuleControlRequest::RouteBind {
9369 consumer_capabilities,
9370 ..
9371 } = bind
9372 else {
9373 panic!("expected route.bind request, got {bind:?}");
9374 };
9375 assert_eq!(consumer_capabilities, None);
9376
9377 handler
9378 .handle_control_frame(&module_ctx, route_bind_ack(bind_frame.header.corr))
9379 .await
9380 .unwrap();
9381 let route_response = route_task.await.unwrap();
9382 assert!(route_response.is_empty());
9383 let published = client_rx.recv().await.unwrap();
9384 assert!(matches!(
9385 serde_json::from_slice::<ClientControlResponse>(&published.body).unwrap(),
9386 ClientControlResponse::RouteOpen { .. }
9387 ));
9388 }
9389
9390 async fn bind_role_versions_for(
9394 sent: Option<BTreeMap<String, String>>,
9395 connection: u64,
9396 ) -> Option<BTreeMap<String, String>> {
9397 let registry = Arc::new(Registry::default());
9398 let forwarding = Arc::new(ForwardingTable::default());
9399 let handler =
9400 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
9401 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(connection));
9402 hello_via_sink(
9403 &handler,
9404 &module_ctx,
9405 &mut module_rx,
9406 hello_frame("aft", PROTOCOL_VERSION, 7),
9407 )
9408 .await;
9409 let project_root = unique_project_root("role-versions");
9410 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(connection + 1));
9411 let route_handler = handler.clone();
9412 let route_task = tokio::spawn(async move {
9413 route_handler
9414 .handle_control_frame(
9415 &client_ctx,
9416 route_open_frame_with_role_versions(403, "aft", project_root, sent),
9417 )
9418 .await
9419 .unwrap()
9420 });
9421 let bind_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
9422 .await
9423 .expect("a well-formed route.open reaches the module as a bind")
9424 .unwrap();
9425 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
9426 let ModuleControlRequest::RouteBind { role_versions, .. } = bind else {
9427 panic!("expected route.bind request, got {bind:?}");
9428 };
9429 handler
9430 .handle_control_frame(&module_ctx, route_bind_ack(bind_frame.header.corr))
9431 .await
9432 .unwrap();
9433 assert!(route_task.await.unwrap().is_empty());
9434 let published = client_rx.recv().await.unwrap();
9435 assert!(matches!(
9436 serde_json::from_slice::<ClientControlResponse>(&published.body).unwrap(),
9437 ClientControlResponse::RouteOpen { .. }
9438 ));
9439 role_versions
9440 }
9441
9442 #[tokio::test]
9443 async fn route_open_relays_role_versions_verbatim() {
9444 let sent = role_versions(&[("tool-provider", "v1"), ("management-surface", "v12")]);
9445 assert_eq!(
9446 bind_role_versions_for(Some(sent.clone()), 141).await,
9447 Some(sent)
9448 );
9449 }
9450
9451 #[tokio::test]
9454 async fn route_open_with_empty_or_absent_role_versions_relays_none() {
9455 assert_eq!(bind_role_versions_for(None, 143).await, None);
9456 assert_eq!(
9457 bind_role_versions_for(Some(BTreeMap::new()), 145).await,
9458 None
9459 );
9460 }
9461
9462 #[tokio::test]
9463 async fn route_open_refuses_malformed_role_versions_before_any_bind() {
9464 let registry = Arc::new(Registry::default());
9465 let forwarding = Arc::new(ForwardingTable::default());
9466 let handler =
9467 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
9468 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(147));
9469 hello_via_sink(
9470 &handler,
9471 &module_ctx,
9472 &mut module_rx,
9473 hello_frame("aft", PROTOCOL_VERSION, 7),
9474 )
9475 .await;
9476
9477 let nine: BTreeMap<String, String> = (0..9)
9478 .map(|index| (format!("role-{index}"), "v1".to_string()))
9479 .collect();
9480 for (label, malformed) in [
9481 (
9482 "invalid role name",
9483 role_versions(&[("Tool_Provider", "v1")]),
9484 ),
9485 ("invalid version", role_versions(&[("tool-provider", "v0")])),
9486 ("nine entries", nine),
9487 ] {
9488 let responses = tokio::time::timeout(
9491 Duration::from_secs(1),
9492 handler.handle_control_frame(
9493 &route_ctx(ConnectionId::new(148)).0,
9494 route_open_frame_with_role_versions(
9495 404,
9496 "aft",
9497 unique_project_root("role-versions-malformed"),
9498 Some(malformed),
9499 ),
9500 ),
9501 )
9502 .await
9503 .unwrap_or_else(|_| panic!("{label}: the open was relayed instead of refused"))
9504 .unwrap();
9505 assert_eq!(responses.len(), 1, "{label}");
9506 assert_eq!(responses[0].header.ty, FrameType::Error, "{label}");
9507 let error = parse_error(&responses[0]);
9508 assert_eq!(error["code"], "invalid_request", "{label}: {error}");
9509 assert_eq!(
9510 error["detail"]["field"], "role_versions",
9511 "{label}: {error}"
9512 );
9513 assert!(
9514 !error_codes::is_retryable_route_open(error["code"].as_str().unwrap()),
9515 "{label}: a malformed declaration is terminal"
9516 );
9517 assert!(
9518 module_rx.try_recv().is_err(),
9519 "{label}: the module must never see a bind"
9520 );
9521 }
9522 }
9523
9524 #[tokio::test]
9528 async fn route_role_versions_capability_is_advertised() {
9529 let handler = ControlHandler::new(Arc::new(Registry::default()));
9530 let (ctx, mut rx) = route_ctx(ConnectionId::new(149));
9531 let ack = hello_via_sink(
9532 &handler,
9533 &ctx,
9534 &mut rx,
9535 hello_frame("m", PROTOCOL_VERSION, 1),
9536 )
9537 .await;
9538 let ack = parse_ack(&ack);
9539 assert!(
9540 ack.subc_capabilities
9541 .iter()
9542 .any(|c| c == "route-role-versions/v1"),
9543 "{:?}",
9544 ack.subc_capabilities
9545 );
9546
9547 let body = serde_json::to_vec(&ClientControlRequest::ServerDescribe {}).unwrap();
9548 let frame = Frame::build(FrameType::Request, control_flags(), 0, 0, 5, body).unwrap();
9549 let reply = handler
9550 .handle_control_frame(&route_ctx(ConnectionId::new(150)).0, frame)
9551 .await
9552 .unwrap()
9553 .pop()
9554 .unwrap();
9555 let ClientControlResponse::ServerDescribe { capabilities, .. } =
9556 serde_json::from_slice(&reply.body).unwrap()
9557 else {
9558 panic!("not a server.describe reply");
9559 };
9560 assert!(
9561 capabilities.iter().any(|c| c == CAP_ROUTE_ROLE_VERSIONS_V1),
9562 "{capabilities:?}"
9563 );
9564 }
9565
9566 #[tokio::test]
9567 async fn supervision_only_module_health_probe_does_not_enable_route_open_and_cleans_up() {
9568 let registry = Arc::new(Registry::default());
9569 let forwarding = Arc::new(ForwardingTable::default());
9570 let handler =
9571 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9572 .with_health_probe_timeout(Duration::from_secs(5));
9573 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(35));
9574 hello_via_sink(
9575 &handler,
9576 &module_ctx,
9577 &mut module_rx,
9578 non_routable_hello_frame_with_control_ops(
9579 "mcp",
9580 300,
9581 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
9582 ),
9583 )
9584 .await;
9585 assert!(registry
9586 .get_module("mcp")
9587 .unwrap()
9588 .unwrap()
9589 .manifest
9590 .provides
9591 .is_empty());
9592
9593 let (route_client_ctx, _route_client_rx) = route_ctx(ConnectionId::new(36));
9594 let route_response = handler
9595 .handle_control_frame(
9596 &route_client_ctx,
9597 route_open_frame(301, "mcp", unique_project_root("non-routable-mcp")),
9598 )
9599 .await
9600 .unwrap();
9601 assert_eq!(route_response[0].header.ty, FrameType::Error);
9602 assert_eq!(
9603 parse_error(&route_response[0])["code"],
9604 "target_unavailable"
9605 );
9606 assert!(parse_error(&route_response[0])["message"]
9607 .as_str()
9608 .unwrap()
9609 .contains("does not provide the requested target"));
9610 assert!(module_rx.try_recv().is_err());
9611
9612 let (health_client_ctx, _health_client_rx) = route_ctx(ConnectionId::new(37));
9613 let health_handler = handler.clone();
9614 let health_task = tokio::spawn(async move {
9615 health_handler
9616 .handle_control_frame(
9617 &health_client_ctx,
9618 supervisor_health_probe_frame(302, "mcp"),
9619 )
9620 .await
9621 .unwrap()
9622 });
9623 let health_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
9624 .await
9625 .unwrap()
9626 .unwrap();
9627 assert_eq!(
9628 serde_json::from_slice::<ModuleControlRequest>(&health_frame.body).unwrap(),
9629 ModuleControlRequest::HealthCheck {}
9630 );
9631 handler
9632 .handle_control_frame(
9633 &module_ctx,
9634 health_response(health_frame.header.corr, HealthStatus::Ok),
9635 )
9636 .await
9637 .unwrap();
9638 let health_response = health_task.await.unwrap();
9639 assert_eq!(health_response[0].header.ty, FrameType::Response);
9640 match serde_json::from_slice::<ClientControlResponse>(&health_response[0].body).unwrap() {
9641 ClientControlResponse::SupervisorHealthProbe {
9642 module_id, status, ..
9643 } => {
9644 assert_eq!(module_id, "mcp");
9645 assert_eq!(status, HealthStatus::Ok);
9646 }
9647 other => panic!("unexpected health response: {other:?}"),
9648 }
9649
9650 forwarding
9655 .cleanup_connection(module_ctx.connection_id)
9656 .unwrap();
9657 let (cleanup_probe_ctx, _cleanup_probe_rx) = route_ctx(ConnectionId::new(38));
9658 let cleanup_response = tokio::time::timeout(
9659 Duration::from_millis(200),
9660 handler.handle_control_frame(
9661 &cleanup_probe_ctx,
9662 supervisor_health_probe_frame(303, "mcp"),
9663 ),
9664 )
9665 .await
9666 .expect("probe should fail immediately when the control lane is gone")
9667 .unwrap();
9668 assert_eq!(cleanup_response[0].header.ty, FrameType::Error);
9669 assert_eq!(
9670 parse_error(&cleanup_response[0])["code"],
9671 "target_unavailable"
9672 );
9673 assert!(parse_error(&cleanup_response[0])["message"]
9674 .as_str()
9675 .unwrap()
9676 .contains("no module connection"));
9677
9678 handler
9679 .cleanup_connection(module_ctx.connection_id)
9680 .unwrap();
9681 }
9682
9683 #[tokio::test]
9684 async fn route_open_classifies_unregistered_running_supervised_module_as_warming() {
9685 let registry = Arc::new(Registry::default());
9686 let supervisor_handle = SupervisorHandle::new();
9687 let supervisor =
9688 Supervisor::new_for_test(Arc::clone(®istry), RestartPolicy::new(0, Duration::ZERO))
9689 .with_handle(supervisor_handle.clone())
9690 .with_connection_file_path(
9691 std::env::temp_dir()
9692 .join(format!("subc-route-open-warming-{}", std::process::id())),
9693 );
9694 let module = supervisor
9695 .supervise_configured(
9696 ModuleSpec {
9697 module_id: "warming".to_string(),
9698 program: fake_aft_stub_path(),
9699 args: Vec::new(),
9700 env: Vec::new(),
9701 reserved: false,
9702 reserved_prefixes: Vec::new(),
9703 protocol: ModuleProtocol::Subc,
9704 overlap: Default::default(),
9705 },
9706 true,
9707 )
9708 .unwrap();
9709 assert_eq!(module.state().unwrap(), ModuleState::Running);
9710
9711 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
9712 let (ctx, _rx) = route_ctx(ConnectionId::new(39));
9713 let response = handler
9714 .handle_control_frame(
9715 &ctx,
9716 route_open_frame(304, "warming", unique_project_root("warming")),
9717 )
9718 .await
9719 .unwrap();
9720 module.stop().await.unwrap();
9721
9722 assert_eq!(response[0].header.ty, FrameType::Error);
9723 let error = parse_error(&response[0]);
9724 assert_eq!(error["code"], "module_warming");
9725 assert!(error["message"]
9726 .as_str()
9727 .unwrap()
9728 .contains("state=running, enabled=true, live=false"));
9729 }
9730
9731 #[test]
9732 fn route_open_connection_cap_logs_admission_reason_and_capacity() {
9733 let handler = ControlHandler::new(Arc::new(Registry::default()));
9734 let capture = EventCapture::default();
9735 let _subscriber =
9736 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
9737 let (ctx, _rx) = route_ctx(ConnectionId::new(96));
9738 let limit = crate::server::MAX_PENDING_ROUTE_OPENS_PER_CONNECTION;
9739 let pending = (0..limit).collect::<Vec<_>>();
9740 let response = handler
9741 .route_open_capacity_refusal(
9742 &ctx,
9743 &route_open_frame(396, "busy", unique_project_root("connection-cap")),
9744 "busy",
9745 pending.len(),
9746 limit,
9747 )
9748 .unwrap();
9749 assert_eq!(parse_error(&response)["code"], "target_unavailable");
9750 let event = capture
9751 .events()
9752 .into_iter()
9753 .find(|event| {
9754 event.target == "control"
9755 && event.fields.get("reason") == Some(&"\"open_admission_full\"".to_string())
9756 })
9757 .expect("connection admission refusal event");
9758 assert_eq!(event.fields.get("in_flight"), Some(&limit.to_string()));
9759 assert_eq!(event.fields.get("limit"), Some(&limit.to_string()));
9760 }
9761
9762 #[test]
9763 fn route_open_target_cap_logs_admission_reason_and_capacity() {
9764 let handler = ControlHandler::new(Arc::new(Registry::default()));
9765 let capture = EventCapture::default();
9766 let _subscriber =
9767 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
9768 let (ctx, _rx) = route_ctx(ConnectionId::new(97));
9769 let limit = MAX_PENDING_ROUTE_BINDS_PER_TARGET;
9770 let guards = (0..limit)
9771 .map(|_| {
9772 handler
9773 .route_bind_concurrency
9774 .try_admit("busy", limit)
9775 .unwrap()
9776 })
9777 .collect::<Vec<_>>();
9778 let in_flight = match handler.route_bind_concurrency.try_admit("busy", limit) {
9779 Err(in_flight) => in_flight,
9780 Ok(_) => panic!("target cap must refuse after {limit} admissions"),
9781 };
9782 let response = handler
9783 .route_open_target_capacity_refusal(
9784 &ctx,
9785 &route_open_frame(397, "busy", unique_project_root("target-cap")),
9786 "busy",
9787 in_flight,
9788 )
9789 .unwrap();
9790 assert_eq!(parse_error(&response)["code"], "target_unavailable");
9791 let event = capture
9792 .events()
9793 .into_iter()
9794 .find(|event| {
9795 event.target == "control"
9796 && event.fields.get("reason") == Some(&"\"target_binds_full\"".to_string())
9797 })
9798 .expect("target admission refusal event");
9799 assert_eq!(event.fields.get("in_flight"), Some(&limit.to_string()));
9800 assert_eq!(event.fields.get("limit"), Some(&limit.to_string()));
9801 drop(guards);
9802 }
9803
9804 #[tokio::test]
9809 async fn route_open_refusal_names_the_check_that_refused() {
9810 let handler = ControlHandler::new(Arc::new(Registry::default()));
9811 let capture = EventCapture::default();
9812 let _subscriber =
9813 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
9814 let (ctx, _rx) = route_ctx(ConnectionId::new(95));
9815 let response = handler
9816 .handle_control_frame(
9817 &ctx,
9818 route_open_frame(395, "nobody", unique_project_root("refusal-reason")),
9819 )
9820 .await
9821 .unwrap();
9822
9823 assert_eq!(parse_error(&response[0])["code"], "unknown_module");
9824 let event = capture
9825 .events()
9826 .into_iter()
9827 .find(|event| {
9828 event.target == "control"
9829 && event.fields.get("code") == Some(&"\"unknown_module\"".to_string())
9830 })
9831 .expect("route.open refusal event");
9832 assert_eq!(
9833 event.fields.get("reason"),
9834 Some(&"\"not_registered\"".to_string())
9835 );
9836 }
9837
9838 #[tokio::test]
9839 async fn route_open_supervised_absence_emits_refusal_fields_and_counts_code() {
9840 let registry = Arc::new(Registry::default());
9841 let supervisor_handle = SupervisorHandle::new();
9842 let supervisor =
9843 Supervisor::new_for_test(Arc::clone(®istry), RestartPolicy::new(0, Duration::ZERO))
9844 .with_handle(supervisor_handle.clone())
9845 .with_connection_file_path(std::env::temp_dir().join(format!(
9846 "subc-route-open-refusal-info-{}",
9847 std::process::id()
9848 )));
9849 let module = supervisor
9850 .supervise_configured(
9851 ModuleSpec {
9852 module_id: "warming".to_string(),
9853 program: fake_aft_stub_path(),
9854 args: Vec::new(),
9855 env: Vec::new(),
9856 reserved: false,
9857 reserved_prefixes: Vec::new(),
9858 protocol: ModuleProtocol::Subc,
9859 overlap: Default::default(),
9860 },
9861 true,
9862 )
9863 .unwrap();
9864 assert_eq!(module.state().unwrap(), ModuleState::Running);
9865
9866 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
9867 assert!(handler
9868 .counters()
9869 .snapshot()
9870 .get("route_open_refused_by_code")
9871 .is_none());
9872 let capture = EventCapture::default();
9873 let _subscriber =
9874 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
9875 let (ctx, _rx) = route_ctx(ConnectionId::new(94));
9876 let response = handler
9877 .handle_control_frame(
9878 &ctx,
9879 route_open_frame(394, "warming", unique_project_root("refusal-info")),
9880 )
9881 .await
9882 .unwrap();
9883 module.stop().await.unwrap();
9884
9885 assert_eq!(parse_error(&response[0])["code"], "module_warming");
9886 let event = capture
9887 .events()
9888 .into_iter()
9889 .find(|event| {
9890 event.target == "control"
9891 && event.fields.get("code") == Some(&"\"module_warming\"".to_string())
9892 })
9893 .expect("route.open refusal event");
9894 assert_eq!(
9895 event.fields.get("module_id"),
9896 Some(&"\"warming\"".to_string())
9897 );
9898 assert_eq!(event.fields.get("connection_id"), Some(&"94".to_string()));
9899 assert_eq!(
9900 event.fields.get("reason"),
9901 Some(&"\"supervised_not_registered\"".to_string())
9902 );
9903 assert_eq!(event.fields.get("state"), Some(&"running".to_string()));
9904 assert_eq!(event.fields.get("enabled"), Some(&"true".to_string()));
9905 assert_eq!(event.fields.get("live"), Some(&"false".to_string()));
9906 assert_eq!(
9907 handler.counters().snapshot()["route_open_refused_by_code"],
9908 json!({ "module_warming": 1 })
9909 );
9910 }
9911
9912 const OUTAGE_START: &str = "route.open refusing module: not serving";
9913 const OUTAGE_RECOVERED: &str = "route.open accepted again after module outage";
9914
9915 fn outage_lines(capture: &EventCapture, message: &str) -> Vec<CapturedEvent> {
9916 capture
9917 .events()
9918 .into_iter()
9919 .filter(|event| event.fields.get("message").map(String::as_str) == Some(message))
9920 .collect()
9921 }
9922
9923 fn supervise_stub(
9924 registry: &Arc<Registry>,
9925 module_id: &str,
9926 enabled: bool,
9927 ) -> (SupervisorHandle, crate::supervise::SupervisedModule) {
9928 let supervisor_handle = SupervisorHandle::new();
9929 let supervisor =
9930 Supervisor::new_for_test(Arc::clone(registry), RestartPolicy::new(0, Duration::ZERO))
9931 .with_handle(supervisor_handle.clone())
9932 .with_connection_file_path(std::env::temp_dir().join(format!(
9933 "subc-route-outage-{module_id}-{}",
9934 std::process::id()
9935 )));
9936 let module = supervisor
9937 .supervise_configured(
9938 ModuleSpec {
9939 module_id: module_id.to_string(),
9940 program: fake_aft_stub_path(),
9941 args: Vec::new(),
9942 env: Vec::new(),
9943 reserved: false,
9944 reserved_prefixes: Vec::new(),
9945 protocol: ModuleProtocol::Subc,
9946 overlap: Default::default(),
9947 },
9948 enabled,
9949 )
9950 .unwrap();
9951 (supervisor_handle, module)
9952 }
9953
9954 fn supervisor_restart_frame(corr: u64, module_id: &str) -> Frame {
9955 let body = serde_json::to_vec(&ClientControlRequest::SupervisorRestart {
9956 module_id: module_id.to_string(),
9957 drain_timeout_ms: Some(50),
9958 })
9959 .unwrap();
9960 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
9961 }
9962
9963 #[test]
9967 fn handlers_over_one_forwarding_table_share_the_outage_tracker() {
9968 let registry = Arc::new(Registry::default());
9969 let forwarding = Arc::new(ForwardingTable::default());
9970 let first = ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
9971 let second = ControlHandler::with_forwarding(registry, forwarding);
9972 assert!(Arc::ptr_eq(&first.route_outages, &second.route_outages));
9973 }
9974
9975 #[tokio::test(flavor = "current_thread")]
9979 async fn route_open_unknown_module_refusals_add_no_outage_state() {
9980 let handler = ControlHandler::new(Arc::new(Registry::default()));
9981 let capture = EventCapture::default();
9982 let _subscriber =
9983 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
9984 let (ctx, _rx) = route_ctx(ConnectionId::new(90));
9985 for corr in 0..8 {
9986 let response = handler
9987 .handle_control_frame(
9988 &ctx,
9989 route_open_frame(
9990 380 + corr,
9991 &format!("nobody-{corr}"),
9992 unique_project_root("outage-unknown"),
9993 ),
9994 )
9995 .await
9996 .unwrap();
9997 assert_eq!(parse_error(&response[0])["code"], "unknown_module");
9998 }
9999
10000 assert_eq!(handler.route_outages.tracked_module_count(), 0);
10001 assert!(outage_lines(&capture, OUTAGE_START).is_empty());
10002 assert!(outage_lines(&capture, OUTAGE_RECOVERED).is_empty());
10003 }
10004
10005 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
10010 async fn route_open_outage_level_separates_operator_restart_from_unexplained() {
10011 let registry = Arc::new(Registry::default());
10012 let (supervisor_handle, module) = supervise_stub(®istry, "outage-restart", true);
10013 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
10014 let capture = EventCapture::default();
10015 let _subscriber =
10016 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
10017 let (ctx, _rx) = route_ctx(ConnectionId::new(91));
10018 handler.route_outages.record_accepted("outage-restart");
10021
10022 let response = handler
10023 .handle_control_frame(
10024 &ctx,
10025 route_open_frame(391, "outage-restart", unique_project_root("outage-a")),
10026 )
10027 .await
10028 .unwrap();
10029 assert_eq!(response[0].header.ty, FrameType::Error);
10030 let starts = outage_lines(&capture, OUTAGE_START);
10031 assert_eq!(starts.len(), 1, "{starts:?}");
10032 assert_eq!(starts[0].level, tracing::Level::WARN);
10033 assert_eq!(starts[0].fields["initiated_by"], "\"unexplained\"");
10034 assert_eq!(starts[0].fields["reason"], "\"supervised_not_registered\"");
10035 assert_eq!(starts[0].fields["module_id"], "\"outage-restart\"");
10036 handler.route_outages.record_accepted("outage-restart");
10037 assert_eq!(outage_lines(&capture, OUTAGE_RECOVERED).len(), 1);
10038
10039 let restart = handler
10040 .handle_control_frame(&ctx, supervisor_restart_frame(392, "outage-restart"))
10041 .await
10042 .unwrap();
10043 assert_eq!(
10044 restart[0].header.ty,
10045 FrameType::Response,
10046 "{:?}",
10047 parse_error(&restart[0])
10048 );
10049 handler
10050 .handle_control_frame(
10051 &ctx,
10052 route_open_frame(393, "outage-restart", unique_project_root("outage-b")),
10053 )
10054 .await
10055 .unwrap();
10056 module.stop().await.unwrap();
10057
10058 let starts = outage_lines(&capture, OUTAGE_START);
10059 assert_eq!(starts.len(), 2, "{starts:?}");
10060 assert_eq!(starts[1].level, tracing::Level::INFO);
10061 assert_eq!(starts[1].fields["initiated_by"], "\"operator\"");
10062 }
10063
10064 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
10068 async fn failed_operator_restart_leaves_no_operator_mark() {
10069 let registry = Arc::new(Registry::default());
10070 let (supervisor_handle, _module) = supervise_stub(®istry, "outage-disabled", false);
10071 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
10072 let capture = EventCapture::default();
10073 let _subscriber =
10074 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
10075 let (ctx, _rx) = route_ctx(ConnectionId::new(92));
10076 handler.route_outages.record_accepted("outage-disabled");
10077
10078 let restart = handler
10079 .handle_control_frame(&ctx, supervisor_restart_frame(394, "outage-disabled"))
10080 .await
10081 .unwrap();
10082 assert_eq!(parse_error(&restart[0])["code"], "module_disabled");
10083 assert!(!handler.route_outages.has_operator_mark("outage-disabled"));
10084
10085 handler
10086 .handle_control_frame(
10087 &ctx,
10088 route_open_frame(395, "outage-disabled", unique_project_root("outage-c")),
10089 )
10090 .await
10091 .unwrap();
10092 let starts = outage_lines(&capture, OUTAGE_START);
10093 assert_eq!(starts.len(), 1, "{starts:?}");
10094 assert_eq!(starts[0].level, tracing::Level::WARN);
10095 }
10096
10097 #[tokio::test(flavor = "current_thread")]
10098 async fn route_open_unknown_module_escapes_target_module_id() {
10099 let handler = ControlHandler::new(Arc::new(Registry::default()));
10100 let capture = EventCapture::default();
10101 let _subscriber =
10102 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
10103 let hostile_module_id = "\u{1b}]52;c;AAAA\u{07}";
10104 let (ctx, _rx) = route_ctx(ConnectionId::new(95));
10105 let response = handler
10106 .handle_control_frame(
10107 &ctx,
10108 route_open_frame(
10109 395,
10110 hostile_module_id,
10111 unique_project_root("hostile-target-module-id"),
10112 ),
10113 )
10114 .await
10115 .unwrap();
10116
10117 assert_eq!(parse_error(&response[0])["code"], "unknown_module");
10118 let event = capture
10119 .events()
10120 .into_iter()
10121 .find(|event| {
10122 event.target == "control"
10123 && event.fields.get("code") == Some(&"\"unknown_module\"".to_string())
10124 })
10125 .expect("route.open unknown-module refusal event");
10126 let logged = event.fields.get("module_id").expect("module_id field");
10127 assert!(!logged.bytes().any(|byte| byte < 0x20));
10128 assert_eq!(logged, r#""\u{1b}]52;c;AAAA\u{7}""#);
10129 }
10130
10131 #[tokio::test(flavor = "current_thread")]
10132 async fn route_open_module_rejection_uses_daemon_counter_key() {
10133 let registry = Arc::new(Registry::default());
10134 let forwarding = Arc::new(ForwardingTable::default());
10135 let handler =
10136 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
10137 let module_connection = ConnectionId::new(95);
10138 let (module_ctx, mut module_rx) = route_ctx(module_connection);
10139 hello_via_sink(
10140 &handler,
10141 &module_ctx,
10142 &mut module_rx,
10143 hello_frame("aft", PROTOCOL_VERSION, 395),
10144 )
10145 .await;
10146
10147 let client_connection = ConnectionId::new(96);
10148 let (client_ctx, _client_rx) = route_ctx(client_connection);
10149 let capture = EventCapture::default();
10150 let _subscriber =
10151 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
10152 let (route_task, bind) = relay_route_open(
10153 &handler,
10154 client_connection,
10155 &client_ctx.egress,
10156 &mut module_rx,
10157 396,
10158 "aft",
10159 "hostile-module-code",
10160 )
10161 .await;
10162 let hostile_code = "\u{1b}]52;c;AAAA\u{07}";
10163 let rejection = Frame::build(
10164 FrameType::Error,
10165 control_flags(),
10166 0,
10167 0,
10168 bind.header.corr,
10169 serde_json::to_vec(&ErrorBody::new(hostile_code, "module refused route.bind")).unwrap(),
10170 )
10171 .unwrap();
10172 handler
10173 .handle_control_frame(&module_ctx, rejection)
10174 .await
10175 .unwrap();
10176
10177 let response = route_task.await.unwrap();
10178 assert_eq!(parse_error(&response[0])["code"], hostile_code);
10179 let counters = handler.counters().snapshot();
10180 assert_eq!(
10181 counters["route_open_refused_by_code"],
10182 json!({ "module_rejected": 1 })
10183 );
10184 assert!(counters["route_open_refused_by_code"]
10185 .get(hostile_code)
10186 .is_none());
10187
10188 let event = capture
10189 .events()
10190 .into_iter()
10191 .find(|event| {
10192 event.target == "control"
10193 && event.fields.get("code") == Some(&"\"module_rejected\"".to_string())
10194 })
10195 .expect("route.open module-rejection refusal event");
10196 let logged = event.fields.get("module_code").expect("module_code field");
10197 assert!(!logged.bytes().any(|byte| byte < 0x20));
10198 assert_eq!(logged, r#""\u{1b}]52;c;AAAA\u{7}""#);
10199 }
10200
10201 #[tokio::test]
10202 async fn route_open_keeps_failed_unregistered_supervised_module_unavailable() {
10203 let registry = Arc::new(Registry::default());
10204 let supervisor_handle = SupervisorHandle::new();
10205 let missing_program = std::env::temp_dir().join(format!(
10206 "subc-route-open-missing-program-{}",
10207 std::process::id()
10208 ));
10209 let supervisor =
10210 Supervisor::new_for_test(Arc::clone(®istry), RestartPolicy::new(0, Duration::ZERO))
10211 .with_handle(supervisor_handle.clone());
10212 let module = supervisor
10213 .supervise_configured(
10214 ModuleSpec {
10215 module_id: "failed".to_string(),
10216 program: missing_program,
10217 args: Vec::new(),
10218 env: Vec::new(),
10219 reserved: false,
10220 reserved_prefixes: Vec::new(),
10221 protocol: ModuleProtocol::Subc,
10222 overlap: Default::default(),
10223 },
10224 true,
10225 )
10226 .unwrap();
10227 assert_eq!(module.state().unwrap(), ModuleState::Failed);
10228
10229 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
10230 let (ctx, _rx) = route_ctx(ConnectionId::new(40));
10231 let response = handler
10232 .handle_control_frame(
10233 &ctx,
10234 route_open_frame(305, "failed", unique_project_root("failed")),
10235 )
10236 .await
10237 .unwrap();
10238
10239 assert_eq!(response[0].header.ty, FrameType::Error);
10240 let error = parse_error(&response[0]);
10241 assert_eq!(error["code"], "target_unavailable");
10242 assert!(error["message"]
10243 .as_str()
10244 .unwrap()
10245 .contains("state=failed, enabled=true, live=false"));
10246 }
10247
10248 #[tokio::test]
10249 async fn route_open_role_mismatch_remains_target_unavailable() {
10250 let registry = Arc::new(Registry::default());
10251 let handler = ControlHandler::new(Arc::clone(®istry));
10252 handler
10253 .handle_control(
10254 ConnectionId::new(41),
10255 non_routable_hello_frame_with_control_ops("health-only", 306, None),
10256 )
10257 .unwrap();
10258
10259 let (ctx, _rx) = route_ctx(ConnectionId::new(42));
10260 let response = handler
10261 .handle_control_frame(
10262 &ctx,
10263 route_open_frame(307, "health-only", unique_project_root("role-mismatch")),
10264 )
10265 .await
10266 .unwrap();
10267
10268 assert_eq!(parse_error(&response[0])["code"], "target_unavailable");
10269 assert!(parse_error(&response[0])["message"]
10270 .as_str()
10271 .unwrap()
10272 .contains("does not provide the requested target"));
10273 }
10274
10275 #[tokio::test]
10276 async fn route_open_inactive_registration_remains_target_unavailable() {
10277 let registry = Arc::new(Registry::default());
10278 let handler = ControlHandler::new(Arc::clone(®istry));
10279 handler
10280 .handle_control(
10281 ConnectionId::new(43),
10282 hello_frame("inactive", PROTOCOL_VERSION, 308),
10283 )
10284 .unwrap();
10285 assert!(registry
10286 .set_module_state_for_test("inactive", ChannelState::Closed)
10287 .unwrap());
10288
10289 let (ctx, _rx) = route_ctx(ConnectionId::new(44));
10290 let response = handler
10291 .handle_control_frame(
10292 &ctx,
10293 route_open_frame(309, "inactive", unique_project_root("inactive")),
10294 )
10295 .await
10296 .unwrap();
10297
10298 assert_eq!(parse_error(&response[0])["code"], "target_unavailable");
10299 assert!(parse_error(&response[0])["message"]
10300 .as_str()
10301 .unwrap()
10302 .contains("is not active"));
10303 }
10304
10305 #[tokio::test]
10306 async fn late_health_reply_is_recorded_through_the_module_response_path() {
10307 let registry = Arc::new(Registry::default());
10308 let forwarding = Arc::new(ForwardingTable::default());
10309 let supervisor_handle = SupervisorHandle::new();
10310 let supervisor =
10311 Supervisor::new_for_test(Arc::clone(®istry), crate::RestartPolicy::default())
10312 .with_forwarding(Arc::clone(&forwarding))
10313 .with_handle(supervisor_handle.clone());
10314 let module = supervisor
10315 .supervise_configured(
10316 crate::ModuleSpec {
10317 module_id: "late-health-response".to_string(),
10318 program: PathBuf::from("disabled-module"),
10319 args: Vec::new(),
10320 env: Vec::new(),
10321 reserved: false,
10322 reserved_prefixes: Vec::new(),
10323 protocol: ModuleProtocol::Subc,
10324 overlap: Default::default(),
10325 },
10326 false,
10327 )
10328 .unwrap();
10329 let handler =
10330 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
10331 .with_supervisor(supervisor_handle);
10332 let (module_ctx, _module_rx) = route_ctx(ConnectionId::new(39));
10333 handler
10334 .handle_control_frame(
10335 &module_ctx,
10336 hello_frame_with_control_ops(
10337 "late-health-response",
10338 PROTOCOL_VERSION,
10339 7,
10340 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
10341 ),
10342 )
10343 .await
10344 .unwrap();
10345 let probe_started_at = Instant::now() - Duration::from_millis(80);
10346 let pending = forwarding
10347 .begin_health_probe_rpc_for(
10348 "late-health-response",
10349 MODULE_CONTROL_OP_HEALTH_CHECK,
10350 probe_started_at,
10351 Instant::now() - Duration::from_millis(1),
10352 )
10353 .unwrap();
10354 assert!(forwarding
10355 .tombstone_health_probe_rpc(pending.endpoint, pending.corr)
10356 .unwrap());
10357
10358 let responses = handler
10359 .handle_control_frame(&module_ctx, health_response(pending.corr, HealthStatus::Ok))
10360 .await
10361 .unwrap();
10362
10363 assert!(responses.is_empty());
10364 let health = module.status().unwrap().health;
10365 assert_eq!(health.late_answer_count, 1);
10366 assert!(health.last_late_answer_latency_ms.unwrap() >= 80);
10367 }
10368
10369 #[tokio::test]
10370 async fn health_probe_timeout_and_module_death_are_typed() {
10371 let registry = Arc::new(Registry::default());
10372 let forwarding = Arc::new(ForwardingTable::default());
10373 let handler =
10374 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
10375 .with_health_probe_timeout(Duration::from_millis(50));
10376 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(40));
10377 hello_via_sink(
10378 &handler,
10379 &module_ctx,
10380 &mut module_rx,
10381 hello_frame_with_control_ops(
10382 "aft",
10383 PROTOCOL_VERSION,
10384 7,
10385 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
10386 ),
10387 )
10388 .await;
10389
10390 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(41));
10391 let responses = handler
10392 .handle_control_frame(&client_ctx, supervisor_health_probe_frame(201, "aft"))
10393 .await
10394 .unwrap();
10395 assert_eq!(responses[0].header.ty, FrameType::Error);
10396 assert_eq!(parse_error(&responses[0])["code"], "module_timeout");
10397 let _ = module_rx.try_recv();
10398
10399 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(42));
10400 let health_handler = handler.clone();
10401 let death_task = tokio::spawn(async move {
10402 health_handler
10403 .handle_control_frame(&client_ctx, supervisor_health_probe_frame(202, "aft"))
10404 .await
10405 .unwrap()
10406 });
10407 tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
10408 .await
10409 .unwrap()
10410 .unwrap();
10411 handler
10412 .cleanup_connection(module_ctx.connection_id)
10413 .unwrap();
10414 let responses = death_task.await.unwrap();
10415 assert_eq!(responses[0].header.ty, FrameType::Error);
10416 assert_eq!(parse_error(&responses[0])["code"], "target_unavailable");
10417 }
10418
10419 #[test]
10420 fn hello_requires_exact_protocol_version() {
10421 for (connection, offered) in [(1, PROTOCOL_VERSION - 1), (2, PROTOCOL_VERSION + 1)] {
10422 let registry = Arc::new(Registry::default());
10423 let handler = ControlHandler::new(Arc::clone(®istry));
10424 let responses = handler
10425 .handle_control(
10426 ConnectionId::new(connection),
10427 hello_frame("aft", offered, 9),
10428 )
10429 .unwrap();
10430
10431 assert_eq!(responses.len(), 1);
10432 assert_eq!(responses[0].header.ty, FrameType::Error);
10433 let error = parse_error(&responses[0]);
10434 assert_eq!(error["code"], "version_unsupported");
10435 assert!(registry.get_module("aft").unwrap().is_none());
10436 assert_eq!(registry.active_registration_count().unwrap(), 0);
10437 }
10438 }
10439
10440 #[test]
10441 fn unknown_module_push_op_is_ignored_but_malformed_known_op_errors() {
10442 let registry = Arc::new(Registry::default());
10443 let forwarding = Arc::new(ForwardingTable::default());
10444 let handler =
10445 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
10446 let module_connection = ConnectionId::new(301);
10447 let registration = registry
10448 .register_with_control_ops(
10449 manifest("aft-push", PROTOCOL_VERSION),
10450 PROTOCOL_VERSION,
10451 module_connection,
10452 module_baseline_control_ops(),
10453 )
10454 .unwrap();
10455 let (module_tx, _module_rx) = mpsc::channel(8);
10456 let endpoint = forwarding
10457 .register_module_connection(
10458 module_connection,
10459 "aft-push".to_string(),
10460 PROTOCOL_VERSION,
10461 manifest_concurrency(®istration.manifest),
10462 FrameSink::new(module_tx),
10463 )
10464 .unwrap();
10465
10466 let unknown = Frame::build(
10468 FrameType::Push,
10469 control_flags(),
10470 0,
10471 0,
10472 5,
10473 serde_json::to_vec(&json!({"op": "route.future.v2", "extra": 1})).unwrap(),
10474 )
10475 .unwrap();
10476 let out = handler.handle_status_update(endpoint, unknown).unwrap();
10477 assert!(
10478 out.is_empty(),
10479 "unknown push op must be ignored, got {out:?}"
10480 );
10481
10482 let malformed = Frame::build(
10484 FrameType::Push,
10485 control_flags(),
10486 0,
10487 0,
10488 6,
10489 serde_json::to_vec(&json!({"op": "route.status"})).unwrap(),
10490 )
10491 .unwrap();
10492 let out = handler.handle_status_update(endpoint, malformed).unwrap();
10493 assert_eq!(out.len(), 1);
10494 assert_eq!(out[0].header.ty, FrameType::Error);
10495 assert_eq!(parse_error(&out[0])["code"], "invalid_control_body");
10496 }
10497
10498 #[test]
10499 fn hello_rejected_when_connection_already_owns_client_routes() {
10500 let registry = Arc::new(Registry::default());
10501 let forwarding = Arc::new(ForwardingTable::default());
10502 let handler =
10503 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
10504 let _ = bind_liveness_route(®istry, &forwarding, "aft-module");
10506 let client_connection = ConnectionId::new(202);
10507
10508 let responses = handler
10511 .handle_control(
10512 client_connection,
10513 hello_frame("aft-second", PROTOCOL_VERSION, 9),
10514 )
10515 .unwrap();
10516 assert_eq!(responses[0].header.ty, FrameType::Error);
10517 assert_eq!(parse_error(&responses[0])["code"], "invalid_hello");
10518 assert!(registry.get_module("aft-second").unwrap().is_none());
10519 }
10520
10521 #[tokio::test]
10522 async fn second_hello_preserves_registration_routes_and_launch_nonce() {
10523 let registry = Arc::new(Registry::default());
10524 let forwarding = Arc::new(ForwardingTable::default());
10525 let handler = ControlHandler::with_forwarding(registry.clone(), forwarding.clone());
10526 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(101));
10527 hello_via_sink(
10528 &handler,
10529 &module_ctx,
10530 &mut module_rx,
10531 hello_frame_with_nonce("alpha", PROTOCOL_VERSION, 1, Some("alpha-nonce")),
10532 )
10533 .await;
10534 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(202));
10535 let pending = forwarding
10536 .begin_route_bind_relay_for_test(
10537 client_ctx.connection_id,
10538 client_ctx.egress.clone(),
10539 2,
10540 "alpha",
10541 )
10542 .unwrap();
10543 forwarding
10544 .complete_pending_relay(
10545 module_ctx.connection_id,
10546 pending.corr,
10547 RouteBindRelayOutcome::Accepted,
10548 )
10549 .unwrap();
10550 client_rx.try_recv().unwrap();
10551 for module_id in ["beta", "alpha"] {
10552 let replies = handler
10553 .handle_control_frame(
10554 &module_ctx,
10555 hello_frame_with_nonce(module_id, PROTOCOL_VERSION, 3, Some("replacement")),
10556 )
10557 .await
10558 .unwrap();
10559 assert_eq!(replies.len(), 1, "second HELLO must be refused");
10560 assert_eq!(parse_error(&replies[0])["code"], "invalid_hello");
10561 }
10562 assert_eq!(registry.list_modules().unwrap().1.len(), 1);
10563 assert!(registry.get_module("beta").unwrap().is_none());
10564 assert!(matches!(
10565 forwarding
10566 .lookup_data_route(
10567 client_ctx.connection_id,
10568 pending.client_channel,
10569 pending.client_epoch,
10570 )
10571 .unwrap(),
10572 DataRoute::Client(DataRouteState::Bound(_))
10573 ));
10574 assert!(handler
10575 .hello_launch_nonces
10576 .lock()
10577 .unwrap()
10578 .presented(module_ctx.connection_id, Some("alpha-nonce")));
10579 assert!(module_rx.try_recv().is_err());
10580 }
10581
10582 #[test]
10583 fn reserved_module_hello_requires_matching_launch_nonce() {
10584 let registry = Arc::new(Registry::default());
10585 let supervisor = SupervisorHandle::new();
10586 supervisor.set_reserved_nonce("vault", "the-real-nonce".to_string());
10589 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor);
10590
10591 let no_nonce = handler
10593 .handle_control(
10594 ConnectionId::new(1),
10595 hello_frame("vault", PROTOCOL_VERSION, 1),
10596 )
10597 .unwrap();
10598 assert_eq!(no_nonce[0].header.ty, FrameType::Error);
10599 assert_eq!(parse_error(&no_nonce[0])["code"], "reserved_module");
10600 assert!(registry.get_module("vault").unwrap().is_none());
10601
10602 let wrong = handler
10604 .handle_control(
10605 ConnectionId::new(2),
10606 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 2, Some("forged")),
10607 )
10608 .unwrap();
10609 assert_eq!(wrong[0].header.ty, FrameType::Error);
10610 assert_eq!(parse_error(&wrong[0])["code"], "reserved_module");
10611 assert!(registry.get_module("vault").unwrap().is_none());
10612
10613 let ok = handler
10615 .handle_control(
10616 ConnectionId::new(3),
10617 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 3, Some("the-real-nonce")),
10618 )
10619 .unwrap();
10620 assert_eq!(ok[0].header.ty, FrameType::HelloAck);
10621 assert!(registry.get_module("vault").unwrap().is_some());
10622 }
10623
10624 #[test]
10625 fn reserved_prefix_hello_uses_delimiter_sensitive_owner_nonce() {
10626 let registry = Arc::new(Registry::default());
10627 let supervisor = SupervisorHandle::new();
10628 supervisor.set_spawn_nonce("federation", "owner-nonce".to_string());
10629 supervisor.set_reserved_prefixes("federation", &["fed:".to_string()]);
10630 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor);
10631
10632 let squat = handler
10633 .handle_control(
10634 ConnectionId::new(1),
10635 hello_frame("fed:peerA:tool", PROTOCOL_VERSION, 1),
10636 )
10637 .unwrap();
10638 assert_eq!(squat[0].header.ty, FrameType::Error);
10639 assert_eq!(parse_error(&squat[0])["code"], "reserved_module");
10640 assert!(parse_error(&squat[0])["message"]
10641 .as_str()
10642 .unwrap()
10643 .contains("fed:"));
10644
10645 let accepted_peer = handler
10646 .handle_control(
10647 ConnectionId::new(2),
10648 hello_frame_with_nonce("fed:peerA:tool", PROTOCOL_VERSION, 2, Some("owner-nonce")),
10649 )
10650 .unwrap();
10651 assert_eq!(accepted_peer[0].header.ty, FrameType::HelloAck);
10652
10653 let accepted_short = handler
10654 .handle_control(
10655 ConnectionId::new(3),
10656 hello_frame_with_nonce("fed:x", PROTOCOL_VERSION, 3, Some("owner-nonce")),
10657 )
10658 .unwrap();
10659 assert_eq!(accepted_short[0].header.ty, FrameType::HelloAck);
10660
10661 for (conn, module_id) in [(4, "fedx:tool"), (5, "fed"), (6, "FED:x")] {
10662 let response = handler
10663 .handle_control(
10664 ConnectionId::new(conn),
10665 hello_frame(module_id, PROTOCOL_VERSION, conn),
10666 )
10667 .unwrap();
10668 assert_eq!(response[0].header.ty, FrameType::HelloAck, "{module_id}");
10669 }
10670 }
10671
10672 #[test]
10673 fn exact_reserved_module_takes_precedence_over_reserved_prefix() {
10674 let registry = Arc::new(Registry::default());
10675 let supervisor = SupervisorHandle::new();
10676 supervisor.set_spawn_nonce("federation", "owner-nonce".to_string());
10677 supervisor.set_reserved_prefixes("federation", &["fed:".to_string()]);
10678 supervisor.set_reserved_nonce("fed:special", "exact-nonce".to_string());
10679 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor);
10680
10681 let owner_nonce = handler
10682 .handle_control(
10683 ConnectionId::new(1),
10684 hello_frame_with_nonce("fed:special", PROTOCOL_VERSION, 1, Some("owner-nonce")),
10685 )
10686 .unwrap();
10687 assert_eq!(owner_nonce[0].header.ty, FrameType::Error);
10688 assert_eq!(parse_error(&owner_nonce[0])["code"], "reserved_module");
10689 assert!(registry.get_module("fed:special").unwrap().is_none());
10690
10691 let exact_nonce = handler
10692 .handle_control(
10693 ConnectionId::new(2),
10694 hello_frame_with_nonce("fed:special", PROTOCOL_VERSION, 2, Some("exact-nonce")),
10695 )
10696 .unwrap();
10697 assert_eq!(exact_nonce[0].header.ty, FrameType::HelloAck);
10698 assert!(registry.get_module("fed:special").unwrap().is_some());
10699 }
10700
10701 #[test]
10702 fn non_reserved_module_ignores_launch_nonce() {
10703 let registry = Arc::new(Registry::default());
10704 let handler = ControlHandler::new(Arc::clone(®istry));
10707 let no_nonce = handler
10708 .handle_control(
10709 ConnectionId::new(1),
10710 hello_frame("aft-no-nonce", PROTOCOL_VERSION, 1),
10711 )
10712 .unwrap();
10713 assert_eq!(no_nonce[0].header.ty, FrameType::HelloAck);
10714 assert!(registry.get_module("aft-no-nonce").unwrap().is_some());
10715
10716 let echoed_nonce = handler
10717 .handle_control(
10718 ConnectionId::new(2),
10719 hello_frame_with_nonce("aft-with-nonce", PROTOCOL_VERSION, 2, Some("spawn-nonce")),
10720 )
10721 .unwrap();
10722 assert_eq!(echoed_nonce[0].header.ty, FrameType::HelloAck);
10723 assert!(registry.get_module("aft-with-nonce").unwrap().is_some());
10724 }
10725
10726 #[test]
10727 fn malformed_hello_returns_error_and_handler_still_answers_ping() {
10728 let handler = ControlHandler::default();
10729 let conn = ConnectionId::new(1);
10730 let malformed = Frame::build(
10731 FrameType::Hello,
10732 control_flags(),
10733 0,
10734 0,
10735 3,
10736 b"{not json".to_vec(),
10737 )
10738 .unwrap();
10739
10740 let error = handler.handle_control(conn, malformed).unwrap();
10741 assert_eq!(error[0].header.ty, FrameType::Error);
10742 assert_eq!(parse_error(&error[0])["code"], "invalid_hello");
10743
10744 let ping = Frame::build(FrameType::Ping, control_flags(), 0, 0, 4, Vec::new()).unwrap();
10745 let pong = handler.handle_control(conn, ping).unwrap();
10746 assert_eq!(pong[0].header.ty, FrameType::Pong);
10747 assert_eq!(pong[0].header.corr, 4);
10748 }
10749
10750 #[test]
10751 fn duplicate_module_id_is_rejected_without_replacing_active_registration() {
10752 let registry = Arc::new(Registry::default());
10753 let handler = ControlHandler::new(Arc::clone(®istry));
10754
10755 handler
10756 .handle_control(
10757 ConnectionId::new(1),
10758 hello_frame("aft", PROTOCOL_VERSION, 1),
10759 )
10760 .unwrap();
10761 let duplicate = handler
10762 .handle_control(
10763 ConnectionId::new(2),
10764 hello_frame("aft", PROTOCOL_VERSION, 2),
10765 )
10766 .unwrap();
10767
10768 assert_eq!(duplicate[0].header.ty, FrameType::Error);
10769 assert_eq!(parse_error(&duplicate[0])["code"], "duplicate_module_id");
10770 let registration = registry.get_module("aft").unwrap().unwrap();
10771 assert_eq!(registration.connection_id, ConnectionId::new(1));
10772 }
10773
10774 #[test]
10775 fn liveness_poll_reports_false_when_process_liveness_reports_dead() {
10776 let registry = Arc::new(Registry::default());
10777 let forwarding = Arc::new(ForwardingTable::default());
10778 let process_liveness = Arc::new(FakeProcessLiveness { live: Some(false) });
10779 let handler =
10780 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
10781 .with_process_liveness(process_liveness);
10782 let (ctx, route_channel, route_epoch) =
10783 bind_liveness_route(®istry, &forwarding, "aft-dead");
10784 let responses = handler
10785 .handle_route_poll(
10786 &ctx,
10787 route_poll_frame(41, PollKind::Liveness, route_channel),
10788 route_channel,
10789 route_epoch,
10790 PollKind::Liveness,
10791 )
10792 .unwrap();
10793
10794 assert_eq!(responses.len(), 1);
10795 assert_eq!(responses[0].header.ty, FrameType::Response);
10796 assert_route_poll_liveness(&responses[0], false);
10797 }
10798
10799 #[test]
10800 fn liveness_poll_without_process_source_uses_bound_route() {
10801 let registry = Arc::new(Registry::default());
10802 let forwarding = Arc::new(ForwardingTable::default());
10803 let handler =
10804 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
10805 let (ctx, route_channel, route_epoch) =
10806 bind_liveness_route(®istry, &forwarding, "aft-bound-only");
10807 let responses = handler
10808 .handle_route_poll(
10809 &ctx,
10810 route_poll_frame(42, PollKind::Liveness, route_channel),
10811 route_channel,
10812 route_epoch,
10813 PollKind::Liveness,
10814 )
10815 .unwrap();
10816
10817 assert_route_poll_liveness(&responses[0], true);
10818 }
10819
10820 #[test]
10821 fn liveness_poll_untracked_process_source_uses_bound_route() {
10822 let registry = Arc::new(Registry::default());
10823 let forwarding = Arc::new(ForwardingTable::default());
10824 let process_liveness = Arc::new(FakeProcessLiveness { live: None });
10825 let handler =
10826 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
10827 .with_process_liveness(process_liveness);
10828 let (ctx, route_channel, route_epoch) =
10829 bind_liveness_route(®istry, &forwarding, "aft-untracked");
10830 let responses = handler
10831 .handle_route_poll(
10832 &ctx,
10833 route_poll_frame(43, PollKind::Liveness, route_channel),
10834 route_channel,
10835 route_epoch,
10836 PollKind::Liveness,
10837 )
10838 .unwrap();
10839
10840 assert_route_poll_liveness(&responses[0], true);
10841 }
10842
10843 #[tokio::test]
10844 async fn unknown_op_returns_unknown_control_op() {
10845 let handler = ControlHandler::default();
10846 let (ctx, _rx) = route_ctx(ConnectionId::new(77));
10847 let request = Frame::build(
10848 FrameType::Request,
10849 control_flags(),
10850 0,
10851 0,
10852 55,
10853 br#"{"op":"route.nope","route_channel":1}"#.to_vec(),
10854 )
10855 .unwrap();
10856
10857 let response = handler.handle_control_frame(&ctx, request).await.unwrap();
10858
10859 assert_eq!(response.len(), 1);
10860 assert_eq!(response[0].header.ty, FrameType::Error);
10861 assert_eq!(response[0].header.corr, 55);
10862 assert_eq!(parse_error(&response[0])["code"], "unknown_control_op");
10863 }
10864
10865 #[tokio::test]
10866 async fn supervisor_provenance_rejects_unknown_exact_module() {
10867 let handler = ControlHandler::default();
10868 let (ctx, _rx) = route_ctx(ConnectionId::new(79));
10869 let request = Frame::build(
10870 FrameType::Request,
10871 control_flags(),
10872 0,
10873 0,
10874 57,
10875 br#"{"op":"supervisor.provenance","module_id":"missing"}"#.to_vec(),
10876 )
10877 .unwrap();
10878
10879 let response = handler.handle_control_frame(&ctx, request).await.unwrap();
10880
10881 assert_eq!(response.len(), 1);
10882 assert_eq!(response[0].header.ty, FrameType::Error);
10883 assert_eq!(response[0].header.corr, 57);
10884 let error = parse_error(&response[0]);
10885 assert_eq!(error["code"], "unknown_module");
10886 assert_eq!(error["message"], "module_id 'missing' is not supervised");
10887 }
10888
10889 #[test]
10890 fn provenance_probe_override_keeps_handler_tests_deterministic() {
10891 let expected = subc_control::RunningImageAgreement::Unavailable {
10892 reason: subc_control::RunningImageUnavailableReason::HashFailed,
10893 };
10894 let handler = ControlHandler::default().with_provenance_probe_result(expected.clone());
10895 assert_eq!(handler.provenance_probe_override, Some(expected));
10896 }
10897
10898 #[test]
10899 fn reload_verdict_detects_configured_program_different_from_spawned_path() {
10900 let verdict = reload_verdict(
10901 std::path::Path::new("/bin/new"),
10902 Some(std::path::Path::new("/bin/old")),
10903 subc_control::RunningImageAgreement::Unavailable {
10904 reason: subc_control::RunningImageUnavailableReason::HashFailed,
10905 },
10906 );
10907 assert!(matches!(
10908 verdict.path,
10909 subc_control::ReloadPathAgreement::Mismatch { configured, spawned_from }
10910 if configured == std::path::Path::new("/bin/new")
10911 && spawned_from == std::path::Path::new("/bin/old")
10912 ));
10913 }
10914
10915 #[test]
10916 fn reload_verdict_detects_replaced_image_at_same_path() {
10917 let image = subc_control::RunningImageAgreement::Mismatch {
10918 running: subc_control::RunningImageEvidence::LinuxProcSha256 {
10919 digest: "old".into(),
10920 },
10921 disk: subc_control::RunningImageEvidence::LinuxProcSha256 {
10922 digest: "new".into(),
10923 },
10924 };
10925 let verdict = reload_verdict(
10926 std::path::Path::new("/bin/same"),
10927 Some(std::path::Path::new("/bin/same")),
10928 image.clone(),
10929 );
10930 assert_eq!(verdict.path, subc_control::ReloadPathAgreement::Match);
10931 assert_eq!(verdict.image, image);
10932 }
10933
10934 #[test]
10935 fn reload_verdict_preserves_stopped_and_unavailable_reasons() {
10936 let image = subc_control::RunningImageAgreement::Unavailable {
10937 reason: subc_control::RunningImageUnavailableReason::NotRunning,
10938 };
10939 let verdict = reload_verdict(std::path::Path::new("/bin/same"), None, image.clone());
10940 assert_eq!(
10941 verdict.path,
10942 subc_control::ReloadPathAgreement::Unavailable {
10943 reason: subc_control::ReloadPathUnavailableReason::NotRunning,
10944 }
10945 );
10946 assert_eq!(verdict.image, image);
10947
10948 let unconfirmed = subc_control::RunningImageAgreement::Unavailable {
10949 reason: subc_control::RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
10950 };
10951 let verdict = reload_verdict(
10952 std::path::Path::new("/bin/same"),
10953 Some(std::path::Path::new("/bin/same")),
10954 unconfirmed.clone(),
10955 );
10956 assert_eq!(verdict.path, subc_control::ReloadPathAgreement::Match);
10957 assert_eq!(verdict.image, unconfirmed);
10958 }
10959
10960 #[test]
10961 fn reload_verdict_preserves_each_image_unavailability_reason() {
10962 use subc_control::RunningImageUnavailableReason as Reason;
10963
10964 for reason in [
10965 Reason::NotRunning,
10966 Reason::UnsupportedPlatform,
10967 Reason::RunningExecutableUnreadable,
10968 Reason::SpawnedPathUnreadable,
10969 Reason::HashFailed,
10970 Reason::ProcessIdentityUnconfirmed,
10971 Reason::Unknown("future_probe_reason".to_string()),
10972 ] {
10973 let image = subc_control::RunningImageAgreement::Unavailable {
10974 reason: reason.clone(),
10975 };
10976 let verdict = reload_verdict(
10977 std::path::Path::new("/bin/same"),
10978 Some(std::path::Path::new("/bin/same")),
10979 image.clone(),
10980 );
10981 assert_eq!(verdict.path, subc_control::ReloadPathAgreement::Match);
10982 assert_eq!(verdict.image, image, "{reason:?}");
10983 }
10984 }
10985
10986 #[tokio::test]
10987 async fn malformed_control_bodies_return_invalid_control_body() {
10988 let handler = ControlHandler::default();
10989 let (ctx, _rx) = route_ctx(ConnectionId::new(78));
10990
10991 for (corr, body) in [
10992 (56, br#"{"route_channel":1}"#.as_slice()),
10993 (57, br#"{"op":17,"route_channel":1}"#.as_slice()),
10994 (
10995 58,
10996 br#"{"op":"route.poll","route_channel":"bad","kind":"status"}"#.as_slice(),
10997 ),
10998 ] {
10999 let request = Frame::build(
11000 FrameType::Request,
11001 control_flags(),
11002 0,
11003 0,
11004 corr,
11005 body.to_vec(),
11006 )
11007 .unwrap();
11008 let response = handler.handle_control_frame(&ctx, request).await.unwrap();
11009
11010 assert_eq!(response.len(), 1);
11011 assert_eq!(response[0].header.ty, FrameType::Error);
11012 assert_eq!(response[0].header.corr, corr);
11013 assert_eq!(parse_error(&response[0])["code"], "invalid_control_body");
11014 }
11015 }
11016
11017 #[tokio::test]
11018 async fn goodbye_tears_down_registration_and_later_channel_is_unknown() {
11019 let registry = Arc::new(Registry::default());
11020 let control = Arc::new(ControlHandler::new(Arc::clone(®istry)));
11021 let router = Router::with_control_handler(Arc::clone(&control));
11022 let connection = router.begin_connection();
11023 let (ctx, mut rx) = route_ctx(connection.id());
11024
11025 router
11026 .route_for_connection(&ctx, hello_frame("aft", PROTOCOL_VERSION, 11))
11027 .await
11028 .unwrap();
11029 let response = rx.recv().await.unwrap();
11030 let ack = parse_ack(&response);
11031 assert_eq!(ack.negotiated_ver, PROTOCOL_VERSION);
11032 let channel = 1;
11033
11034 let goodbye =
11035 Frame::build(FrameType::Goodbye, control_flags(), 0, 0, 12, Vec::new()).unwrap();
11036 router.route_for_connection(&ctx, goodbye).await.unwrap();
11037 assert!(rx.try_recv().is_err());
11038 assert!(registry.get_module("aft").unwrap().is_none());
11039
11040 router
11041 .route_for_connection(&ctx, channel_request(channel, 13))
11042 .await
11043 .unwrap();
11044 let error_frame = rx.recv().await.unwrap();
11045 assert_eq!(error_frame.header.ty, FrameType::Error);
11046 assert_eq!(error_frame.header.channel, channel);
11047 }
11048
11049 #[tokio::test]
11050 async fn module_goodbye_refreshes_requirements_and_pushes_route_closed() {
11051 let registry = Arc::new(Registry::default());
11052 let handler = ControlHandler::new(registry).with_capability_config(
11053 [("prov".to_string(), true), ("cons".to_string(), true)],
11054 BTreeMap::new(),
11055 );
11056 let (provider_ctx, mut provider_rx) = route_ctx(ConnectionId::new(701));
11057 register_capability_manifest(
11058 &handler,
11059 &provider_ctx,
11060 &mut provider_rx,
11061 capability_manifest("prov", &["thing/v1"], &[]),
11062 1,
11063 )
11064 .await;
11065 let mut consumer = capability_manifest("cons", &[], &[]);
11066 consumer.capabilities.as_mut().unwrap().requires.push(
11067 subc_protocol::manifest::CapabilityRequirement {
11068 capability: "thing/v1".to_string(),
11069 need: subc_protocol::manifest::CapabilityNeed::Required,
11070 },
11071 );
11072 let (consumer_ctx, mut consumer_rx) = route_ctx(ConnectionId::new(702));
11073 register_capability_manifest(&handler, &consumer_ctx, &mut consumer_rx, consumer, 2).await;
11074 assert_eq!(
11075 handler.capability_evaluator.verdict("cons", "thing/v1"),
11076 Some(CapabilityVerdict::Provided)
11077 );
11078 let (mut client_rx, _) = open_route_for_capability_test(
11079 &handler,
11080 &provider_ctx,
11081 &mut provider_rx,
11082 703,
11083 3,
11084 "prov",
11085 None,
11086 )
11087 .await;
11088 let goodbye =
11089 Frame::build(FrameType::Goodbye, control_flags(), 0, 0, 4, Vec::new()).unwrap();
11090 handler
11091 .handle_control_frame(&provider_ctx, goodbye)
11092 .await
11093 .unwrap();
11094 assert_eq!(
11095 handler.capability_evaluator.verdict("cons", "thing/v1"),
11096 Some(CapabilityVerdict::NeverProvided)
11097 );
11098 let closed = client_rx
11099 .try_recv()
11100 .expect("GOODBYE pushes route.closed before route GOODBYE");
11101 assert!(
11102 matches!(serde_json::from_slice::<ClientControlPush>(&closed.body).unwrap(),
11103 ClientControlPush::RouteClosed { module_id, channels, .. } if module_id == "prov" && channels.len() == 1)
11104 );
11105 assert_eq!(client_rx.try_recv().unwrap().header.ty, FrameType::Goodbye);
11106 assert_eq!(handler.forwarding.active_binding_count().unwrap(), 0);
11107 }
11108
11109 #[tokio::test]
11110 async fn dropping_router_connection_releases_registration() {
11111 let registry = Arc::new(Registry::default());
11112 let control = Arc::new(ControlHandler::new(Arc::clone(®istry)));
11113 let router = Router::with_control_handler(control);
11114 let connection = router.begin_connection();
11115 let (ctx, mut rx) = route_ctx(connection.id());
11116
11117 router
11118 .route_for_connection(&ctx, hello_frame("aft", PROTOCOL_VERSION, 31))
11119 .await
11120 .unwrap();
11121 let response = rx.recv().await.unwrap();
11122 let ack = parse_ack(&response);
11123 assert_eq!(ack.negotiated_ver, PROTOCOL_VERSION);
11124 assert!(registry.get_module("aft").unwrap().is_some());
11125
11126 drop(connection);
11127
11128 assert!(registry.get_module("aft").unwrap().is_none());
11129 assert_eq!(registry.active_registration_count().unwrap(), 0);
11130 }
11131
11132 fn capability_manifest(
11133 module_id: &str,
11134 provides: &[&str],
11135 must_never_reach: &[&str],
11136 ) -> ModuleManifest {
11137 let mut manifest = manifest(module_id, PROTOCOL_VERSION);
11138 manifest.capabilities = Some(CapabilityDeclarations {
11139 provides: provides
11140 .iter()
11141 .map(|capability| (*capability).to_string())
11142 .collect(),
11143 requires: Vec::new(),
11144 must_never_reach: must_never_reach
11145 .iter()
11146 .map(|capability| (*capability).to_string())
11147 .collect(),
11148 });
11149 manifest
11150 }
11151
11152 fn hello_frame_with_manifest(manifest: ModuleManifest, corr: u64) -> Frame {
11153 Frame::build(
11154 FrameType::Hello,
11155 control_flags(),
11156 0,
11157 0,
11158 corr,
11159 serde_json::to_vec(&ModuleHelloBody {
11160 protocol_ver: manifest.protocol_ver,
11161 manifest,
11162 control_ops: None,
11163 launch_nonce: None,
11164 })
11165 .expect("capability test HELLO serializes"),
11166 )
11167 .expect("capability test HELLO frame builds")
11168 }
11169
11170 fn catalog_update_with_capabilities_frame(
11171 corr: u64,
11172 capabilities: CapabilityDeclarations,
11173 ) -> Frame {
11174 Frame::build(
11175 FrameType::Request,
11176 control_flags(),
11177 0,
11178 0,
11179 corr,
11180 serde_json::to_vec(&ModuleControlRequestFromModule::CatalogUpdate {
11181 provides: manifest("catalog-update-placeholder", PROTOCOL_VERSION).provides,
11182 capabilities: Some(capabilities),
11183 ready: None,
11184 })
11185 .expect("capability catalog.update serializes"),
11186 )
11187 .expect("capability catalog.update frame builds")
11188 }
11189
11190 async fn register_capability_manifest(
11191 handler: &ControlHandler,
11192 ctx: &RouteCtx,
11193 rx: &mut mpsc::Receiver<crate::router::OutboundFrame>,
11194 manifest: ModuleManifest,
11195 corr: u64,
11196 ) {
11197 hello_via_sink(handler, ctx, rx, hello_frame_with_manifest(manifest, corr)).await;
11198 }
11199
11200 async fn open_route_for_capability_test(
11201 handler: &ControlHandler,
11202 target_ctx: &RouteCtx,
11203 target_rx: &mut mpsc::Receiver<crate::router::OutboundFrame>,
11204 client_connection_id: u64,
11205 corr: u64,
11206 target_module_id: &str,
11207 consumer_identity: Option<ConsumerIdentity>,
11208 ) -> (
11209 mpsc::Receiver<crate::router::OutboundFrame>,
11210 ModuleControlRequest,
11211 ) {
11212 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(client_connection_id));
11213 let route_handler = handler.clone();
11214 let target_module_id = target_module_id.to_string();
11215 let route_task = tokio::spawn(async move {
11216 route_handler
11217 .handle_control_frame(
11218 &client_ctx,
11219 route_open_frame_with_admission_facts(
11220 corr,
11221 &target_module_id,
11222 unique_project_root("admission-facts"),
11223 consumer_identity,
11224 None,
11225 ),
11226 )
11227 .await
11228 .expect("capability test route.open succeeds")
11229 });
11230 let bind = tokio::time::timeout(Duration::from_secs(1), target_rx.recv())
11231 .await
11232 .expect("capability test route.open must reach route.bind")
11233 .expect("target control receiver stays open");
11234 let bind_request: ModuleControlRequest =
11235 serde_json::from_slice(&bind.body).expect("route.bind decodes");
11236 handler
11237 .handle_control_frame(target_ctx, route_bind_ack(bind.header.corr))
11238 .await
11239 .expect("capability test route.bind ACK succeeds");
11240 assert!(route_task.await.expect("route.open task joins").is_empty());
11241 let opened = client_rx
11242 .recv()
11243 .await
11244 .expect("successful route.open publishes a response");
11245 assert!(matches!(
11246 serde_json::from_slice::<ClientControlResponse>(&opened.body),
11247 Ok(ClientControlResponse::RouteOpen { .. })
11248 ));
11249 (client_rx, bind_request)
11250 }
11251
11252 fn assert_capability_denied_push(frame: Frame, target_module_id: &str) {
11253 assert_eq!(frame.header.ty, FrameType::Push);
11254 assert_eq!(frame.header.channel, 0);
11255 let push = serde_json::from_slice::<ClientControlPush>(&frame.body)
11256 .expect("route.closed control push decodes");
11257 let ClientControlPush::RouteClosed { channels, .. } = &push else {
11258 panic!("expected route.closed");
11259 };
11260 assert_eq!(channels.len(), 1, "exactly one violating route closed");
11261 let channels = channels.clone();
11262 assert_eq!(
11263 push,
11264 ClientControlPush::RouteClosed {
11265 module_id: target_module_id.to_string(),
11266 channels,
11267 reason: RouteCloseReason::CapabilityDenied,
11268 drained: false,
11269 abandoned: 0,
11270 excluded_subscriptions: 0,
11271 terminal: Some(false),
11272 }
11273 );
11274 }
11275
11276 #[tokio::test]
11277 async fn route_open_capability_forbidden_mutation_proof_creates_no_route() {
11278 let registry = Arc::new(Registry::default());
11279 let forwarding = Arc::new(ForwardingTable::default());
11280 let supervisor = SupervisorHandle::new();
11281 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
11282 let handler =
11283 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
11284 .with_supervisor(supervisor);
11285 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(700));
11286 let (opener_ctx, mut opener_rx) = route_ctx(ConnectionId::new(701));
11287 register_capability_manifest(
11288 &handler,
11289 &target_ctx,
11290 &mut target_rx,
11291 capability_manifest("target", &["credentials-provider/v1"], &[]),
11292 1,
11293 )
11294 .await;
11295 register_capability_manifest(
11296 &handler,
11297 &opener_ctx,
11298 &mut opener_rx,
11299 capability_manifest("opener", &[], &["credentials-provider/v1"]),
11300 2,
11301 )
11302 .await;
11303
11304 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(702));
11305 let replies = handler
11306 .handle_control_frame(
11307 &client_ctx,
11308 route_open_frame_with_admission_facts(
11309 3,
11310 "target",
11311 unique_project_root("admission-facts"),
11312 Some(ConsumerIdentity {
11313 module_id: "opener".to_string(),
11314 launch_nonce: "opener-nonce".to_string(),
11315 }),
11316 None,
11317 ),
11318 )
11319 .await
11320 .expect("denied route.open returns a typed frame");
11321 assert_eq!(parse_error(&replies[0])["code"], "capability_forbidden");
11322 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
11323 assert!(
11324 target_rx.try_recv().is_err(),
11325 "forbidden route.open must not relay route.bind"
11326 );
11327 }
11328
11329 #[tokio::test]
11330 async fn capability_deny_edge_hello_mutation_proof_force_closes_existing_route() {
11331 let registry = Arc::new(Registry::default());
11332 let forwarding = Arc::new(ForwardingTable::default());
11333 let supervisor = SupervisorHandle::new();
11334 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
11335 let handler =
11336 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
11337 .with_supervisor(supervisor);
11338 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(710));
11339 let (old_opener_ctx, mut old_opener_rx) = route_ctx(ConnectionId::new(711));
11340 register_capability_manifest(
11341 &handler,
11342 &target_ctx,
11343 &mut target_rx,
11344 capability_manifest("target", &["credentials-provider/v1"], &[]),
11345 1,
11346 )
11347 .await;
11348 register_capability_manifest(
11349 &handler,
11350 &old_opener_ctx,
11351 &mut old_opener_rx,
11352 capability_manifest("opener", &[], &[]),
11353 2,
11354 )
11355 .await;
11356 let (mut client_rx, _) = open_route_for_capability_test(
11357 &handler,
11358 &target_ctx,
11359 &mut target_rx,
11360 712,
11361 3,
11362 "target",
11363 Some(ConsumerIdentity {
11364 module_id: "opener".to_string(),
11365 launch_nonce: "opener-nonce".to_string(),
11366 }),
11367 )
11368 .await;
11369 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
11370
11371 handler
11372 .cleanup_connection(old_opener_ctx.connection_id)
11373 .expect("old opener registration cleans up");
11374 let (new_opener_ctx, mut new_opener_rx) = route_ctx(ConnectionId::new(713));
11375 register_capability_manifest(
11376 &handler,
11377 &new_opener_ctx,
11378 &mut new_opener_rx,
11379 capability_manifest("opener", &[], &["credentials-provider/v1"]),
11380 4,
11381 )
11382 .await;
11383
11384 assert_capability_denied_push(
11385 client_rx
11386 .try_recv()
11387 .expect("HELLO deny addition must emit route.closed")
11388 .frame,
11389 "target",
11390 );
11391 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
11392 assert!(matches!(
11393 target_rx.try_recv(),
11394 Ok(outbound) if outbound.header.ty == FrameType::Goodbye
11395 ));
11396 }
11397
11398 #[tokio::test]
11399 async fn capability_claim_catalog_update_mutation_proof_force_closes_existing_route() {
11400 let registry = Arc::new(Registry::default());
11401 let forwarding = Arc::new(ForwardingTable::default());
11402 let supervisor = SupervisorHandle::new();
11403 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
11404 let handler =
11405 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
11406 .with_supervisor(supervisor);
11407 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(720));
11408 let (opener_ctx, mut opener_rx) = route_ctx(ConnectionId::new(721));
11409 register_capability_manifest(
11410 &handler,
11411 &target_ctx,
11412 &mut target_rx,
11413 capability_manifest("target", &[], &[]),
11414 1,
11415 )
11416 .await;
11417 register_capability_manifest(
11418 &handler,
11419 &opener_ctx,
11420 &mut opener_rx,
11421 capability_manifest("opener", &[], &["credentials-provider/v1"]),
11422 2,
11423 )
11424 .await;
11425 let (mut client_rx, _) = open_route_for_capability_test(
11426 &handler,
11427 &target_ctx,
11428 &mut target_rx,
11429 722,
11430 3,
11431 "target",
11432 Some(ConsumerIdentity {
11433 module_id: "opener".to_string(),
11434 launch_nonce: "opener-nonce".to_string(),
11435 }),
11436 )
11437 .await;
11438 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
11439
11440 let replies = handler
11441 .handle_control_frame(
11442 &target_ctx,
11443 catalog_update_with_capabilities_frame(
11444 4,
11445 CapabilityDeclarations {
11446 provides: vec!["credentials-provider/v1".to_string()],
11447 requires: Vec::new(),
11448 must_never_reach: Vec::new(),
11449 },
11450 ),
11451 )
11452 .await
11453 .expect("claim catalog.update succeeds");
11454 assert!(matches!(
11455 serde_json::from_slice::<ModuleControlResponseToModule>(&replies[0].body),
11456 Ok(ModuleControlResponseToModule::CatalogUpdate {})
11457 ));
11458 assert_capability_denied_push(
11459 client_rx
11460 .try_recv()
11461 .expect("claim addition must emit route.closed")
11462 .frame,
11463 "target",
11464 );
11465 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
11466 assert!(matches!(
11467 target_rx.try_recv(),
11468 Ok(outbound) if outbound.header.ty == FrameType::Goodbye
11469 ));
11470 }
11471
11472 #[tokio::test]
11473 async fn capability_claim_removal_mutation_proof_keeps_route_open_without_close_frame() {
11474 let registry = Arc::new(Registry::default());
11475 let forwarding = Arc::new(ForwardingTable::default());
11476 let supervisor = SupervisorHandle::new();
11477 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
11478 let handler =
11479 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
11480 .with_supervisor(supervisor);
11481 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(730));
11482 let (opener_ctx, mut opener_rx) = route_ctx(ConnectionId::new(731));
11483 register_capability_manifest(
11484 &handler,
11485 &target_ctx,
11486 &mut target_rx,
11487 capability_manifest("target", &["credentials-provider/v1"], &[]),
11488 1,
11489 )
11490 .await;
11491 register_capability_manifest(
11492 &handler,
11493 &opener_ctx,
11494 &mut opener_rx,
11495 capability_manifest("opener", &[], &[]),
11496 2,
11497 )
11498 .await;
11499 let (mut client_rx, _) = open_route_for_capability_test(
11500 &handler,
11501 &target_ctx,
11502 &mut target_rx,
11503 732,
11504 3,
11505 "target",
11506 Some(ConsumerIdentity {
11507 module_id: "opener".to_string(),
11508 launch_nonce: "opener-nonce".to_string(),
11509 }),
11510 )
11511 .await;
11512 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
11513
11514 handler
11515 .handle_control_frame(
11516 &target_ctx,
11517 catalog_update_with_capabilities_frame(
11518 4,
11519 CapabilityDeclarations {
11520 provides: Vec::new(),
11521 requires: Vec::new(),
11522 must_never_reach: Vec::new(),
11523 },
11524 ),
11525 )
11526 .await
11527 .expect("claim removal catalog.update succeeds");
11528 assert_eq!(
11529 forwarding.active_binding_count().unwrap(),
11530 1,
11531 "removing an attested target claim must leave the route census unchanged"
11532 );
11533 assert!(
11534 client_rx.try_recv().is_err(),
11535 "claim removal must not emit route.closed capability_denied"
11536 );
11537 assert!(
11538 target_rx.try_recv().is_err(),
11539 "claim removal must not send the target a route GOODBYE"
11540 );
11541 }
11542
11543 #[tokio::test]
11546 async fn direct_client_scope_honesty_mutation_proof_opens_denied_capability_provider() {
11547 let registry = Arc::new(Registry::default());
11548 let forwarding = Arc::new(ForwardingTable::default());
11549 let supervisor = SupervisorHandle::new();
11550 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
11551 let handler =
11552 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
11553 .with_supervisor(supervisor);
11554 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(740));
11555 let (opener_ctx, mut opener_rx) = route_ctx(ConnectionId::new(741));
11556 register_capability_manifest(
11557 &handler,
11558 &target_ctx,
11559 &mut target_rx,
11560 capability_manifest("target", &["credentials-provider/v1"], &[]),
11561 1,
11562 )
11563 .await;
11564 register_capability_manifest(
11565 &handler,
11566 &opener_ctx,
11567 &mut opener_rx,
11568 capability_manifest("opener", &[], &["credentials-provider/v1"]),
11569 2,
11570 )
11571 .await;
11572
11573 let (_client_rx, bind) = open_route_for_capability_test(
11574 &handler,
11575 &target_ctx,
11576 &mut target_rx,
11577 742,
11578 3,
11579 "target",
11580 None,
11581 )
11582 .await;
11583 let ModuleControlRequest::RouteBind { principal, .. } = bind else {
11584 panic!("direct scope-honesty route must bind");
11585 };
11586 assert_eq!(principal, Some(Principal::Direct));
11587 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
11588 }
11589
11590 #[tokio::test]
11593 async fn must_never_reach_self_route_is_capability_forbidden() {
11594 let registry = Arc::new(Registry::default());
11595 let forwarding = Arc::new(ForwardingTable::default());
11596 let supervisor = SupervisorHandle::new();
11597 supervisor.set_spawn_nonce("self-provider", "self-nonce".to_string());
11598 let handler =
11599 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
11600 .with_supervisor(supervisor);
11601 let (self_ctx, mut self_rx) = route_ctx(ConnectionId::new(750));
11602 register_capability_manifest(
11603 &handler,
11604 &self_ctx,
11605 &mut self_rx,
11606 capability_manifest(
11607 "self-provider",
11608 &["credentials-provider/v1"],
11609 &["credentials-provider/v1"],
11610 ),
11611 1,
11612 )
11613 .await;
11614
11615 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(751));
11616 let replies = handler
11617 .handle_control_frame(
11618 &client_ctx,
11619 route_open_frame_with_admission_facts(
11620 2,
11621 "self-provider",
11622 unique_project_root("admission-facts"),
11623 Some(ConsumerIdentity {
11624 module_id: "self-provider".to_string(),
11625 launch_nonce: "self-nonce".to_string(),
11626 }),
11627 None,
11628 ),
11629 )
11630 .await
11631 .expect("self-route refusal returns a typed frame");
11632 assert_eq!(parse_error(&replies[0])["code"], "capability_forbidden");
11633 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
11634 assert!(
11635 self_rx.try_recv().is_err(),
11636 "self denial must not relay route.bind"
11637 );
11638 }
11639
11640 #[test]
11641 fn unsupported_channel_zero_frame_returns_error() {
11642 let handler = ControlHandler::default();
11643 let request = Frame::build(
11644 FrameType::Request,
11645 control_flags(),
11646 0,
11647 0,
11648 21,
11649 b"opaque".to_vec(),
11650 )
11651 .unwrap();
11652
11653 let response = handler
11654 .handle_control(ConnectionId::new(1), request)
11655 .unwrap();
11656
11657 assert_eq!(response[0].header.ty, FrameType::Error);
11658 assert_eq!(
11659 parse_error(&response[0])["code"],
11660 "unsupported_control_frame"
11661 );
11662 }
11663
11664 mod swap {
11669 use super::*;
11670
11671 const INCUMBENT: ConnectionId = ConnectionId::new(30);
11672 const CANDIDATE: ConnectionId = ConnectionId::new(40);
11673
11674 struct Swap {
11675 registry: Arc<Registry>,
11676 forwarding: Arc<ForwardingTable>,
11677 handler: ControlHandler,
11678 incumbent_ctx: RouteCtx,
11679 incumbent_rx: mpsc::Receiver<crate::router::OutboundFrame>,
11680 candidate_ctx: RouteCtx,
11681 candidate_rx: mpsc::Receiver<crate::router::OutboundFrame>,
11682 }
11683
11684 async fn swap_with_incumbent() -> Swap {
11685 let registry = Arc::new(Registry::default());
11686 let forwarding = Arc::new(ForwardingTable::default());
11687 let handler =
11688 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
11689 let (incumbent_ctx, mut incumbent_rx) = route_ctx(INCUMBENT);
11690 hello_via_sink(
11691 &handler,
11692 &incumbent_ctx,
11693 &mut incumbent_rx,
11694 hello_frame("aft", PROTOCOL_VERSION, 7),
11695 )
11696 .await;
11697 let (candidate_ctx, candidate_rx) = route_ctx(CANDIDATE);
11698 Swap {
11699 registry,
11700 forwarding,
11701 handler,
11702 incumbent_ctx,
11703 incumbent_rx,
11704 candidate_ctx,
11705 candidate_rx,
11706 }
11707 }
11708
11709 fn register_candidate(swap: &Swap, ready: Option<bool>) {
11710 let mut candidate_manifest = manifest("aft", PROTOCOL_VERSION);
11711 candidate_manifest.ready = ready;
11712 let registration = swap
11713 .registry
11714 .register_candidate_with_control_ops(
11715 candidate_manifest,
11716 PROTOCOL_VERSION,
11717 CANDIDATE,
11718 module_baseline_control_ops(),
11719 )
11720 .unwrap();
11721 swap.forwarding
11722 .register_candidate_module_connection(
11723 CANDIDATE,
11724 "aft".to_string(),
11725 PROTOCOL_VERSION,
11726 manifest_concurrency(®istration.manifest),
11727 swap.candidate_ctx.egress.clone(),
11728 )
11729 .unwrap();
11730 }
11731
11732 fn cutover(swap: &Swap) -> crate::forwarding::ModuleEndpointId {
11733 let cutover = swap.forwarding.cutover_candidate("aft").unwrap().unwrap();
11734 swap.registry.promote_candidate("aft").unwrap().unwrap();
11735 cutover.incumbent.unwrap()
11736 }
11737
11738 fn keyed_total(counters: &Value, key: &str) -> u64 {
11739 counters[key]
11740 .as_object()
11741 .map(|counts| counts.values().filter_map(Value::as_u64).sum())
11742 .unwrap_or(0)
11743 }
11744
11745 #[tokio::test]
11751 async fn incumbent_ack_between_promotion_and_drain_keeps_the_incumbent_serving() {
11752 let mut swap = swap_with_incumbent().await;
11753 let handler = swap.handler.clone();
11754
11755 let cotenant = ConnectionId::new(31);
11757 let (cotenant_ctx, mut cotenant_rx) = route_ctx(cotenant);
11758 let (cotenant_task, cotenant_bind) = relay_route_open(
11759 &handler,
11760 cotenant,
11761 &cotenant_ctx.egress,
11762 &mut swap.incumbent_rx,
11763 100,
11764 "aft",
11765 "swap-cotenant",
11766 )
11767 .await;
11768 handler
11769 .handle_control_frame(
11770 &swap.incumbent_ctx,
11771 route_bind_ack(cotenant_bind.header.corr),
11772 )
11773 .await
11774 .unwrap();
11775 assert!(cotenant_task.await.unwrap().is_empty());
11776 let (cotenant_channel, cotenant_epoch) =
11777 published_route(&cotenant_rx.recv().await.unwrap());
11778
11779 let caller = ConnectionId::new(32);
11781 let (caller_ctx, mut caller_rx) = route_ctx(caller);
11782 let (caller_task, caller_bind) = relay_route_open(
11783 &handler,
11784 caller,
11785 &caller_ctx.egress,
11786 &mut swap.incumbent_rx,
11787 101,
11788 "aft",
11789 "swap-caller",
11790 )
11791 .await;
11792 let (abandoned_channel, abandoned_epoch) = route_bind_channel(&caller_bind);
11793
11794 register_candidate(&swap, None);
11795 cutover(&swap);
11796
11797 let ack = handler
11799 .handle_control_frame(&swap.incumbent_ctx, route_bind_ack(caller_bind.header.corr))
11800 .await;
11801 let module_loop_error = ack.as_ref().err().map(ToString::to_string);
11802 if module_loop_error.is_some() {
11803 handler.cleanup_connection(INCUMBENT).unwrap();
11806 }
11807
11808 assert!(
11810 cotenant_rx.try_recv().is_err(),
11811 "the co-tenant route on the incumbent was torn down by one late ack: \
11812 {module_loop_error:?}"
11813 );
11814 assert!(matches!(
11815 swap.forwarding
11816 .lookup_data_route(cotenant, cotenant_channel, cotenant_epoch)
11817 .unwrap(),
11818 DataRoute::Client(DataRouteState::Bound(_))
11819 ));
11820 assert_eq!(module_loop_error, None);
11821 assert!(swap
11822 .registry
11823 .get_module_by_connection(INCUMBENT)
11824 .unwrap()
11825 .is_some());
11826
11827 let goodbye = tokio::time::timeout(Duration::from_secs(1), swap.incumbent_rx.recv())
11829 .await
11830 .expect("the incumbent is told to drop the abandoned binding")
11831 .unwrap()
11832 .frame;
11833 assert_eq!(goodbye.header.ty, FrameType::Goodbye);
11834 assert_eq!(goodbye.header.channel, abandoned_channel);
11835 assert_eq!(goodbye.header.epoch, abandoned_epoch);
11836 assert!(swap.incumbent_rx.try_recv().is_err());
11837
11838 let response = caller_task.await.unwrap();
11840 assert_eq!(response.len(), 1);
11841 assert_eq!(parse_error(&response[0])["code"], "module_reloading");
11842 assert!(caller_rx.try_recv().is_err());
11843
11844 assert_eq!(swap.forwarding.reserved_route_count().unwrap(), (0, 0));
11848 let counters = handler.counters().snapshot();
11849 assert_eq!(
11850 keyed_total(&counters, "route_open_accepted_by_principal"),
11851 1
11852 );
11853 assert_eq!(keyed_total(&counters, "route_open_refused_by_code"), 1);
11854 assert_eq!(counters["route_open_refused_by_code"]["module_rejected"], 1);
11855 }
11856
11857 #[tokio::test]
11861 async fn route_open_after_cutover_and_incumbent_drain_is_relayed_to_the_candidate() {
11862 let mut swap = swap_with_incumbent().await;
11863 register_candidate(&swap, None);
11864 let incumbent = cutover(&swap);
11865 swap.forwarding
11866 .begin_endpoint_drain(incumbent, RouteCloseReason::Restart)
11867 .unwrap()
11868 .expect("the incumbent is still registered");
11869
11870 let client = ConnectionId::new(33);
11871 let (client_ctx, mut client_rx) = route_ctx(client);
11872 let route_handler = swap.handler.clone();
11873 let open_ctx = RouteCtx {
11874 connection_id: client,
11875 egress: client_ctx.egress.clone(),
11876 };
11877 let mut route_task = tokio::spawn(async move {
11878 route_handler
11879 .handle_control_frame(
11880 &open_ctx,
11881 route_open_frame(90, "aft", unique_project_root("swap-after-drain")),
11882 )
11883 .await
11884 .unwrap()
11885 });
11886 let bind = tokio::select! {
11887 bind = swap.candidate_rx.recv() => bind.expect("candidate egress is open").frame,
11888 response = &mut route_task => {
11889 let response = response.unwrap();
11890 panic!(
11891 "post-cutover route.open was refused instead of relayed to the candidate: {}",
11892 parse_error(&response[0])["code"]
11893 );
11894 }
11895 };
11896 swap.handler
11897 .handle_control_frame(&swap.candidate_ctx, route_bind_ack(bind.header.corr))
11898 .await
11899 .unwrap();
11900 assert!(route_task.await.unwrap().is_empty());
11901 let (channel, epoch) = published_route(&client_rx.recv().await.unwrap());
11902 match swap
11903 .forwarding
11904 .lookup_data_route(client, channel, epoch)
11905 .unwrap()
11906 {
11907 DataRoute::Client(DataRouteState::Bound(route)) => {
11908 assert_eq!(route.module_endpoint.connection_id, CANDIDATE)
11909 }
11910 other => panic!("expected a bound route on the candidate, got {other:?}"),
11911 }
11912 assert!(swap.incumbent_rx.try_recv().is_err());
11913 }
11914
11915 #[tokio::test]
11920 async fn candidate_catalog_update_ready_reaches_the_candidate_registration() {
11921 let swap = swap_with_incumbent().await;
11922 register_candidate(&swap, Some(false));
11923 let update = Frame::build(
11924 FrameType::Request,
11925 control_flags(),
11926 0,
11927 0,
11928 55,
11929 serde_json::to_vec(&ModuleControlRequestFromModule::CatalogUpdate {
11930 provides: manifest("aft", PROTOCOL_VERSION).provides,
11931 capabilities: None,
11932 ready: Some(true),
11933 })
11934 .unwrap(),
11935 )
11936 .unwrap();
11937
11938 let replies = swap
11939 .handler
11940 .handle_control_frame(&swap.candidate_ctx, update)
11941 .await
11942 .unwrap();
11943
11944 assert_eq!(replies.len(), 1);
11945 assert_eq!(
11946 replies[0].header.ty,
11947 FrameType::Response,
11948 "candidate catalog.update was refused: {:?}",
11949 serde_json::from_slice::<Value>(&replies[0].body).ok()
11950 );
11951 assert!(swap.registry.get_candidate("aft").unwrap().unwrap().ready);
11952 assert_eq!(
11953 swap.registry
11954 .get_module("aft")
11955 .unwrap()
11956 .unwrap()
11957 .connection_id,
11958 INCUMBENT
11959 );
11960 }
11961 }
11962
11963 mod swap_admission {
11967 use super::*;
11968
11969 const INCUMBENT_NONCE: &str = "incumbent-nonce";
11970 const CANDIDATE_NONCE: &str = "candidate-nonce";
11971
11972 fn handler_with_incumbent(
11973 module_id: &str,
11974 reserved: bool,
11975 ) -> (Arc<Registry>, SupervisorHandle, ControlHandler) {
11976 let registry = Arc::new(Registry::default());
11977 let supervisor = SupervisorHandle::new();
11978 supervisor.set_spawn_nonce(module_id, INCUMBENT_NONCE.to_string());
11979 if reserved {
11980 supervisor.set_reserved_nonce(module_id, INCUMBENT_NONCE.to_string());
11981 }
11982 let handler =
11983 ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor.clone());
11984 let incumbent = handler
11985 .handle_control(
11986 ConnectionId::new(1),
11987 hello_frame_with_nonce(module_id, PROTOCOL_VERSION, 1, Some(INCUMBENT_NONCE)),
11988 )
11989 .unwrap();
11990 assert_eq!(incumbent[0].header.ty, FrameType::HelloAck);
11991 supervisor.open_swap(module_id, CANDIDATE_NONCE.to_string());
11992 (registry, supervisor, handler)
11993 }
11994
11995 #[test]
12001 fn unminted_nonce_on_an_unreserved_id_with_an_open_swap_is_refused() {
12002 let (registry, _supervisor, handler) = handler_with_incumbent("aft", false);
12003
12004 for (connection, nonce) in [(2, Some("forged")), (3, None)] {
12005 let replies = handler
12006 .handle_control(
12007 ConnectionId::new(connection),
12008 hello_frame_with_nonce("aft", PROTOCOL_VERSION, connection, nonce),
12009 )
12010 .unwrap();
12011 assert_eq!(replies[0].header.ty, FrameType::Error);
12012 assert_eq!(
12013 parse_error(&replies[0])["code"],
12014 "swap_token_invalid",
12015 "nonce {nonce:?}"
12016 );
12017 }
12018 assert!(registry.get_candidate("aft").unwrap().is_none());
12019 assert_eq!(
12020 registry.get_module("aft").unwrap().unwrap().connection_id,
12021 ConnectionId::new(1)
12022 );
12023
12024 let admitted = handler
12027 .handle_control(
12028 ConnectionId::new(4),
12029 hello_frame_with_nonce("aft", PROTOCOL_VERSION, 4, Some(CANDIDATE_NONCE)),
12030 )
12031 .unwrap();
12032 assert_eq!(admitted[0].header.ty, FrameType::HelloAck);
12033 assert_eq!(
12034 registry
12035 .get_candidate("aft")
12036 .unwrap()
12037 .unwrap()
12038 .connection_id,
12039 ConnectionId::new(4)
12040 );
12041 assert_eq!(
12042 registry.get_module("aft").unwrap().unwrap().connection_id,
12043 ConnectionId::new(1),
12044 "the candidate must not take the active slot"
12045 );
12046 let replayed = handler
12047 .handle_control(
12048 ConnectionId::new(5),
12049 hello_frame_with_nonce("aft", PROTOCOL_VERSION, 5, Some(CANDIDATE_NONCE)),
12050 )
12051 .unwrap();
12052 assert_eq!(parse_error(&replayed[0])["code"], "swap_token_invalid");
12053
12054 handler.cleanup_connection(ConnectionId::new(1)).unwrap();
12058 let squatter = handler
12059 .handle_control(
12060 ConnectionId::new(6),
12061 hello_frame_with_nonce("aft", PROTOCOL_VERSION, 6, Some("forged")),
12062 )
12063 .unwrap();
12064 assert_eq!(parse_error(&squatter[0])["code"], "swap_token_invalid");
12065 assert!(
12066 registry.get_module("aft").unwrap().is_none(),
12067 "a squatter took the active slot of an id being swapped"
12068 );
12069 }
12070
12071 #[test]
12076 fn reserved_module_candidate_is_admitted_ahead_of_the_reserved_gate() {
12077 let (registry, _supervisor, handler) = handler_with_incumbent("vault", true);
12078
12079 let replies = handler
12080 .handle_control(
12081 ConnectionId::new(2),
12082 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 2, Some(CANDIDATE_NONCE)),
12083 )
12084 .unwrap();
12085
12086 assert_eq!(
12087 replies[0].header.ty,
12088 FrameType::HelloAck,
12089 "reserved candidate refused: {:?}",
12090 serde_json::from_slice::<Value>(&replies[0].body).ok()
12091 );
12092 assert_eq!(
12093 registry
12094 .get_candidate("vault")
12095 .unwrap()
12096 .unwrap()
12097 .connection_id,
12098 ConnectionId::new(2)
12099 );
12100 }
12101
12102 #[test]
12105 fn without_an_open_swap_the_ordinary_gates_decide() {
12106 let (registry, supervisor, handler) = handler_with_incumbent("vault", true);
12107 supervisor.close_swap("vault");
12108
12109 let candidate = handler
12110 .handle_control(
12111 ConnectionId::new(2),
12112 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 2, Some(CANDIDATE_NONCE)),
12113 )
12114 .unwrap();
12115 assert_eq!(parse_error(&candidate[0])["code"], "reserved_module");
12116 let duplicate = handler
12117 .handle_control(
12118 ConnectionId::new(3),
12119 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 3, Some(INCUMBENT_NONCE)),
12120 )
12121 .unwrap();
12122 assert_eq!(parse_error(&duplicate[0])["code"], "duplicate_module_id");
12123 assert!(registry.get_candidate("vault").unwrap().is_none());
12124 }
12125 }
12126
12127 mod scopes {
12131 use subc_protocol::scope::{
12132 ParentState, ScopeCarrier, ScopeKind, ScopeParent, ScopeRecordOutcome, ScopeStamp,
12133 ScopeStatus,
12134 };
12135
12136 use super::*;
12137
12138 const OWNER: &str = "prefrontal-core";
12139
12140 fn head(scope_ref: &str, scope_epoch: u64) -> ScopeRecord {
12141 ScopeRecord {
12142 scope_ref: scope_ref.to_string(),
12143 scope_epoch,
12144 kind: ScopeKind::Head,
12145 parent: None,
12146 child_owners: Vec::new(),
12147 carriers: Vec::new(),
12148 attributes: Default::default(),
12149 }
12150 }
12151
12152 async fn call(
12153 handler: &ControlHandler,
12154 ctx: &RouteCtx,
12155 request: &ModuleControlRequestFromModule,
12156 ) -> Frame {
12157 let body = serde_json::to_vec(request).unwrap();
12158 let frame = Frame::build(FrameType::Request, control_flags(), 0, 0, 77, body).unwrap();
12159 let mut replies = handler.handle_control_frame(ctx, frame).await.unwrap();
12160 assert_eq!(replies.len(), 1, "{replies:?}");
12161 replies.pop().unwrap()
12162 }
12163
12164 async fn sync(
12165 handler: &ControlHandler,
12166 ctx: &RouteCtx,
12167 generation: u64,
12168 scopes: Vec<ScopeRecord>,
12169 ) -> Result<ModuleControlResponseToModule, String> {
12170 let reply = call(
12171 handler,
12172 ctx,
12173 &ModuleControlRequestFromModule::ScopeSync { generation, scopes },
12174 )
12175 .await;
12176 match reply.header.ty {
12177 FrameType::Response => Ok(serde_json::from_slice(&reply.body).unwrap()),
12178 _ => Err(parse_error(&reply)["code"].as_str().unwrap().to_string()),
12179 }
12180 }
12181
12182 async fn describe(
12183 handler: &ControlHandler,
12184 ctx: &RouteCtx,
12185 owner: &str,
12186 scope_ref: &str,
12187 ) -> ModuleControlResponseToModule {
12188 let reply = call(
12189 handler,
12190 ctx,
12191 &ModuleControlRequestFromModule::ScopeDescribe {
12192 owner: Principal::Reserved {
12193 module_id: owner.to_string(),
12194 },
12195 scope_ref: scope_ref.to_string(),
12196 },
12197 )
12198 .await;
12199 assert_eq!(
12200 reply.header.ty,
12201 FrameType::Response,
12202 "{:?}",
12203 parse_error(&reply)
12204 );
12205 serde_json::from_slice(&reply.body).unwrap()
12206 }
12207
12208 async fn module(
12210 handler: &ControlHandler,
12211 connection: u64,
12212 module_id: &str,
12213 nonce: Option<&str>,
12214 ) -> (RouteCtx, mpsc::Receiver<crate::router::OutboundFrame>) {
12215 let (ctx, mut rx) = route_ctx(ConnectionId::new(connection));
12216 hello_via_sink(
12217 handler,
12218 &ctx,
12219 &mut rx,
12220 hello_frame_with_nonce(module_id, PROTOCOL_VERSION, connection, nonce),
12221 )
12222 .await;
12223 (ctx, rx)
12224 }
12225
12226 #[tokio::test]
12229 async fn a_client_connection_cannot_sync_or_describe() {
12230 let handler = ControlHandler::new(Arc::new(Registry::default()));
12231 let (ctx, _rx) = route_ctx(ConnectionId::new(9));
12232 for request in [
12233 ModuleControlRequestFromModule::ScopeSync {
12234 generation: 1,
12235 scopes: vec![head("s", 1)],
12236 },
12237 ModuleControlRequestFromModule::ScopeDescribe {
12238 owner: Principal::Direct,
12239 scope_ref: "s".to_string(),
12240 },
12241 ] {
12242 let reply = call(&handler, &ctx, &request).await;
12243 assert_eq!(parse_error(&reply)["code"], "not_registered", "{request:?}");
12244 }
12245 assert!(
12246 !handler
12247 .scopes
12248 .read()
12249 .unwrap()
12250 .describe(
12251 &Principal::Reserved {
12252 module_id: OWNER.to_string()
12253 },
12254 "s"
12255 )
12256 .owner_synced
12257 );
12258 }
12259
12260 #[tokio::test]
12263 async fn a_module_without_a_supervised_launch_cannot_sync() {
12264 let handler = ControlHandler::new(Arc::new(Registry::default()));
12265 let (ctx, _rx) = module(&handler, 1, OWNER, None).await;
12266 assert_eq!(
12267 sync(&handler, &ctx, 1, vec![head("s", 1)]).await,
12268 Err(error_codes::SCOPE_SYNC_NOT_AUTHORITY.to_string())
12269 );
12270 }
12271
12272 #[tokio::test]
12273 async fn sync_authority_follows_the_supervisors_recorded_spawn_nonce_across_a_swap() {
12274 let supervisor = SupervisorHandle::new();
12275 supervisor.set_spawn_nonce(OWNER, "n1".to_string());
12276 let handler = ControlHandler::new(Arc::new(Registry::default()))
12277 .with_supervisor(supervisor.clone());
12278 let (incumbent, _incumbent_rx) = module(&handler, 1, OWNER, Some("n1")).await;
12279 sync(&handler, &incumbent, 1, vec![head("s", 1)])
12280 .await
12281 .expect("the current launch syncs");
12282
12283 supervisor.open_swap(OWNER, "n2".to_string());
12286 let (candidate, _candidate_rx) = module(&handler, 2, OWNER, Some("n2")).await;
12287 assert_eq!(
12288 sync(&handler, &candidate, 1, vec![head("x", 1)]).await,
12289 Err(error_codes::SCOPE_SYNC_NOT_AUTHORITY.to_string())
12290 );
12291 sync(&handler, &incumbent, 2, vec![head("s", 1)])
12292 .await
12293 .expect("the serving owner syncs during the swap");
12294
12295 supervisor.close_swap(OWNER);
12298 assert_eq!(
12299 sync(&handler, &candidate, 1, vec![head("x", 1)]).await,
12300 Err(error_codes::SCOPE_SYNC_NOT_AUTHORITY.to_string())
12301 );
12302 sync(&handler, &incumbent, 3, vec![head("s", 1)])
12303 .await
12304 .expect("the serving owner syncs after the rollback");
12305 handler.cleanup_connection(candidate.connection_id).unwrap();
12306
12307 supervisor.open_swap(OWNER, "n3".to_string());
12312 let (promoted, _promoted_rx) = module(&handler, 3, OWNER, Some("n3")).await;
12313 supervisor.set_spawn_nonce(OWNER, "n3".to_string());
12314 let reply = sync(&handler, &promoted, 1, vec![head("s", 1)])
12315 .await
12316 .expect("the promoted launch takes authority");
12317 let ModuleControlResponseToModule::ScopeSync { results, .. } = reply else {
12318 panic!("unexpected reply {reply:?}");
12319 };
12320 assert_eq!(results[0].outcome, ScopeRecordOutcome::Unchanged);
12321 assert_eq!(
12322 sync(&handler, &incumbent, 4, Vec::new()).await,
12323 Err(error_codes::SCOPE_SYNC_NOT_AUTHORITY.to_string())
12324 );
12325 }
12326
12327 #[tokio::test]
12330 async fn closing_the_authority_connection_frees_sync_authority() {
12331 let supervisor = SupervisorHandle::new();
12332 supervisor.set_spawn_nonce(OWNER, "n1".to_string());
12333 let handler = ControlHandler::new(Arc::new(Registry::default()))
12334 .with_supervisor(supervisor.clone());
12335 let (first, _first_rx) = module(&handler, 1, OWNER, Some("n1")).await;
12336 sync(&handler, &first, 10, vec![head("s", 1)])
12337 .await
12338 .unwrap();
12339 handler.cleanup_connection(first.connection_id).unwrap();
12340
12341 let (second, _second_rx) = module(&handler, 2, OWNER, Some("n1")).await;
12342 sync(&handler, &second, 1, vec![head("s", 1)])
12343 .await
12344 .expect("the next connection takes the released authority");
12345 }
12346
12347 #[tokio::test]
12348 async fn module_goodbye_releases_scope_sync_authority_without_socket_close() {
12349 let supervisor = SupervisorHandle::new();
12350 supervisor.set_spawn_nonce(OWNER, "n1".to_string());
12351 let handler =
12352 ControlHandler::new(Arc::new(Registry::default())).with_supervisor(supervisor);
12353 let (first, _rx) = module(&handler, 1, OWNER, Some("n1")).await;
12354 sync(&handler, &first, 10, vec![head("s", 1)])
12355 .await
12356 .unwrap();
12357 handler
12358 .handle_control_frame(
12359 &first,
12360 Frame::build(FrameType::Goodbye, control_flags(), 0, 0, 4, Vec::new()).unwrap(),
12361 )
12362 .await
12363 .unwrap();
12364 let (second, _rx) = module(&handler, 2, OWNER, Some("n1")).await;
12365 sync(&handler, &second, 1, vec![head("s", 1)])
12366 .await
12367 .expect("GOODBYE releases authority even if the old socket remains open");
12368 }
12369
12370 #[tokio::test]
12371 async fn describe_reports_the_incarnation_and_whether_the_owner_is_configured() {
12372 let registry = Arc::new(Registry::default());
12373 let supervisor_handle = SupervisorHandle::new();
12374 let supervisor =
12375 Supervisor::new_for_test(Arc::clone(®istry), RestartPolicy::default())
12376 .with_handle(supervisor_handle.clone())
12377 .with_daemon_incarnation("incarnation-7".to_string());
12378 supervisor
12381 .supervise_configured(
12382 ModuleSpec {
12383 module_id: OWNER.to_string(),
12384 program: PathBuf::from("/nonexistent/prefrontal-core"),
12385 args: Vec::new(),
12386 env: Vec::new(),
12387 reserved: false,
12388 reserved_prefixes: Vec::new(),
12389 protocol: ModuleProtocol::Subc,
12390 overlap: Default::default(),
12391 },
12392 false,
12393 )
12394 .unwrap();
12395 supervisor_handle.set_spawn_nonce(OWNER, "n1".to_string());
12396 let handler =
12397 ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
12398 let (reader, _reader_rx) = module(&handler, 5, "reader", None).await;
12399
12400 let ModuleControlResponseToModule::ScopeDescribe {
12402 status,
12403 daemon_incarnation,
12404 owner_synced,
12405 owner_configured,
12406 scope,
12407 ..
12408 } = describe(&handler, &reader, OWNER, "s").await
12409 else {
12410 panic!("not a describe reply");
12411 };
12412 assert_eq!(status, ScopeStatus::NotLive);
12413 assert_eq!(daemon_incarnation, "incarnation-7");
12414 assert!(!owner_synced);
12415 assert!(owner_configured);
12416 assert!(scope.is_none());
12417
12418 let ModuleControlResponseToModule::ScopeDescribe {
12421 status,
12422 owner_configured,
12423 ..
12424 } = describe(&handler, &reader, "ghost", "s").await
12425 else {
12426 panic!("not a describe reply");
12427 };
12428 assert_eq!(status, ScopeStatus::NotLive);
12429 assert!(!owner_configured);
12430
12431 let (owner, _owner_rx) = module(&handler, 6, OWNER, Some("n1")).await;
12433 sync(&handler, &owner, 1, vec![head("s", 4)]).await.unwrap();
12434 let ModuleControlResponseToModule::ScopeDescribe {
12435 status,
12436 scope_epoch,
12437 owner_synced,
12438 scope,
12439 ..
12440 } = describe(&handler, &reader, OWNER, "s").await
12441 else {
12442 panic!("not a describe reply");
12443 };
12444 assert_eq!(status, ScopeStatus::Live);
12445 assert_eq!(scope_epoch, Some(4));
12446 assert!(owner_synced);
12447 let stamp = scope.expect("a live scope carries its stamp");
12448 assert!(
12449 stamp.owner_authorized,
12450 "prefrontal-core is the default authority"
12451 );
12452 assert_eq!(stamp.kind, ScopeKind::Head);
12453 }
12454
12455 #[tokio::test]
12456 async fn scope_authority_owners_decides_owner_authorized() {
12457 let supervisor = SupervisorHandle::new();
12458 supervisor.set_spawn_nonce("broca", "b1".to_string());
12459 let handler = ControlHandler::new(Arc::new(Registry::default()))
12460 .with_supervisor(supervisor)
12461 .with_scope_authority_owners(vec!["broca".to_string()]);
12462 let (broca, _rx) = module(&handler, 1, "broca", Some("b1")).await;
12463 let mut gated = head("s", 1);
12464 gated.attributes.agent_id = Some("agent".to_string());
12465 sync(&handler, &broca, 1, vec![gated]).await.unwrap();
12466 let ModuleControlResponseToModule::ScopeDescribe { scope, .. } =
12467 describe(&handler, &broca, "broca", "s").await
12468 else {
12469 panic!("not a describe reply");
12470 };
12471 assert!(scope.unwrap().owner_authorized);
12472 }
12473
12474 #[tokio::test]
12478 async fn scope_ops_and_the_scopes_capability_are_advertised() {
12479 let handler = ControlHandler::new(Arc::new(Registry::default()));
12480 let (ctx, mut rx) = route_ctx(ConnectionId::new(1));
12481 let ack = hello_via_sink(
12482 &handler,
12483 &ctx,
12484 &mut rx,
12485 hello_frame("m", PROTOCOL_VERSION, 1),
12486 )
12487 .await;
12488 let ack = parse_ack(&ack);
12489 for op in [SCOPE_SYNC_OP, SCOPE_DESCRIBE_OP] {
12490 assert!(ack.subc_ops.iter().any(|o| o == op), "{:?}", ack.subc_ops);
12491 }
12492 assert!(ack.subc_capabilities.iter().any(|c| c == CAP_SCOPES_V1));
12493
12494 let (client, _client_rx) = route_ctx(ConnectionId::new(2));
12495 let body = serde_json::to_vec(&ClientControlRequest::ServerDescribe {}).unwrap();
12496 let frame = Frame::build(FrameType::Request, control_flags(), 0, 0, 5, body).unwrap();
12497 let reply = handler
12498 .handle_control_frame(&client, frame)
12499 .await
12500 .unwrap()
12501 .pop()
12502 .unwrap();
12503 let ClientControlResponse::ServerDescribe { capabilities, .. } =
12504 serde_json::from_slice(&reply.body).unwrap()
12505 else {
12506 panic!("not a server.describe reply");
12507 };
12508 assert!(
12509 capabilities.iter().any(|c| c == CAP_SCOPES_V1),
12510 "{capabilities:?}"
12511 );
12512 }
12513
12514 const PLEXUS: &str = "plexus";
12517 const OTHER: &str = "other";
12518 const AFT: &str = "aft";
12519 const BROCA: &str = "broca";
12520 const MAGIC: &str = "magic-context";
12521
12522 fn nonce(module_id: &str) -> String {
12523 format!("nonce-{module_id}")
12524 }
12525
12526 fn wide_ctx(connection: u64) -> (RouteCtx, mpsc::Receiver<crate::router::OutboundFrame>) {
12527 let (tx, rx) = mpsc::channel(64);
12528 (
12529 RouteCtx {
12530 connection_id: ConnectionId::new(connection),
12531 egress: FrameSink::new(tx),
12532 },
12533 rx,
12534 )
12535 }
12536
12537 struct Rig {
12541 handler: ControlHandler,
12542 forwarding: Arc<ForwardingTable>,
12543 owner: RouteCtx,
12544 _owner_rx: mpsc::Receiver<crate::router::OutboundFrame>,
12545 modules: BTreeMap<String, (RouteCtx, mpsc::Receiver<crate::router::OutboundFrame>)>,
12546 generation: u64,
12547 next_connection: u64,
12548 _supervisor: Supervisor,
12549 }
12550
12551 async fn rig() -> Rig {
12552 rig_with_flow_support(true).await
12553 }
12554
12555 async fn rig_with_flow_support(flow_support: bool) -> Rig {
12556 let registry = Arc::new(Registry::default());
12557 let forwarding = Arc::new(ForwardingTable::default());
12558 let supervisor_handle = SupervisorHandle::new();
12559 let supervisor =
12560 Supervisor::new_for_test(Arc::clone(®istry), RestartPolicy::default())
12561 .with_handle(supervisor_handle.clone());
12562 supervisor
12563 .supervise_configured(
12564 ModuleSpec {
12565 module_id: OWNER.to_string(),
12566 program: PathBuf::from("/nonexistent/prefrontal-core"),
12567 args: Vec::new(),
12568 env: Vec::new(),
12569 reserved: false,
12570 reserved_prefixes: Vec::new(),
12571 protocol: ModuleProtocol::Subc,
12572 overlap: Default::default(),
12573 },
12574 false,
12575 )
12576 .unwrap();
12577 for module_id in [OWNER, AFT, BROCA, MAGIC] {
12578 supervisor_handle.set_spawn_nonce(module_id, nonce(module_id));
12579 }
12580 let handler =
12581 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
12582 .with_supervisor(supervisor_handle);
12583 let (owner, mut owner_rx) = wide_ctx(1);
12584 hello_via_sink(
12585 &handler,
12586 &owner,
12587 &mut owner_rx,
12588 hello_frame_with_nonce(OWNER, PROTOCOL_VERSION, 1, Some(&nonce(OWNER))),
12589 )
12590 .await;
12591 let mut modules = BTreeMap::new();
12592 for (connection, module_id) in [(2, PLEXUS), (3, OTHER)] {
12593 let (ctx, mut rx) = wide_ctx(connection);
12594 let hello = hello_frame(module_id, PROTOCOL_VERSION, connection);
12595 let mut body: Value = serde_json::from_slice(&hello.body).unwrap();
12596 body["manifest"]["provenance"] =
12600 serde_json::json!({"wire_crate_version": "0.29.0"});
12601 if flow_support {
12602 body["manifest"]["capabilities"] =
12603 serde_json::json!({"provides": ["flow-scopes/v1"]});
12604 }
12605 let hello = Frame::build(
12606 FrameType::Hello,
12607 control_flags(),
12608 0,
12609 0,
12610 connection,
12611 serde_json::to_vec(&body).unwrap(),
12612 )
12613 .unwrap();
12614 hello_via_sink(&handler, &ctx, &mut rx, hello).await;
12615 modules.insert(module_id.to_string(), (ctx, rx));
12616 }
12617 Rig {
12618 handler,
12619 forwarding,
12620 owner,
12621 _owner_rx: owner_rx,
12622 modules,
12623 generation: 0,
12624 next_connection: 100,
12625 _supervisor: supervisor,
12626 }
12627 }
12628
12629 fn carrier(module_id: &str, targets: Option<&[&str]>) -> ScopeCarrier {
12630 ScopeCarrier {
12631 principal: Principal::Reserved {
12632 module_id: module_id.to_string(),
12633 },
12634 targets: targets.map(|targets| targets.iter().map(|t| t.to_string()).collect()),
12635 }
12636 }
12637
12638 fn session(scope_epoch: u64) -> ScopeRecord {
12641 let mut record = head("s", scope_epoch);
12642 record.carriers = vec![carrier(AFT, None), carrier(BROCA, Some(&[PLEXUS, OTHER]))];
12643 record.attributes.agent_id = Some("agent-1".to_string());
12644 record.attributes.delegates = true;
12645 record
12646 }
12647
12648 impl Rig {
12649 async fn sync(&mut self, scopes: Vec<ScopeRecord>) {
12650 self.generation += 1;
12651 sync(&self.handler, &self.owner, self.generation, scopes)
12652 .await
12653 .expect("the owner's sync is accepted");
12654 }
12655
12656 fn selector(&self, scope_ref: &str, scope_epoch: Option<u64>) -> ScopeSelector {
12657 ScopeSelector {
12658 owner: Principal::Reserved {
12659 module_id: OWNER.to_string(),
12660 },
12661 scope_ref: scope_ref.to_string(),
12662 scope_epoch,
12663 }
12664 }
12665
12666 fn open_frame(
12667 &mut self,
12668 opener: Option<&str>,
12669 target: &str,
12670 scope: Option<ScopeSelector>,
12671 ) -> (
12672 RouteCtx,
12673 mpsc::Receiver<crate::router::OutboundFrame>,
12674 Frame,
12675 ) {
12676 self.next_connection += 1;
12677 let (ctx, rx) = wide_ctx(self.next_connection);
12678 let root = unique_project_root("scoped-open");
12679 let body = serde_json::to_vec(&ClientControlRequest::RouteOpen {
12680 target: RouteTarget::ToolProvider {
12681 module_id: target.to_string(),
12682 },
12683 identity: BindIdentity::new(
12684 root.path().to_path_buf(),
12685 "unit".to_string(),
12686 "session".to_string(),
12687 ),
12688 consumer_identity: opener.map(|module_id| ConsumerIdentity {
12689 module_id: module_id.to_string(),
12690 launch_nonce: nonce(module_id),
12691 }),
12692 consumer_capabilities: None,
12693 role_versions: None,
12694 admission_facts: None,
12695 scope,
12696 })
12697 .unwrap();
12698 let frame = Frame::build(
12699 FrameType::Request,
12700 control_flags(),
12701 0,
12702 0,
12703 self.next_connection,
12704 body,
12705 )
12706 .unwrap();
12707 (ctx, rx, frame)
12708 }
12709
12710 async fn refused(
12712 &mut self,
12713 opener: Option<&str>,
12714 target: &str,
12715 scope: Option<ScopeSelector>,
12716 ) -> String {
12717 self.refusal_body(opener, target, scope).await["code"]
12718 .as_str()
12719 .unwrap()
12720 .to_string()
12721 }
12722
12723 async fn refusal_body(
12724 &mut self,
12725 opener: Option<&str>,
12726 target: &str,
12727 scope: Option<ScopeSelector>,
12728 ) -> Value {
12729 let (ctx, _rx, frame) = self.open_frame(opener, target, scope);
12730 let replies = tokio::time::timeout(
12731 Duration::from_secs(2),
12732 self.handler.handle_control_frame(&ctx, frame),
12733 )
12734 .await
12735 .expect("the open must be refused before waiting for a bind ack")
12736 .unwrap();
12737 assert_eq!(replies.len(), 1, "{replies:?}");
12738 assert_eq!(replies[0].header.ty, FrameType::Error);
12739 let (_, module_rx) = self.modules.get_mut(target).unwrap();
12740 assert!(
12741 module_rx.try_recv().is_err(),
12742 "a refused open relays nothing"
12743 );
12744 assert_eq!(self.forwarding.reserved_route_count().unwrap(), (0, 0));
12745 parse_error(&replies[0])
12746 }
12747
12748 async fn relayed(
12750 &mut self,
12751 opener: Option<&str>,
12752 target: &str,
12753 scope: Option<ScopeSelector>,
12754 ) -> Relayed {
12755 let (ctx, rx, frame) = self.open_frame(opener, target, scope);
12756 let handler = self.handler.clone();
12757 let task_ctx = ctx.clone();
12758 let task = tokio::spawn(async move {
12759 handler
12760 .handle_control_frame(&task_ctx, frame)
12761 .await
12762 .unwrap()
12763 });
12764 let (_, module_rx) = self.modules.get_mut(target).unwrap();
12765 let bind = tokio::time::timeout(Duration::from_secs(2), module_rx.recv())
12766 .await
12767 .expect("the target receives the relayed route.bind")
12768 .unwrap()
12769 .frame;
12770 Relayed {
12771 target: target.to_string(),
12772 client: ctx,
12773 client_rx: rx,
12774 task,
12775 bind,
12776 }
12777 }
12778
12779 async fn ack(&self, relayed: &Relayed) {
12780 let (module, _) = &self.modules[&relayed.target];
12781 self.handler
12782 .handle_control_frame(module, route_bind_ack(relayed.bind.header.corr))
12783 .await
12784 .unwrap();
12785 }
12786
12787 async fn bound(
12789 &mut self,
12790 opener: Option<&str>,
12791 target: &str,
12792 scope: Option<ScopeSelector>,
12793 ) -> Bound {
12794 let relayed = self.relayed(opener, target, scope).await;
12795 self.ack(&relayed).await;
12796 let Relayed {
12797 target,
12798 client,
12799 mut client_rx,
12800 task,
12801 bind,
12802 } = relayed;
12803 assert!(
12804 task.await.unwrap().is_empty(),
12805 "the open is answered by commit"
12806 );
12807 let (channel, epoch) = published_route(&client_rx.recv().await.unwrap().frame);
12808 Bound {
12809 target,
12810 client,
12811 client_rx,
12812 channel,
12813 epoch,
12814 bind,
12815 }
12816 }
12817
12818 fn live(&self, route: &Bound) -> bool {
12819 matches!(
12820 self.forwarding
12821 .lookup_data_route(route.client.connection_id, route.channel, route.epoch)
12822 .unwrap(),
12823 DataRoute::Client(DataRouteState::Bound(_))
12824 )
12825 }
12826 }
12827
12828 struct Relayed {
12829 target: String,
12830 client: RouteCtx,
12831 client_rx: mpsc::Receiver<crate::router::OutboundFrame>,
12832 task: tokio::task::JoinHandle<Vec<Frame>>,
12833 bind: Frame,
12834 }
12835
12836 struct Bound {
12837 target: String,
12838 client: RouteCtx,
12839 client_rx: mpsc::Receiver<crate::router::OutboundFrame>,
12840 channel: u16,
12841 epoch: u32,
12842 bind: Frame,
12843 }
12844
12845 impl Bound {
12846 fn closed_reason(&mut self) -> RouteCloseReason {
12849 let mut reason = None;
12850 let mut goodbye = false;
12851 while let Ok(outbound) = self.client_rx.try_recv() {
12852 let frame = outbound.frame;
12853 match frame.header.ty {
12854 FrameType::Goodbye => {
12855 assert_eq!(
12856 (frame.header.channel, frame.header.epoch),
12857 (self.channel, self.epoch)
12858 );
12859 goodbye = true;
12860 }
12861 FrameType::Push => {
12862 let ClientControlPush::RouteClosed {
12863 reason: r,
12864 module_id,
12865 ..
12866 } = serde_json::from_slice(&frame.body).unwrap()
12867 else {
12868 panic!("unexpected push");
12869 };
12870 assert_eq!(module_id, self.target);
12871 reason = Some(r);
12872 }
12873 other => panic!("unexpected frame {other:?}"),
12874 }
12875 }
12876 assert!(goodbye, "the client is sent a GOODBYE for the closed route");
12877 reason.expect("the client is told why the route closed")
12878 }
12879
12880 fn untouched(&mut self) -> bool {
12881 self.client_rx.try_recv().is_err()
12882 }
12883
12884 fn stamp(&self) -> Option<ScopeStamp> {
12885 match serde_json::from_slice::<ModuleControlRequest>(&self.bind.body).unwrap() {
12886 ModuleControlRequest::RouteBind { scope, .. } => scope,
12887 other => panic!("expected a route.bind, got {other:?}"),
12888 }
12889 }
12890 }
12891
12892 #[tokio::test]
12893 async fn only_the_owner_or_a_listed_carrier_is_admitted_and_a_targeted_carrier_only_to_its_modules(
12894 ) {
12895 let mut rig = rig().await;
12896 let mut record = session(1);
12897 record.carriers = vec![carrier(AFT, None), carrier(BROCA, Some(&[PLEXUS]))];
12898 record.child_owners = vec![Principal::Reserved {
12899 module_id: MAGIC.to_string(),
12900 }];
12901 rig.sync(vec![record]).await;
12902 let scope = || Some(rig_selector("s", Some(1)));
12903
12904 rig.bound(Some(OWNER), PLEXUS, scope()).await;
12907 rig.bound(Some(AFT), OTHER, scope()).await;
12908 rig.bound(Some(BROCA), PLEXUS, scope()).await;
12909
12910 for (opener, target) in [(Some(BROCA), OTHER), (Some(MAGIC), PLEXUS), (None, PLEXUS)] {
12914 assert_eq!(
12915 rig.refused(opener, target, scope()).await,
12916 error_codes::SCOPE_NOT_CARRIER,
12917 "{opener:?} -> {target}"
12918 );
12919 }
12920 }
12921
12922 fn rig_selector(scope_ref: &str, scope_epoch: Option<u64>) -> ScopeSelector {
12923 ScopeSelector {
12924 owner: Principal::Reserved {
12925 module_id: OWNER.to_string(),
12926 },
12927 scope_ref: scope_ref.to_string(),
12928 scope_epoch,
12929 }
12930 }
12931
12932 #[tokio::test]
12933 async fn an_open_without_an_epoch_is_refused_the_owners_included() {
12934 let mut rig = rig().await;
12935 rig.sync(vec![session(1)]).await;
12936 for opener in [OWNER, AFT] {
12937 assert_eq!(
12938 rig.refused(Some(opener), PLEXUS, Some(rig.selector("s", None)))
12939 .await,
12940 error_codes::SCOPE_EPOCH_REQUIRED,
12941 "{opener}"
12942 );
12943 }
12944 }
12945
12946 #[tokio::test]
12947 async fn admission_separates_not_synced_not_live_and_ended() {
12948 let mut rig = rig().await;
12949 let code = rig
12951 .refused(Some(AFT), PLEXUS, Some(rig_selector("s", Some(1))))
12952 .await;
12953 assert_eq!(code, error_codes::SCOPE_NOT_SYNCED);
12954 assert!(subc_protocol::error_codes::is_retryable_route_open(&code));
12955
12956 let ghost = ScopeSelector {
12958 owner: Principal::Reserved {
12959 module_id: "ghost".to_string(),
12960 },
12961 scope_ref: "s".to_string(),
12962 scope_epoch: Some(1),
12963 };
12964 assert_eq!(
12965 rig.refused(Some(AFT), PLEXUS, Some(ghost)).await,
12966 error_codes::SCOPE_NOT_LIVE
12967 );
12968
12969 rig.sync(vec![session(2)]).await;
12970 assert_eq!(
12971 rig.refused(Some(AFT), PLEXUS, Some(rig_selector("missing", Some(1))))
12972 .await,
12973 error_codes::SCOPE_NOT_LIVE
12974 );
12975 for epoch in [1, 3] {
12976 assert_eq!(
12977 rig.refused(Some(AFT), PLEXUS, Some(rig_selector("s", Some(epoch))))
12978 .await,
12979 error_codes::SCOPE_ENDED,
12980 "epoch {epoch}"
12981 );
12982 }
12983 rig.bound(Some(AFT), PLEXUS, Some(rig_selector("s", Some(2))))
12985 .await;
12986 }
12987
12988 #[tokio::test]
12989 async fn the_bind_is_stamped_and_owner_authorized_only_for_listed_owners() {
12990 let mut rig = rig().await;
12991 rig.sync(vec![session(1)]).await;
12992 let route = rig
12993 .bound(Some(AFT), PLEXUS, Some(rig_selector("s", Some(1))))
12994 .await;
12995 let stamp = route.stamp().expect("a scoped bind carries the stamp");
12996 assert_eq!(stamp.scope_ref, "s");
12997 assert_eq!(stamp.scope_epoch, 1);
12998 assert_eq!(stamp.kind, ScopeKind::Head);
12999 assert_eq!(stamp.attributes.agent_id.as_deref(), Some("agent-1"));
13000 assert!(stamp.attributes.delegates);
13001 assert!(stamp.owner_authorized);
13002 let unscoped = rig.bound(Some(AFT), PLEXUS, None).await;
13003 assert_eq!(unscoped.stamp(), None, "an unscoped open is not stamped");
13004
13005 let (broca, mut broca_rx) = wide_ctx(50);
13008 hello_via_sink(
13009 &rig.handler,
13010 &broca,
13011 &mut broca_rx,
13012 hello_frame_with_nonce(BROCA, PROTOCOL_VERSION, 50, Some(&nonce(BROCA))),
13013 )
13014 .await;
13015 sync(&rig.handler, &broca, 1, vec![head("b", 1)])
13016 .await
13017 .unwrap();
13018 let own = ScopeSelector {
13019 owner: Principal::Reserved {
13020 module_id: BROCA.to_string(),
13021 },
13022 scope_ref: "b".to_string(),
13023 scope_epoch: Some(1),
13024 };
13025 let route = rig.bound(Some(BROCA), PLEXUS, Some(own)).await;
13026 assert!(!route.stamp().unwrap().owner_authorized);
13027 }
13028
13029 #[tokio::test]
13030 async fn an_authority_owners_flow_id_without_an_agent_is_stamped_verbatim_on_bind() {
13031 let mut rig = rig().await;
13032 let mut record = head("s", 1);
13033 record.carriers = vec![carrier(AFT, None)];
13034 let flow_id = "Flow:run-7/step_2!~";
13035 record.attributes.flow_id = Some(flow_id.to_string());
13036 let reply = sync(&rig.handler, &rig.owner, 1, vec![record])
13037 .await
13038 .unwrap();
13039 let ModuleControlResponseToModule::ScopeSync { results, .. } = reply else {
13040 panic!("not a sync reply");
13041 };
13042 assert_eq!(results[0].outcome, ScopeRecordOutcome::Created);
13043 let route = rig
13044 .bound(Some(AFT), PLEXUS, Some(rig_selector("s", Some(1))))
13045 .await;
13046 assert!(rig.live(&route), "the stamped bind committed");
13047 let stamp = route.stamp().expect("a flow scope carries a stamp");
13048 assert_eq!(stamp.attributes.flow_id.as_deref(), Some(flow_id));
13049 assert_eq!(stamp.attributes.agent_id, None);
13050 assert!(!stamp.attributes.delegates);
13051 assert!(stamp.owner_authorized);
13052 let unscoped = rig.bound(Some(AFT), PLEXUS, None).await;
13053 assert_eq!(unscoped.stamp(), None);
13054 }
13055
13056 #[tokio::test]
13057 async fn flow_scope_refuses_a_0_29_target_without_flow_capability_and_relays_nothing() {
13058 let mut rig = rig_with_flow_support(false).await;
13059 let mut record = session(1);
13060 record.attributes.flow_id = Some("flow:7".to_string());
13061 rig.sync(vec![record]).await;
13062 for opener in [OWNER, AFT] {
13063 let body = rig
13064 .refusal_body(Some(opener), PLEXUS, Some(rig_selector("s", Some(1))))
13065 .await;
13066 assert_eq!(body["code"], "target_flow_unsupported");
13067 let message = body["message"].as_str().unwrap();
13068 for required in [PLEXUS, "flow-scopes/v1"] {
13069 assert!(message.contains(required), "{message}");
13070 }
13071 }
13072 }
13073
13074 #[tokio::test]
13075 async fn flow_scope_admits_a_capable_target_and_preserves_flow_id_on_bind() {
13076 let mut rig = rig_with_flow_support(true).await;
13077 let mut record = session(1);
13078 record.attributes.flow_id = Some("flow:7".to_string());
13079 rig.sync(vec![record]).await;
13080 for opener in [OWNER, AFT] {
13081 let route = rig
13082 .bound(Some(opener), PLEXUS, Some(rig_selector("s", Some(1))))
13083 .await;
13084 assert!(rig.live(&route));
13085 assert_eq!(
13086 route.stamp().unwrap().attributes.flow_id.as_deref(),
13087 Some("flow:7")
13088 );
13089 }
13090 }
13091
13092 #[tokio::test]
13093 async fn flow_scope_rechecks_the_relay_target_after_a_reconnect() {
13094 use std::future::Future;
13095
13096 let mut rig = rig_with_flow_support(true).await;
13097 let mut record = session(1);
13098 record.attributes.flow_id = Some("flow:7".to_string());
13099 rig.sync(vec![record]).await;
13100 let (client, mut client_rx, frame) =
13101 rig.open_frame(Some(AFT), PLEXUS, Some(rig_selector("s", Some(1))));
13102 for _ in 0..64 {
13105 client.egress.try_send(route_bind_ack(1)).unwrap();
13106 }
13107 let handler = rig.handler.clone();
13108 let mut open = Box::pin(handler.handle_control_frame(&client, frame));
13109 std::future::poll_fn(|cx| {
13110 assert!(open.as_mut().poll(cx).is_pending());
13111 std::task::Poll::Ready(())
13112 })
13113 .await;
13114 assert_eq!(rig.forwarding.reserved_route_count().unwrap(), (0, 0));
13115
13116 let old_connection = rig.modules[PLEXUS].0.connection_id;
13117 rig.handler.cleanup_connection(old_connection).unwrap();
13118 let (replacement, mut replacement_rx) = wide_ctx(200);
13119 let hello = hello_frame(PLEXUS, PROTOCOL_VERSION, 200);
13120 let mut body: Value = serde_json::from_slice(&hello.body).unwrap();
13121 body["manifest"]["provenance"] = serde_json::json!({"wire_crate_version": "0.29.0"});
13122 let hello = Frame::build(
13123 FrameType::Hello,
13124 control_flags(),
13125 0,
13126 0,
13127 200,
13128 serde_json::to_vec(&body).unwrap(),
13129 )
13130 .unwrap();
13131 hello_via_sink(&rig.handler, &replacement, &mut replacement_rx, hello).await;
13132
13133 client_rx.try_recv().unwrap();
13134 let replies = tokio::time::timeout(Duration::from_secs(2), open)
13135 .await
13136 .expect("the replacement is refused without waiting for a bind ack")
13137 .unwrap();
13138 assert_eq!(replies.len(), 1);
13139 let body = parse_error(&replies[0]);
13140 assert_eq!(body["code"], "target_flow_unsupported");
13141 for required in [PLEXUS, "flow-scopes/v1"] {
13142 assert!(body["message"].as_str().unwrap().contains(required));
13143 }
13144 assert!(replacement_rx.try_recv().is_err(), "no bind is relayed");
13145 assert!(rig.modules.get_mut(PLEXUS).unwrap().1.try_recv().is_err());
13146 assert_eq!(rig.forwarding.reserved_route_count().unwrap(), (0, 0));
13147 assert!(rig
13148 .handler
13149 .registry
13150 .get_module_by_connection(replacement.connection_id)
13151 .unwrap()
13152 .is_some());
13153 }
13154
13155 #[tokio::test]
13156 async fn scope_without_flow_id_and_unscoped_routes_admit_a_target_without_flow_capability()
13157 {
13158 let mut rig = rig_with_flow_support(false).await;
13159 rig.sync(vec![session(1)]).await;
13160 let route = rig
13161 .bound(Some(AFT), PLEXUS, Some(rig_selector("s", Some(1))))
13162 .await;
13163 assert!(rig.live(&route));
13164 assert_eq!(route.stamp().unwrap().attributes.flow_id, None);
13165 let mut record = session(1);
13166 record.attributes.flow_id = Some("flow:7".to_string());
13167 rig.sync(vec![record]).await;
13168 let unscoped = rig.bound(Some(AFT), OTHER, None).await;
13169 assert!(rig.live(&unscoped));
13170 assert_eq!(unscoped.stamp(), None);
13171 }
13172
13173 #[tokio::test]
13174 async fn a_same_epoch_flow_id_change_bumps_version_and_drains_all_scoped_routes() {
13175 let mut rig = rig().await;
13176 let mut record = session(1);
13177 record.attributes.flow_id = Some("flow:7".to_string());
13178 rig.sync(vec![record.clone()]).await;
13179 let before = rig.forwarding.published_scope_tag(OWNER, "s");
13180 let mut owner_route = rig
13181 .bound(Some(OWNER), PLEXUS, Some(rig_selector("s", Some(1))))
13182 .await;
13183 let mut carrier_route = rig
13184 .bound(Some(AFT), OTHER, Some(rig_selector("s", Some(1))))
13185 .await;
13186 let mut unscoped = rig.bound(Some(AFT), PLEXUS, None).await;
13187 rig.sync(vec![record.clone()]).await;
13188 assert_eq!(rig.forwarding.published_scope_tag(OWNER, "s"), before);
13189 assert!(rig.live(&owner_route) && owner_route.untouched());
13190 assert!(rig.live(&carrier_route) && carrier_route.untouched());
13191
13192 record.attributes.flow_id = Some("flow:8".to_string());
13193 rig.sync(vec![record]).await;
13194 let after = rig.forwarding.published_scope_tag(OWNER, "s").unwrap();
13195 let before = before.unwrap();
13196 assert_eq!(after.scope_epoch, before.scope_epoch);
13197 assert!(after.version > before.version);
13198 for route in [&mut owner_route, &mut carrier_route] {
13199 assert!(!rig.live(route));
13200 assert_eq!(
13201 route.closed_reason(),
13202 RouteCloseReason::ScopeDelegationChanged
13203 );
13204 }
13205 assert!(rig.live(&unscoped) && unscoped.untouched());
13206 for target in [PLEXUS, OTHER] {
13209 let (_, module_rx) = rig.modules.get_mut(target).unwrap();
13210 let goodbye = module_rx
13211 .try_recv()
13212 .expect("the provider sees the drain")
13213 .frame;
13214 assert_eq!(goodbye.header.ty, FrameType::Goodbye);
13215 assert!(module_rx.try_recv().is_err());
13216 }
13217 let rebound = rig
13218 .bound(Some(AFT), PLEXUS, Some(rig_selector("s", Some(1))))
13219 .await;
13220 assert_eq!(
13221 rebound.stamp().unwrap().attributes.flow_id.as_deref(),
13222 Some("flow:8")
13223 );
13224 }
13225
13226 #[tokio::test]
13231 async fn a_scope_changed_or_ended_between_admission_and_commit_refuses_the_open() {
13232 let mut rig = rig().await;
13233 rig.sync(vec![session(1)]).await;
13234 let mut cotenant = rig
13235 .bound(Some(OWNER), PLEXUS, Some(rig_selector("s", Some(1))))
13236 .await;
13237
13238 let mut changed = session(1);
13239 changed.child_owners.push(Principal::Reserved {
13240 module_id: MAGIC.to_string(),
13241 });
13242 let mut ended = None;
13243 for (code, next) in [
13244 (error_codes::SCOPE_CHANGED, vec![changed]),
13245 (error_codes::SCOPE_ENDED, Vec::new()),
13246 ] {
13247 let relayed = rig
13248 .relayed(Some(AFT), PLEXUS, Some(rig_selector("s", Some(1))))
13249 .await;
13250 let (bind_channel, bind_epoch) = route_bind_channel(&relayed.bind);
13251 ended = Some(next.is_empty());
13252 rig.sync(next).await;
13253 rig.ack(&relayed).await;
13254 let replies = relayed.task.await.unwrap();
13255 assert_eq!(replies.len(), 1, "{replies:?}");
13256 assert_eq!(parse_error(&replies[0])["code"], code);
13257 assert_eq!(
13258 subc_protocol::error_codes::is_retryable_route_open(code),
13259 code == error_codes::SCOPE_CHANGED
13260 );
13261 assert_eq!(rig.forwarding.reserved_route_count().unwrap(), (0, 0));
13262 let (_, plexus_rx) = rig.modules.get_mut(PLEXUS).unwrap();
13264 let mut goodbyes = Vec::new();
13267 while let Ok(outbound) = plexus_rx.try_recv() {
13268 assert_eq!(outbound.frame.header.ty, FrameType::Goodbye);
13269 goodbyes.push((outbound.frame.header.channel, outbound.frame.header.epoch));
13270 }
13271 assert!(
13272 goodbyes.contains(&(bind_channel, bind_epoch)),
13273 "{goodbyes:?}"
13274 );
13275 assert!(rig
13276 .handler
13277 .registry
13278 .get_module_by_connection(rig.modules[PLEXUS].0.connection_id)
13279 .unwrap()
13280 .is_some());
13281 }
13282 assert_eq!(ended, Some(true));
13283 assert_eq!(cotenant.closed_reason(), RouteCloseReason::ScopeEnded);
13286 }
13287
13288 #[tokio::test]
13291 async fn each_revocation_drains_exactly_the_affected_routes_with_its_own_reason() {
13292 struct Case {
13293 name: &'static str,
13294 change: fn(&mut ScopeRecord),
13295 closed: [Option<RouteCloseReason>; 4],
13298 }
13299 use RouteCloseReason::*;
13300 let cases = [
13301 Case {
13302 name: "a carrier entry removed",
13303 change: |r| {
13304 r.carriers.retain(|c| {
13305 c.principal
13306 != Principal::Reserved {
13307 module_id: AFT.to_string(),
13308 }
13309 })
13310 },
13311 closed: [None, Some(ScopeCarrierRemoved), None, None],
13312 },
13313 Case {
13314 name: "a target removed from a carrier",
13315 change: |r| r.carriers[1].targets = Some(vec![PLEXUS.to_string()]),
13316 closed: [None, None, None, Some(ScopeCarrierRemoved)],
13317 },
13318 Case {
13319 name: "a bare carrier narrowed to targets",
13320 change: |r| r.carriers[0].targets = Some(vec![OTHER.to_string()]),
13321 closed: [None, Some(ScopeCarrierRemoved), None, None],
13322 },
13323 Case {
13324 name: "delegates turned off",
13325 change: |r| r.attributes.delegates = false,
13326 closed: [Some(ScopeDelegationChanged); 4],
13327 },
13328 Case {
13329 name: "agent_id changed",
13330 change: |r| r.attributes.agent_id = Some("agent-2".to_string()),
13331 closed: [Some(ScopeDelegationChanged); 4],
13332 },
13333 Case {
13334 name: "a carrier added, child owners changed, the record re-sent",
13335 change: |r| {
13336 r.carriers.push(carrier(MAGIC, None));
13337 r.child_owners.push(Principal::Reserved {
13338 module_id: MAGIC.to_string(),
13339 });
13340 },
13341 closed: [None; 4],
13342 },
13343 Case {
13344 name: "a target added",
13345 change: |r| {
13346 r.carriers[1]
13347 .targets
13348 .as_mut()
13349 .unwrap()
13350 .push("third".to_string())
13351 },
13352 closed: [None; 4],
13353 },
13354 Case {
13355 name: "delegates turned on",
13356 change: |r| r.attributes.delegates = true,
13357 closed: [None; 4],
13358 },
13359 ];
13360 for case in cases {
13361 let mut rig = rig().await;
13362 rig.sync(vec![session(1)]).await;
13363 let scope = || Some(rig_selector("s", Some(1)));
13364 let mut routes = [
13365 rig.bound(Some(OWNER), PLEXUS, scope()).await,
13366 rig.bound(Some(AFT), PLEXUS, scope()).await,
13367 rig.bound(Some(BROCA), PLEXUS, scope()).await,
13368 rig.bound(Some(BROCA), OTHER, scope()).await,
13369 ];
13370 let mut record = session(1);
13371 (case.change)(&mut record);
13372 rig.sync(vec![record]).await;
13373 for (index, expected) in case.closed.iter().enumerate() {
13374 let route = &mut routes[index];
13375 match expected {
13376 Some(reason) => {
13377 assert!(!rig.live(route), "{}: route {index} still live", case.name);
13378 assert_eq!(
13379 route.closed_reason(),
13380 *reason,
13381 "{}: route {index}",
13382 case.name
13383 );
13384 }
13385 None => {
13386 assert!(rig.live(route), "{}: route {index} closed", case.name);
13387 assert!(
13388 route.untouched(),
13389 "{}: route {index} was told something",
13390 case.name
13391 );
13392 }
13393 }
13394 }
13395 }
13396 }
13397
13398 #[tokio::test]
13399 async fn ending_or_replacing_a_scope_and_a_parent_ending_drain_every_route_under_it() {
13400 for next in [Vec::new(), vec![session(2)]] {
13402 let mut rig = rig().await;
13403 rig.sync(vec![session(1)]).await;
13404 let mut route = rig
13405 .bound(Some(AFT), PLEXUS, Some(rig_selector("s", Some(1))))
13406 .await;
13407 rig.sync(next).await;
13408 assert!(!rig.live(&route));
13409 assert_eq!(route.closed_reason(), RouteCloseReason::ScopeEnded);
13410 }
13411
13412 let mut rig = rig().await;
13415 let mut child = session(1);
13416 child.scope_ref = "child".to_string();
13417 child.kind = ScopeKind::Worker;
13418 child.parent = Some(ScopeParent {
13419 owner: Principal::Reserved {
13420 module_id: OWNER.to_string(),
13421 },
13422 scope_ref: "s".to_string(),
13423 scope_epoch: 1,
13424 });
13425 rig.sync(vec![session(1), child.clone()]).await;
13426 let mut child_route = rig
13427 .bound(Some(AFT), PLEXUS, Some(rig_selector("child", Some(1))))
13428 .await;
13429 assert_eq!(
13430 child_route.stamp().unwrap().parent_state,
13431 Some(ParentState::Linked)
13432 );
13433 rig.sync(vec![child]).await;
13434 assert!(!rig.live(&child_route));
13435 assert_eq!(
13436 child_route.closed_reason(),
13437 RouteCloseReason::ScopeParentEnded
13438 );
13439 }
13440
13441 #[tokio::test]
13442 async fn re_sending_an_unchanged_record_drains_nothing_and_a_new_carrier_leaves_in_flight_calls(
13443 ) {
13444 let mut rig = rig().await;
13445 rig.sync(vec![session(1)]).await;
13446 let mut route = rig
13447 .bound(Some(AFT), PLEXUS, Some(rig_selector("s", Some(1))))
13448 .await;
13449 let before = rig.forwarding.published_scope_tag(OWNER, "s");
13450 rig.sync(vec![session(1)]).await;
13451 assert_eq!(rig.forwarding.published_scope_tag(OWNER, "s"), before);
13452 assert!(rig.live(&route) && route.untouched());
13453
13454 let DataRoute::Client(DataRouteState::Bound(binding)) = rig
13458 .forwarding
13459 .lookup_data_route(route.client.connection_id, route.channel, route.epoch)
13460 .unwrap()
13461 else {
13462 panic!("the route is bound");
13463 };
13464 binding.flow.acquire_tagged(9, false).await.unwrap();
13465 let mut widened = session(1);
13466 widened.carriers.push(carrier(MAGIC, None));
13467 rig.sync(vec![widened]).await;
13468 assert!(rig.live(&route) && route.untouched());
13469 let (_, plexus_rx) = rig.modules.get_mut(PLEXUS).unwrap();
13470 assert!(plexus_rx.try_recv().is_err(), "the module is told nothing");
13471 assert_eq!(binding.flow.in_flight(), 1);
13474 binding
13475 .flow
13476 .acquire_tagged(10, false)
13477 .await
13478 .expect("the flow is still open");
13479 }
13480
13481 #[tokio::test]
13484 async fn ending_a_scope_drains_its_routes_on_a_superseded_endpoint() {
13485 let mut rig = rig().await;
13486 rig.sync(vec![session(1)]).await;
13487 let mut on_incumbent = rig
13488 .bound(Some(AFT), PLEXUS, Some(rig_selector("s", Some(1))))
13489 .await;
13490
13491 let (candidate, _candidate_rx) = wide_ctx(9);
13494 let registration = rig
13495 .handler
13496 .registry
13497 .register_candidate_with_control_ops(
13498 manifest(PLEXUS, PROTOCOL_VERSION),
13499 PROTOCOL_VERSION,
13500 candidate.connection_id,
13501 module_baseline_control_ops(),
13502 )
13503 .unwrap();
13504 rig.forwarding
13505 .register_candidate_module_connection(
13506 candidate.connection_id,
13507 PLEXUS.to_string(),
13508 PROTOCOL_VERSION,
13509 manifest_concurrency(®istration.manifest),
13510 candidate.egress.clone(),
13511 )
13512 .unwrap();
13513 rig.forwarding.cutover_candidate(PLEXUS).unwrap().unwrap();
13514 rig.handler
13515 .registry
13516 .promote_candidate(PLEXUS)
13517 .unwrap()
13518 .unwrap();
13519 assert!(rig.live(&on_incumbent), "cutover alone does not drain");
13520
13521 rig.sync(Vec::new()).await;
13522 assert!(!rig.live(&on_incumbent));
13523 assert_eq!(on_incumbent.closed_reason(), RouteCloseReason::ScopeEnded);
13524 let (_, incumbent_rx) = rig.modules.get_mut(PLEXUS).unwrap();
13525 let goodbye = incumbent_rx
13526 .try_recv()
13527 .expect("the superseded endpoint is told")
13528 .frame;
13529 assert_eq!(goodbye.header.ty, FrameType::Goodbye);
13530 }
13531 }
13532}
13533
13534#[cfg(test)]
13535mod concurrency_default_exposure_tests {
13536 use super::*;
13537
13538 fn hello_body(role_json: &str) -> Vec<u8> {
13539 format!(
13540 r#"{{"protocol_ver":2,"module_id":"m","manifest":{{"module_id":"m","module_version":"1.0.0","protocol_ver":2,"trust_tier":"first_party","provides":[{role_json}],"consumes":[],"bindings":{{"storage":{{"kind":"sqlite","scope":"project","owns_schema":false}},"vault_grants":[],"identity":{{"requires":[],"optional":[]}}}}}}}}"#
13541 )
13542 .into_bytes()
13543 }
13544
13545 fn manifest_from(body: &[u8]) -> ModuleManifest {
13546 let value: serde_json::Value = serde_json::from_slice(body).expect("hello parses");
13547 serde_json::from_value(value.get("manifest").expect("manifest key").clone())
13548 .expect("manifest parses")
13549 }
13550
13551 const SURFACE_TAIL: &str = r#""operations":[],"config_schema":{"type":"object"},"observability":[],"identity_scope":[]"#;
13552
13553 #[test]
13554 fn absent_concurrency_on_management_surface_is_reported_as_defaulted() {
13555 let body = hello_body(&format!(
13556 r#"{{"role":"management_surface",{SURFACE_TAIL}}}"#
13557 ));
13558 let manifest = manifest_from(&body);
13559 assert_eq!(manifest_concurrency(&manifest), Concurrency::ModuleManaged);
13562 assert!(manifest_concurrency_was_defaulted(&body, &manifest));
13563 }
13564
13565 #[test]
13566 fn declared_concurrency_is_not_reported_even_when_it_equals_the_default() {
13567 let body = hello_body(&format!(
13568 r#"{{"role":"management_surface",{SURFACE_TAIL},"concurrency":"module_managed"}}"#
13569 ));
13570 let manifest = manifest_from(&body);
13571 assert_eq!(manifest_concurrency(&manifest), Concurrency::ModuleManaged);
13572 assert!(!manifest_concurrency_was_defaulted(&body, &manifest));
13573 }
13574
13575 #[test]
13576 fn non_management_roles_are_never_reported() {
13577 let body = hello_body(
13578 r#"{"role":"internal_service","service_id":"s","transport":"bulk","agent_facing":false,"operations":[]}"#,
13579 );
13580 let manifest = manifest_from(&body);
13581 assert!(!manifest_concurrency_was_defaulted(&body, &manifest));
13582 }
13583}