1use std::{
2 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
3 fmt,
4 path::PathBuf,
5 sync::{Arc, Mutex},
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, PollKind, RouteCloseReason,
14 SpawnCursor, StderrCaptureState, StderrTail, StderrTailEntry, SupervisorDaemonProvenance,
15 SupervisorEntry, SupervisorHealthEntry, SupervisorModuleProvenance, SupervisorObservedProcess,
16 SupervisorRescanResult, SupervisorRoute, SupervisorRouteConsumer, SupervisorRouteModule,
17};
18use subc_protocol::{
19 error_codes,
20 manifest::{
21 validate_hello_capability_grammar, validate_hello_self_signal_declarations,
22 CapabilityDeclarations, CapabilityNeed, Concurrency, ManifestProvenance, ModuleManifest,
23 ProviderRole,
24 },
25 session::{
26 HealthReport, ModuleControlPush, ModuleControlRequest, ModuleControlRequestFromModule,
27 ModuleControlResponse, ModuleControlResponseToModule, MODULE_CONTROL_OP_HEALTH_CHECK,
28 MODULE_TO_SUBC_OP_CATALOG_UPDATE,
29 },
30 BindIdentity, ErrorBody, Flags, FrameType, ModuleHelloAckBody, ModuleHelloBody, Principal,
31 Priority, RouteTarget, PROTOCOL_VERSION,
32};
33use tokio::time::{timeout_at, Instant};
34use tracing::{debug, info, warn};
35
36use crate::{
37 capability_requirements::{
38 log_duplicate_claim_events, log_requirement_events, CapabilityRequirementEvaluator,
39 CapabilityVerdict, DuplicateClaimSource, RegisteredModule, RequirementStatus,
40 RuntimeModule,
41 },
42 daemon_config::RestartRequiredSection,
43 forwarding::{
44 CloseReason, EndpointRoute, ForwardingError, ForwardingTable, GoodbyeTarget,
45 ModuleControlRpcCompletion, ModuleControlRpcOutcome, ModuleEndpointId,
46 PendingModuleControlRpc, RouteBindRelayOutcome, RoutePollSnapshot, RouteRelease,
47 },
48 observability::{
49 ROUTE_OPEN_REFUSED_DECLARED_NOT_READY, ROUTE_OPEN_REFUSED_REQUIRED_CAPABILITY_UNPROVIDED,
50 },
51 provenance::{
52 process_start_time, spawned_file_identity, ExecutableIdentityProbe, SpawnedFileIdentity,
53 },
54 registry::{ChannelState, ConnectionId, Registry, RegistryError},
55 router::{RouteCtx, RouterError},
56 server::MAX_PENDING_ROUTE_BINDS_PER_TARGET,
57 stderr_tail::{CaptureState, TailEntry},
58 supervise::{
59 validate_spec, ModuleProcessLiveness, ReservedHelloRejection, SpawnSubscribeRefusal,
60 SupervisorHandle, SwapHelloAdmission,
61 },
62 ConnectedClients, DaemonCounters, Frame, ProjectRootId, Supervisor,
63};
64
65pub const MIN_SUPPORTED_VERSION: u8 = PROTOCOL_VERSION;
71
72const CAP_MANIFEST_REGISTRATION: &str = "manifest_registration_v1";
73const CAP_CHANNEL_LIFECYCLE: &str = "channel_lifecycle_v1";
74const CAP_PING_PONG: &str = "ping_pong_v1";
75const CAP_SESSION_ATTACH: &str = "session_attach_v1";
76const CAP_ADMISSION_FACTS_RELAY: &str = "admission_facts_relay_v1";
77
78const SUBC_CONTROL_OPS: &[&str] = &[
79 ops::SERVER_DESCRIBE,
80 ops::CATALOG_LIST,
81 ops::ROUTE_OPEN,
82 ops::ROUTE_POLL,
83 ops::ROUTE_CLOSING,
84 ops::ROUTE_CLOSED,
85 ops::SUPERVISOR_LIST,
86 ops::SUPERVISOR_RESTART,
87 ops::SUPERVISOR_SWAP,
88 ops::SUPERVISOR_RELOAD,
89 ops::SUPERVISOR_RESCAN,
90 ops::SUPERVISOR_RELEASE_RESERVED,
91 ops::SUPERVISOR_SET_ENABLED,
92 ops::SUPERVISOR_HEALTH_PROBE,
93 ops::SUPERVISOR_HEALTH,
94 ops::SUPERVISOR_STDERR_TAIL,
95 ops::SUPERVISOR_TERMINALS,
96 ops::SUPERVISOR_ROUTES,
97 ops::SUPERVISOR_PROVENANCE,
98 ops::SUPERVISOR_SPAWN_SNAPSHOT,
99 ops::SUPERVISOR_SPAWN_SUBSCRIBE,
100];
101
102const MODULE_TO_SUBC_CONTROL_OPS: &[&str] =
103 &[MODULE_TO_SUBC_OP_CATALOG_UPDATE, "supervisor.live_roots"];
104
105const MODULE_BASELINE_CONTROL_OPS: &[&str] = &["route.bind", "route.status"];
106
107pub const DEFAULT_ROUTE_BIND_RELAY_TIMEOUT: Duration = Duration::from_secs(12);
115
116pub const DEFAULT_ROUTE_BIND_BREAKER_THRESHOLD: u32 = 3;
131
132pub const DEFAULT_ROUTE_BIND_BREAKER_COOLDOWN: Duration = Duration::from_secs(20);
150
151const DEFAULT_HEALTH_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
152const SLOW_CONTROL_DISPATCH_THRESHOLD: Duration = Duration::from_secs(1);
153
154#[derive(Clone)]
155struct DaemonProvenanceFacts {
156 build: DaemonBuildProvenance,
157 pid: Option<u32>,
158 started_at_ms: Option<u64>,
159 start_clock: Option<crate::clock::StartClock>,
160 executable_path: Option<PathBuf>,
161 executable_identity: Option<SpawnedFileIdentity>,
162 process_start_time: Option<u64>,
163 probe: ExecutableIdentityProbe,
164}
165
166impl Default for DaemonProvenanceFacts {
167 fn default() -> Self {
168 Self {
169 build: DaemonBuildProvenance {
170 build_git_sha: None,
171 build_lock_digest: None,
172 },
173 pid: None,
174 started_at_ms: None,
175 start_clock: None,
176 executable_path: None,
177 executable_identity: None,
178 process_start_time: None,
179 probe: ExecutableIdentityProbe::default(),
180 }
181 }
182}
183
184#[derive(Debug, Clone)]
185struct SupervisorRescanContext {
186 supervisor: Supervisor,
187 config_path: PathBuf,
188 configured_port: Option<u16>,
189 storage_config: Option<crate::daemon_config::StorageConfig>,
190 admission_facts_carrier_module_id: Option<String>,
191 admission_facts_targets: Option<Vec<String>>,
192}
193
194#[derive(Clone)]
196pub struct ControlHandler {
197 registry: Arc<Registry>,
198 forwarding: Arc<ForwardingTable>,
199 process_liveness: Option<Arc<dyn ModuleProcessLiveness>>,
200 supervisor: SupervisorHandle,
201 subc_capabilities: Arc<[String]>,
202 route_bind_relay_timeout: Duration,
206 route_bind_relay_timeouts: BTreeMap<String, Duration>,
210 route_bind_breakers: RouteBindBreakers,
213 route_bind_concurrency: RouteBindConcurrency,
216 route_bind_breaker_threshold: u32,
218 route_bind_breaker_cooldown: Duration,
220 health_probe_timeout: Duration,
221 storage_config: Option<crate::daemon_config::StorageConfig>,
224 machine_id: Option<crate::machine_id::MachineId>,
228 admission_facts_carrier_module_id: Option<String>,
229 admission_facts_targets: Option<Vec<String>>,
230 rescan: Option<SupervisorRescanContext>,
231 connected_clients: ConnectedClients,
232 counters: DaemonCounters,
233 capability_evaluator: Arc<CapabilityRequirementEvaluator>,
234 daemon_provenance: DaemonProvenanceFacts,
235 #[cfg(test)]
236 control_dispatch_delay: Option<Duration>,
237 #[cfg(test)]
238 provenance_probe_override: Option<subc_control::RunningImageAgreement>,
239}
240
241impl fmt::Debug for ControlHandler {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 f.debug_struct("ControlHandler")
244 .field("registry", &self.registry)
245 .field("forwarding", &self.forwarding)
246 .field("process_liveness", &self.process_liveness.is_some())
247 .field("supervisor", &self.supervisor)
248 .field("subc_capabilities", &self.subc_capabilities)
249 .finish()
250 }
251}
252
253struct RouteOpenRequest {
254 target: RouteTarget,
255 identity: BindIdentity,
256 consumer_identity: Option<ConsumerIdentity>,
257 consumer_capabilities: Option<Vec<String>>,
258 admission_facts: Option<serde_json::Value>,
259}
260
261struct RouteBindReservationGuard {
262 forwarding: Arc<ForwardingTable>,
263 endpoint: ModuleEndpointId,
264 relay_corr: u64,
265 armed: bool,
266}
267
268struct ModuleControlRpcGuard {
269 forwarding: Arc<ForwardingTable>,
270 endpoint: ModuleEndpointId,
271 corr: u64,
272 armed: bool,
273}
274
275impl ModuleControlRpcGuard {
276 fn new(forwarding: Arc<ForwardingTable>, endpoint: ModuleEndpointId, corr: u64) -> Self {
277 Self {
278 forwarding,
279 endpoint,
280 corr,
281 armed: true,
282 }
283 }
284
285 fn disarm(&mut self) {
286 self.armed = false;
287 }
288}
289
290impl Drop for ModuleControlRpcGuard {
291 fn drop(&mut self) {
292 if self.armed {
293 let _ = self
294 .forwarding
295 .cancel_module_control_rpc(self.endpoint, self.corr);
296 }
297 }
298}
299
300impl RouteBindReservationGuard {
301 fn new(forwarding: Arc<ForwardingTable>, endpoint: ModuleEndpointId, relay_corr: u64) -> Self {
302 Self {
303 forwarding,
304 endpoint,
305 relay_corr,
306 armed: true,
307 }
308 }
309
310 fn release_and_disarm(&mut self) {
311 if !self.armed {
312 return;
313 }
314 if let Ok(Some(target)) = self.forwarding.abort_pending_relay(
315 self.endpoint,
316 self.relay_corr,
317 RouteBindRelayOutcome::ModuleGone("route.open handler canceled".to_string()),
318 ) {
319 send_goodbye_target_best_effort(&target, "canceled route.bind");
320 }
321 self.armed = false;
322 }
323
324 fn disarm(&mut self) {
325 self.armed = false;
326 }
327}
328
329impl Drop for RouteBindReservationGuard {
330 fn drop(&mut self) {
331 self.release_and_disarm();
332 }
333}
334
335#[derive(Debug, Clone, Default)]
365pub(crate) struct RouteBindBreakers {
366 modules: Arc<Mutex<HashMap<String, ModuleBreakerState>>>,
367}
368
369#[derive(Debug, Clone, Default)]
370pub(crate) struct RouteBindConcurrency {
371 modules: Arc<Mutex<HashMap<String, usize>>>,
372}
373
374struct RouteBindConcurrencyGuard {
375 concurrency: RouteBindConcurrency,
376 module_id: String,
377}
378
379impl RouteBindConcurrency {
380 fn try_admit(&self, module_id: &str, limit: usize) -> Result<RouteBindConcurrencyGuard, usize> {
383 let mut modules = self
384 .modules
385 .lock()
386 .expect("route.bind concurrency mutex poisoned");
387 let in_flight = modules.entry(module_id.to_string()).or_default();
388 if *in_flight >= limit {
389 return Err(*in_flight);
390 }
391 *in_flight += 1;
392 Ok(RouteBindConcurrencyGuard {
393 concurrency: self.clone(),
394 module_id: module_id.to_string(),
395 })
396 }
397}
398
399impl Drop for RouteBindConcurrencyGuard {
400 fn drop(&mut self) {
401 let mut modules = self
402 .concurrency
403 .modules
404 .lock()
405 .expect("route.bind concurrency mutex poisoned");
406 let remove = {
407 let in_flight = modules
408 .get_mut(&self.module_id)
409 .expect("admitted route.bind has a concurrency entry");
410 *in_flight -= 1;
411 *in_flight == 0
412 };
413 if remove {
414 modules.remove(&self.module_id);
415 }
416 }
417}
418
419#[derive(Debug, Default)]
420struct ModuleBreakerState {
421 consecutive_timeouts: u32,
423 cooldown_until: Option<Instant>,
426 probe_in_flight: bool,
431}
432
433enum RouteBindAdmission<'a> {
435 Admitted {
436 guard: RouteBindBreakerGuard<'a>,
437 probe: bool,
440 },
441 Refused {
442 consecutive_timeouts: u32,
443 retry_in: Duration,
447 probe_in_flight: bool,
448 },
449}
450
451struct RouteBindBreakerGuard<'a> {
458 breakers: RouteBindBreakers,
459 module_id: &'a str,
460 settled: bool,
461}
462
463impl RouteBindBreakerGuard<'_> {
464 fn record_accepted(&mut self) -> bool {
468 self.settled = true;
469 self.breakers.record_accepted(self.module_id)
470 }
471
472 fn record_timeout(&mut self, threshold: u32, cooldown: Duration) -> Option<BreakerOpened> {
475 self.settled = true;
476 self.breakers
477 .record_timeout(self.module_id, threshold, cooldown)
478 }
479
480 fn record_inconclusive(&mut self) {
489 self.settled = true;
490 self.breakers.record_inconclusive(self.module_id);
491 }
492}
493
494impl Drop for RouteBindBreakerGuard<'_> {
495 fn drop(&mut self) {
496 if !self.settled {
497 self.breakers.record_inconclusive(self.module_id);
498 }
499 }
500}
501
502struct BreakerOpened {
506 consecutive_timeouts: u32,
507 reopened_after_probe: bool,
510}
511
512impl RouteBindBreakers {
513 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, ModuleBreakerState>> {
514 self.modules
515 .lock()
516 .expect("route.bind breaker mutex poisoned")
517 }
518
519 fn admit<'a>(&self, module_id: &'a str) -> RouteBindAdmission<'a> {
522 let admitted = |probe| RouteBindAdmission::Admitted {
523 guard: RouteBindBreakerGuard {
524 breakers: self.clone(),
525 module_id,
526 settled: false,
527 },
528 probe,
529 };
530
531 let mut modules = self.lock();
532 let Some(state) = modules.get_mut(module_id) else {
533 return admitted(false);
534 };
535 let Some(cooldown_until) = state.cooldown_until else {
536 return admitted(false);
537 };
538 if state.probe_in_flight {
539 return RouteBindAdmission::Refused {
540 consecutive_timeouts: state.consecutive_timeouts,
541 retry_in: Duration::ZERO,
542 probe_in_flight: true,
543 };
544 }
545 let now = Instant::now();
546 if now < cooldown_until {
547 return RouteBindAdmission::Refused {
548 consecutive_timeouts: state.consecutive_timeouts,
549 retry_in: cooldown_until - now,
550 probe_in_flight: false,
551 };
552 }
553 state.probe_in_flight = true;
554 admitted(true)
555 }
556
557 fn record_accepted(&self, module_id: &str) -> bool {
558 self.lock()
559 .remove(module_id)
560 .is_some_and(|state| state.cooldown_until.is_some())
561 }
562
563 fn record_timeout(
564 &self,
565 module_id: &str,
566 threshold: u32,
567 cooldown: Duration,
568 ) -> Option<BreakerOpened> {
569 let mut modules = self.lock();
570 let state = modules.entry(module_id.to_string()).or_default();
571 let was_open = state.cooldown_until.is_some();
572 let was_probe = state.probe_in_flight;
573 state.probe_in_flight = false;
574 state.consecutive_timeouts = state.consecutive_timeouts.saturating_add(1);
575 if state.consecutive_timeouts < threshold {
576 return None;
577 }
578 state.cooldown_until = Some(Instant::now() + cooldown);
579 Some(BreakerOpened {
580 consecutive_timeouts: state.consecutive_timeouts,
581 reopened_after_probe: was_open && was_probe,
582 })
583 }
584
585 fn record_inconclusive(&self, module_id: &str) {
586 if let Some(state) = self.lock().get_mut(module_id) {
587 state.probe_in_flight = false;
588 }
589 }
590
591 pub(crate) fn reset_for_new_module_connection(&self, module_id: &str) -> Option<u32> {
606 self.lock()
607 .remove(module_id)
608 .map(|state| state.consecutive_timeouts)
609 .filter(|discarded| *discarded > 0)
610 }
611
612 fn open_snapshot(&self) -> Option<serde_json::Value> {
619 let now = Instant::now();
620 let modules = self.lock();
621 let open = modules
622 .iter()
623 .filter_map(|(module_id, state)| {
624 let cooldown_until = state.cooldown_until?;
625 Some((
626 module_id.clone(),
627 serde_json::json!({
628 "consecutive_timeouts": state.consecutive_timeouts,
629 "cooldown_remaining_ms":
630 cooldown_until.saturating_duration_since(now).as_millis() as u64,
631 "probe_in_flight": state.probe_in_flight,
632 }),
633 ))
634 })
635 .collect::<serde_json::Map<String, serde_json::Value>>();
636 (!open.is_empty()).then_some(serde_json::Value::Object(open))
637 }
638}
639
640impl ControlHandler {
641 pub fn new(registry: Arc<Registry>) -> Self {
642 Self::with_forwarding(registry, Arc::new(ForwardingTable::default()))
643 }
644
645 pub fn with_forwarding(registry: Arc<Registry>, forwarding: Arc<ForwardingTable>) -> Self {
646 let counters = forwarding.counters();
647 let route_bind_breakers = forwarding.route_bind_breakers();
651 let route_bind_concurrency = forwarding.route_bind_concurrency();
652 Self {
653 registry,
654 forwarding,
655 process_liveness: None,
656 supervisor: SupervisorHandle::new(),
657 subc_capabilities: Arc::from([
658 CAP_MANIFEST_REGISTRATION.to_string(),
659 CAP_CHANNEL_LIFECYCLE.to_string(),
660 CAP_PING_PONG.to_string(),
661 CAP_SESSION_ATTACH.to_string(),
662 CAP_ADMISSION_FACTS_RELAY.to_string(),
663 ]),
664 route_bind_relay_timeout: DEFAULT_ROUTE_BIND_RELAY_TIMEOUT,
665 route_bind_relay_timeouts: BTreeMap::new(),
666 route_bind_breakers,
667 route_bind_concurrency,
668 route_bind_breaker_threshold: DEFAULT_ROUTE_BIND_BREAKER_THRESHOLD,
669 route_bind_breaker_cooldown: DEFAULT_ROUTE_BIND_BREAKER_COOLDOWN,
670 health_probe_timeout: DEFAULT_HEALTH_PROBE_TIMEOUT,
671 storage_config: None,
672 machine_id: None,
673 admission_facts_carrier_module_id: None,
674 admission_facts_targets: None,
675 rescan: None,
676 connected_clients: ConnectedClients::new(),
677 counters,
678 capability_evaluator: Arc::new(CapabilityRequirementEvaluator::new()),
679 daemon_provenance: DaemonProvenanceFacts::default(),
680 #[cfg(test)]
681 control_dispatch_delay: None,
682 #[cfg(test)]
683 provenance_probe_override: None,
684 }
685 }
686
687 pub fn with_storage_config(
690 mut self,
691 storage_config: Option<crate::daemon_config::StorageConfig>,
692 ) -> Self {
693 self.storage_config = storage_config;
694 self
695 }
696
697 pub fn with_machine_id(mut self, machine_id: Option<crate::machine_id::MachineId>) -> Self {
700 self.machine_id = machine_id;
701 self
702 }
703
704 pub fn with_admission_facts_config(
708 mut self,
709 carrier_module_id: Option<String>,
710 targets: Option<Vec<String>>,
711 ) -> Self {
712 self.admission_facts_carrier_module_id = carrier_module_id;
713 self.admission_facts_targets = targets;
714 self
715 }
716
717 pub fn with_route_bind_relay_timeout(mut self, timeout: Duration) -> Self {
720 self.route_bind_relay_timeout = timeout;
721 self
722 }
723
724 pub fn with_route_bind_relay_timeouts(
730 mut self,
731 timeouts: impl IntoIterator<Item = (String, Duration)>,
732 ) -> Self {
733 self.route_bind_relay_timeouts = timeouts.into_iter().collect();
734 self
735 }
736
737 pub fn route_bind_relay_timeout_for(&self, module_id: &str) -> Duration {
743 self.route_bind_relay_timeouts
744 .get(module_id)
745 .copied()
746 .unwrap_or(self.route_bind_relay_timeout)
747 }
748
749 pub fn with_route_bind_breaker(mut self, threshold: u32, cooldown: Duration) -> Self {
757 self.route_bind_breaker_threshold = threshold.max(1);
758 self.route_bind_breaker_cooldown = cooldown;
759 self
760 }
761
762 #[cfg(test)]
763 pub(crate) fn with_health_probe_timeout(mut self, timeout: Duration) -> Self {
764 self.health_probe_timeout = timeout;
765 self
766 }
767
768 #[cfg(test)]
769 pub(crate) fn with_control_dispatch_delay(mut self, delay: Duration) -> Self {
770 self.control_dispatch_delay = Some(delay);
771 self
772 }
773
774 pub fn with_process_liveness(
775 mut self,
776 process_liveness: Arc<dyn ModuleProcessLiveness>,
777 ) -> Self {
778 self.process_liveness = Some(process_liveness);
779 self
780 }
781
782 pub fn with_supervisor(mut self, supervisor: SupervisorHandle) -> Self {
783 self.supervisor = supervisor;
784 self
785 }
786
787 pub fn with_daemon_provenance(
788 mut self,
789 pid: u32,
790 started_at_ms: u64,
791 executable_path: Option<PathBuf>,
792 build_git_sha: Option<String>,
793 build_lock_digest: Option<String>,
794 ) -> Self {
795 let executable_identity = executable_path.as_deref().and_then(spawned_file_identity);
796 let process_start_time = process_start_time(pid);
797 self.daemon_provenance = DaemonProvenanceFacts {
798 build: DaemonBuildProvenance {
799 build_git_sha,
800 build_lock_digest,
801 },
802 pid: Some(pid),
803 started_at_ms: Some(started_at_ms),
804 start_clock: None,
805 executable_path,
806 executable_identity,
807 process_start_time,
808 probe: ExecutableIdentityProbe::default(),
809 };
810 self
811 }
812
813 pub(crate) fn with_daemon_start_clock(mut self, clock: crate::clock::StartClock) -> Self {
814 self.daemon_provenance.start_clock = Some(clock);
815 self
816 }
817
818 #[cfg(test)]
819 fn with_provenance_probe_result(mut self, result: subc_control::RunningImageAgreement) -> Self {
820 self.provenance_probe_override = Some(result);
821 self
822 }
823
824 pub fn with_capability_config(
828 self,
829 modules: impl IntoIterator<Item = (String, bool)>,
830 reserved_capabilities: BTreeMap<String, String>,
831 ) -> Self {
832 self.capability_evaluator
833 .configure(modules, reserved_capabilities);
834 self
835 }
836
837 pub fn with_supervisor_rescan(
838 mut self,
839 supervisor: Supervisor,
840 config_path: impl Into<PathBuf>,
841 configured_port: Option<u16>,
842 ) -> Self {
843 self.rescan = Some(SupervisorRescanContext {
844 supervisor,
845 config_path: config_path.into(),
846 configured_port,
847 storage_config: self.storage_config.clone(),
848 admission_facts_carrier_module_id: self.admission_facts_carrier_module_id.clone(),
849 admission_facts_targets: self.admission_facts_targets.clone(),
850 });
851 self
852 }
853
854 pub fn with_connected_clients(mut self, connected_clients: ConnectedClients) -> Self {
855 self.connected_clients = connected_clients;
856 self
857 }
858
859 pub fn forwarding(&self) -> Arc<ForwardingTable> {
860 Arc::clone(&self.forwarding)
861 }
862
863 pub(crate) fn counters(&self) -> DaemonCounters {
864 self.counters.clone()
865 }
866
867 pub fn spawn_capability_deadline_loop(self: Arc<Self>) {
870 tokio::spawn(async move {
871 loop {
872 self.capability_evaluator
873 .wait_for_change_or_deadline()
874 .await;
875 self.refresh_capability_requirements();
876 }
877 });
878 }
879
880 fn runtime_capability_snapshot(
881 &self,
882 ) -> Result<(Vec<RuntimeModule>, Vec<RegisteredModule>), RouterError> {
883 let runtime = self
884 .supervisor
885 .list()
886 .into_iter()
887 .map(|module| {
888 let status = module.status().map_err(|err| {
889 RouterError::backend(0, 0, format!("failed to read capability status: {err}"))
890 })?;
891 Ok(RuntimeModule {
892 module_id: status.module_id,
893 state: status.state,
894 enabled: status.enabled,
895 })
896 })
897 .collect::<Result<Vec<_>, RouterError>>()?;
898 let (_, registrations) = self.registry.list_modules().map_err(|err| {
899 RouterError::backend(
900 0,
901 0,
902 format!("failed to list capability registrations: {err}"),
903 )
904 })?;
905 let registrations = registrations
906 .into_iter()
907 .map(|registration| RegisteredModule {
908 module_id: registration.manifest.module_id,
909 module_version: registration.manifest.module_version,
910 capabilities: registration.manifest.capabilities,
911 })
912 .collect();
913 Ok((runtime, registrations))
914 }
915
916 fn apply_registration_capabilities(&self, registration: &crate::registry::ModuleRegistration) {
923 let cached_registration = RegisteredModule {
924 module_id: registration.manifest.module_id.clone(),
925 module_version: registration.manifest.module_version.clone(),
926 capabilities: registration.manifest.capabilities.clone(),
927 };
928 if self.capability_evaluator.record_hello(&cached_registration) {
929 warn!(
930 module_id = %cached_registration.module_id,
931 "capability claims drifted from the cached manifest"
932 );
933 }
934 if capability_census_trigger(None, registration.manifest.capabilities.as_ref()) {
935 self.enforce_capability_denies();
936 }
937 self.refresh_capability_requirements();
938 }
939
940 pub(crate) fn install_swap_promotion_observer(self: &Arc<Self>) {
944 let observer: std::sync::Weak<dyn crate::supervise::SwapPromotionObserver> =
945 Arc::downgrade(self) as std::sync::Weak<ControlHandler>;
946 self.supervisor.set_swap_promotion_observer(observer);
947 }
948
949 pub fn refresh_capability_requirements(&self) {
950 match self.runtime_capability_snapshot() {
951 Ok((runtime, registrations)) => {
952 log_requirement_events(
953 self.capability_evaluator
954 .evaluate_now(&runtime, ®istrations),
955 );
956 }
957 Err(err) => warn!(error = %err, "failed to recompute capability requirements"),
958 }
959 }
960
961 fn enforce_capability_denies(&self) {
965 let (_, registrations) = match self.registry.list_modules() {
966 Ok(snapshot) => snapshot,
967 Err(err) => {
968 warn!(error = %err, "failed to read registrations for capability deny census");
969 return;
970 }
971 };
972 let manifests = registrations
973 .into_iter()
974 .map(|registration| {
975 (
976 registration.manifest.module_id.clone(),
977 registration.manifest,
978 )
979 })
980 .collect::<BTreeMap<_, _>>();
981 let census = match self.forwarding.route_census(None) {
982 Ok(census) => census,
983 Err(err) => {
984 warn!(error = %err, "failed to read route census for capability deny enforcement");
985 return;
986 }
987 };
988
989 for (target_module_id, routes) in census {
990 let Some(target_manifest) = manifests.get(&target_module_id) else {
991 continue;
992 };
993 let mut closed_routes = Vec::new();
994 let mut module_goodbyes = Vec::new();
995 for route in routes {
996 let Principal::Reserved {
997 module_id: opening_module_id,
998 } = &route.principal
999 else {
1000 continue;
1001 };
1002 let Some(opening_manifest) = manifests.get(opening_module_id) else {
1003 continue;
1004 };
1005 let Some(capability) = denied_capability(opening_manifest, target_manifest) else {
1006 continue;
1007 };
1008
1009 match self.forwarding.release_client_route(
1010 route.goodbye_target.connection_id,
1011 route.goodbye_target.channel,
1012 route.goodbye_target.epoch,
1013 ) {
1014 Ok(RouteRelease::Removed(module_goodbye)) => {
1015 warn!(
1016 opening_module_id,
1017 target_module_id,
1018 capability,
1019 "force-closing route because an attested capability deny edge now matches"
1020 );
1021 closed_routes.push(route);
1022 module_goodbyes.push(module_goodbye);
1023 }
1024 Ok(RouteRelease::Stale | RouteRelease::Absent) => {}
1025 Err(err) => warn!(
1026 opening_module_id,
1027 target_module_id,
1028 capability,
1029 error = %err,
1030 "failed to force-close capability-denied route"
1031 ),
1032 }
1033 }
1034
1035 if closed_routes.is_empty() {
1036 continue;
1037 }
1038 send_route_control_pushes(
1039 &self.forwarding,
1040 closed_routes,
1041 ClientControlPush::RouteClosed {
1042 module_id: target_module_id,
1043 reason: RouteCloseReason::CapabilityDenied,
1044 drained: false,
1045 abandoned: 0,
1046 excluded_subscriptions: 0,
1047 terminal: Some(false),
1048 },
1049 );
1050 self.emit_route_goodbyes(module_goodbyes);
1051 }
1052 }
1053
1054 fn not_ready_reason(
1060 &self,
1061 registration: &crate::registry::ModuleRegistration,
1062 ) -> Option<NotReadyReason> {
1063 if !registration.ready {
1064 return Some(NotReadyReason {
1065 reason: NotReadyReason::DECLARED_NOT_READY.to_string(),
1066 capability: None,
1067 });
1068 }
1069 self.first_unprovided_required_capability(registration)
1070 .map(|capability| NotReadyReason {
1071 reason: NotReadyReason::REQUIRED_CAPABILITY_UNPROVIDED.to_string(),
1072 capability: Some(capability),
1073 })
1074 }
1075
1076 fn first_unprovided_required_capability(
1094 &self,
1095 registration: &crate::registry::ModuleRegistration,
1096 ) -> Option<String> {
1097 let required = registration
1098 .manifest
1099 .capabilities
1100 .iter()
1101 .flat_map(|declarations| declarations.requires.iter())
1102 .filter(|requirement| requirement.need == CapabilityNeed::Required)
1103 .map(|requirement| requirement.capability.as_str())
1104 .collect::<BTreeSet<_>>();
1105 if required.is_empty() {
1106 return None;
1107 }
1108 let module_id = registration.manifest.module_id.as_str();
1109 let verdict = |capability: &str| self.capability_evaluator.verdict(module_id, capability);
1110 if required
1111 .iter()
1112 .any(|capability| verdict(capability).is_none())
1113 {
1114 self.refresh_capability_requirements();
1115 }
1116 required
1117 .into_iter()
1118 .find(|capability| verdict(capability) != Some(CapabilityVerdict::Provided))
1119 .map(str::to_string)
1120 }
1121
1122 fn capability_requirement_statuses(&self) -> Vec<CapabilityRequirementStatus> {
1123 self.capability_evaluator
1124 .statuses()
1125 .into_iter()
1126 .map(capability_requirement_status)
1127 .collect()
1128 }
1129
1130 fn deregister_connection(
1137 &self,
1138 connection_id: ConnectionId,
1139 ) -> Result<Vec<crate::registry::ModuleRegistration>, RegistryError> {
1140 self.registry.deregister_connection(connection_id)
1141 }
1142
1143 pub(crate) fn route_open_target(&self, frame: &Frame) -> Option<String> {
1144 if frame.header.channel != 0 || frame.header.ty != FrameType::Request {
1145 return None;
1146 }
1147 let Ok(ClientControlRequest::RouteOpen { target, .. }) =
1148 parse_client_control_request(&frame.body)
1149 else {
1150 return None;
1151 };
1152 Some(target_module_id(&target).to_string())
1153 }
1154
1155 pub(crate) fn route_open_capacity_refusal(
1156 &self,
1157 ctx: &RouteCtx,
1158 frame: &Frame,
1159 target_module_id: &str,
1160 limit: usize,
1161 ) -> Result<Frame, RouterError> {
1162 self.route_open_admission_refusal_frame(
1163 ctx,
1164 frame,
1165 target_module_id,
1166 format!(
1167 "connection already has {limit} route.open binds in flight; retry after one settles"
1168 ),
1169 )
1170 }
1171
1172 fn route_open_admission_refusal_frame(
1180 &self,
1181 ctx: &RouteCtx,
1182 frame: &Frame,
1183 target_module_id: &str,
1184 message: impl Into<String>,
1185 ) -> Result<Frame, RouterError> {
1186 self.route_open_refusal_frame(
1187 ctx,
1188 frame,
1189 target_module_id,
1190 "open_admission_full",
1191 error_codes::TARGET_UNAVAILABLE,
1192 message,
1193 )
1194 }
1195
1196 #[cfg(test)]
1202 pub fn handle_control(
1203 &self,
1204 connection_id: ConnectionId,
1205 frame: Frame,
1206 ) -> Result<Vec<Frame>, RouterError> {
1207 match frame.header.ty {
1208 FrameType::Ping => Ok(vec![pong(&frame)?]),
1209 FrameType::Hello => self.handle_hello(connection_id, None, frame),
1210 FrameType::Goodbye => self.handle_goodbye(connection_id),
1211 ty => Ok(vec![control_error_frame(
1212 &frame,
1213 "unsupported_control_frame",
1214 format!("unsupported channel-0 frame {ty:?}"),
1215 )?]),
1216 }
1217 }
1218
1219 pub async fn handle_control_frame(
1220 &self,
1221 ctx: &RouteCtx,
1222 frame: Frame,
1223 ) -> Result<Vec<Frame>, RouterError> {
1224 self.handle_control_frame_timed(ctx, frame, None).await
1225 }
1226
1227 pub(crate) async fn handle_control_frame_timed(
1228 &self,
1229 ctx: &RouteCtx,
1230 frame: Frame,
1231 dispatch_started_at: Option<StdInstant>,
1232 ) -> Result<Vec<Frame>, RouterError> {
1233 match frame.header.ty {
1234 FrameType::Ping => Ok(vec![pong(&frame)?]),
1235 FrameType::Hello => {
1236 self.handle_hello(ctx.connection_id, Some(ctx.egress.clone()), frame)
1237 }
1238 FrameType::Goodbye => self.handle_goodbye(ctx.connection_id),
1239 FrameType::Cancel => {
1240 if self
1241 .supervisor
1242 .cancel_spawn_subscription(ctx.connection_id, frame.header.corr)
1243 {
1244 Ok(Vec::new())
1245 } else {
1246 Ok(vec![control_error_frame(
1247 &frame,
1248 "unknown_subscription",
1249 "no supervisor spawn subscription has this correlation id",
1250 )?])
1251 }
1252 }
1253 FrameType::Request => {
1254 if self
1255 .forwarding
1256 .module_endpoint_for_connection(ctx.connection_id)
1257 .map_err(RouterError::Forwarding)?
1258 .is_some()
1259 {
1260 if !is_known_module_request_op(&frame.body) {
1261 return Ok(vec![control_error_frame(
1262 &frame,
1263 "unsupported_control_frame",
1264 "module-originated channel-0 REQUEST is not supported",
1265 )?]);
1266 }
1267 let request = match parse_module_control_request_from_module(&frame.body) {
1268 Ok(request) => request,
1269 Err((err, ControlRequestBodyError::UnknownOp)) => {
1270 return Ok(vec![control_error_frame(
1271 &frame,
1272 "unsupported_control_frame",
1273 format!("unsupported module-originated channel-0 REQUEST: {err}"),
1274 )?])
1275 }
1276 Err((err, ControlRequestBodyError::InvalidBody)) => {
1277 return Ok(vec![control_error_frame(
1278 &frame,
1279 "invalid_control_body",
1280 format!("malformed module control body: {err}"),
1281 )?])
1282 }
1283 };
1284 let op = module_control_request_op(&request);
1285 let corr = frame.header.corr;
1286 log_control_dispatch_arrival(op, ctx.connection_id, corr);
1287 let result =
1288 self.handle_module_control_request(ctx.connection_id, frame, request);
1289 log_slow_control_dispatch(dispatch_started_at, op, ctx.connection_id, corr);
1290 return result;
1291 }
1292
1293 if is_known_module_request_op(&frame.body) {
1294 return Ok(vec![control_error_frame(
1295 &frame,
1296 "not_registered",
1297 "catalog.update requires an active module registration owned by this connection",
1298 )?]);
1299 }
1300
1301 let request = match parse_client_control_request(&frame.body) {
1302 Ok(request) => request,
1303 Err((err, ControlRequestBodyError::UnknownOp)) => {
1304 return Ok(vec![control_error_frame(
1305 &frame,
1306 "unknown_control_op",
1307 format!("unknown client control op: {err}"),
1308 )?])
1309 }
1310 Err((err, ControlRequestBodyError::InvalidBody)) => {
1311 return Ok(vec![control_error_frame(
1312 &frame,
1313 "invalid_control_body",
1314 format!("malformed client control body: {err}"),
1315 )?])
1316 }
1317 };
1318 let op = client_control_request_op(&request);
1319 let corr = frame.header.corr;
1320 log_control_dispatch_arrival(op, ctx.connection_id, corr);
1321 #[cfg(test)]
1322 if let Some(delay) = self.control_dispatch_delay {
1323 tokio::time::sleep(delay).await;
1324 }
1325 let result = self
1326 .handle_client_control_request(ctx, frame, request)
1327 .await;
1328 log_slow_control_dispatch(dispatch_started_at, op, ctx.connection_id, corr);
1329 result
1330 }
1331 FrameType::Push => {
1332 let Some(endpoint) = self
1333 .forwarding
1334 .module_endpoint_for_connection(ctx.connection_id)
1335 .map_err(RouterError::Forwarding)?
1336 else {
1337 return Ok(vec![control_error_frame(
1338 &frame,
1339 "unsupported_control_frame",
1340 "client-originated channel-0 PUSH is not supported",
1341 )?]);
1342 };
1343 self.handle_status_update(endpoint, frame)
1344 }
1345 FrameType::Response | FrameType::Error
1346 if self
1347 .forwarding
1348 .module_endpoint_for_connection(ctx.connection_id)
1349 .map_err(RouterError::Forwarding)?
1350 .is_some() =>
1351 {
1352 self.handle_module_relay_response(ctx.connection_id, frame)
1353 }
1354 ty => Ok(vec![control_error_frame(
1355 &frame,
1356 "unsupported_control_frame",
1357 format!("unsupported channel-0 frame {ty:?}"),
1358 )?]),
1359 }
1360 }
1361
1362 pub fn cleanup_connection(
1363 &self,
1364 connection_id: ConnectionId,
1365 ) -> Result<Vec<crate::registry::ModuleRegistration>, RegistryError> {
1366 let crash_closed = self
1367 .registry
1368 .get_module_by_connection(connection_id)?
1369 .and_then(|registration| {
1370 self.forwarding
1371 .module_endpoint_for_connection(connection_id)
1372 .ok()
1373 .flatten()
1374 .and_then(|endpoint| self.forwarding.endpoint_routes(endpoint).ok())
1375 .map(|routes| (registration.manifest.module_id, routes))
1376 });
1377 if let Some((module_id, routes)) = crash_closed {
1378 let terminal = match self.supervisor.get(&module_id) {
1379 None => false,
1380 Some(module) => match module.will_recover_after_connection_loss() {
1381 Ok(will_recover) => !will_recover,
1382 Err(err) => {
1383 warn!(
1384 %module_id,
1385 error = %err,
1386 "failed to read crash recovery verdict; reporting non-terminal conservatively"
1387 );
1388 false
1389 }
1390 },
1391 };
1392 send_route_control_pushes(
1393 &self.forwarding,
1394 routes,
1395 ClientControlPush::RouteClosed {
1396 module_id,
1397 reason: RouteCloseReason::Crash,
1398 drained: false,
1399 abandoned: 0,
1400 excluded_subscriptions: 0,
1401 terminal: Some(terminal),
1402 },
1403 );
1404 }
1405 let registrations = self.deregister_connection(connection_id);
1406 if let Ok(released_routes) = self.forwarding.cleanup_connection(connection_id) {
1407 self.emit_route_goodbyes(released_routes);
1408 }
1409 if matches!(®istrations, Ok(r) if !r.is_empty()) {
1413 crate::supervise::notify_registration_release();
1414 self.capability_evaluator.wake_deadline_loop();
1415 self.refresh_capability_requirements();
1416 }
1417 self.supervisor.remove_spawn_subscribers(connection_id);
1418 registrations
1419 }
1420
1421 pub(crate) fn handle_route_goodbye(
1422 &self,
1423 connection_id: ConnectionId,
1424 route_channel: u16,
1425 route_epoch: u32,
1426 ) -> Result<bool, RouterError> {
1427 debug!(
1428 connection_id = connection_id.get(),
1429 route_channel, route_epoch, "handling route GOODBYE"
1430 );
1431 let RouteRelease::Removed(released_route) = self
1432 .forwarding
1433 .release_client_route(connection_id, route_channel, route_epoch)
1434 .map_err(RouterError::Forwarding)?
1435 else {
1436 return Ok(false);
1437 };
1438 self.emit_route_goodbyes(vec![released_route]);
1439 Ok(true)
1440 }
1441
1442 fn emit_route_goodbyes(&self, released_routes: Vec<GoodbyeTarget>) {
1443 for released in released_routes {
1444 let frame = match Frame::build_with_version(
1445 released.negotiated_ver,
1446 FrameType::Goodbye,
1447 control_flags(),
1448 released.channel,
1449 released.epoch,
1450 0,
1451 Vec::new(),
1452 ) {
1453 Ok(frame) => frame,
1454 Err(err) => {
1455 warn!(
1456 route_channel = released.channel,
1457 error = %err,
1458 "failed to build route GOODBYE frame"
1459 );
1460 continue;
1461 }
1462 };
1463 if let Err(err) = released.sink.try_send(frame) {
1464 if released.close_on_delivery_failure() {
1465 warn!(
1466 target_connection_id = released.connection_id.get(),
1467 route_channel = released.channel,
1468 error = %err,
1469 "route GOODBYE was not delivered to client; closing target connection"
1470 );
1471 if self
1472 .forwarding
1473 .escalate_client_delivery_failure(
1474 released.connection_id,
1475 released.channel,
1476 released.epoch,
1477 CloseReason::new(
1478 "route_goodbye_delivery_failed",
1479 format!(
1480 "failed to enqueue route GOODBYE for channel {}: {err}",
1481 released.channel
1482 ),
1483 ),
1484 crate::forwarding::UndeliveredFrame {
1485 module_id: released.module_id.as_deref(),
1486 sink: &released.sink,
1487 },
1488 )
1489 .unwrap_or(false)
1490 {
1491 self.counters.increment_goodbye_relay_client_failed();
1492 }
1493 } else {
1494 self.counters
1495 .increment_goodbye_relay_module_dropped(released.module_id.as_deref());
1496 warn!(
1497 target_connection_id = released.connection_id.get(),
1498 route_channel = released.channel,
1499 error = %err,
1500 "route GOODBYE to module dropped under backpressure; not closing shared module connection"
1501 );
1502 }
1503 }
1504 }
1505 }
1506
1507 fn send_abandoned_route_bind_goodbye(
1519 &self,
1520 module_sink: &crate::FrameSink,
1521 negotiated_ver: u8,
1522 module_channel: u16,
1523 module_epoch: u32,
1524 ) {
1525 let frame = match Frame::build_with_version(
1526 negotiated_ver,
1527 FrameType::Goodbye,
1528 control_flags(),
1529 module_channel,
1530 module_epoch,
1531 0,
1532 Vec::new(),
1533 ) {
1534 Ok(frame) => frame,
1535 Err(err) => {
1536 warn!(
1537 route_channel = module_channel,
1538 error = %err,
1539 "failed to build GOODBYE for abandoned route.bind"
1540 );
1541 return;
1542 }
1543 };
1544 if let Err(err) = module_sink.try_send(frame) {
1545 warn!(
1546 route_channel = module_channel,
1547 error = %err,
1548 "GOODBYE for abandoned route.bind dropped; module idle reaper will reclaim the binding"
1549 );
1550 }
1551 }
1552
1553 fn handle_hello(
1554 &self,
1555 connection_id: ConnectionId,
1556 sink: Option<crate::FrameSink>,
1557 frame: Frame,
1558 ) -> Result<Vec<Frame>, RouterError> {
1559 debug!(
1560 connection_id = connection_id.get(),
1561 corr = frame.header.corr,
1562 "handling HELLO"
1563 );
1564 let hello_value = match serde_json::from_slice::<serde_json::Value>(&frame.body) {
1565 Ok(value) => value,
1566 Err(err) => {
1567 return Ok(vec![control_error_frame(
1568 &frame,
1569 "invalid_hello",
1570 format!("malformed HELLO body: {err}"),
1571 )?])
1572 }
1573 };
1574 if let Err(err) = validate_hello_capability_grammar(&hello_value) {
1575 return Ok(vec![control_error_frame(
1576 &frame,
1577 "invalid_capability_grammar",
1578 err.to_string(),
1579 )?]);
1580 }
1581 if let Err(err) = validate_hello_self_signal_declarations(&hello_value) {
1582 return Ok(vec![control_error_frame(
1583 &frame,
1584 "invalid_manifest",
1585 err.to_string(),
1586 )?]);
1587 }
1588 if let Some(provenance) = hello_value
1589 .get("manifest")
1590 .and_then(|manifest| manifest.get("provenance"))
1591 {
1592 if let Err(err) = serde_json::from_value::<ManifestProvenance>(provenance.clone()) {
1593 return Ok(vec![control_error_frame(
1594 &frame,
1595 "invalid_manifest",
1596 format!("malformed manifest provenance: {err}"),
1597 )?]);
1598 }
1599 }
1600 let hello = match serde_json::from_value::<ModuleHelloBody>(hello_value) {
1601 Ok(hello) => hello,
1602 Err(err) => {
1603 return Ok(vec![control_error_frame(
1604 &frame,
1605 "invalid_hello",
1606 format!("malformed HELLO body: {err}"),
1607 )?])
1608 }
1609 };
1610
1611 if hello.protocol_ver != hello.manifest.protocol_ver {
1612 return Ok(vec![control_error_frame(
1613 &frame,
1614 "invalid_manifest",
1615 format!(
1616 "HELLO protocol_ver {} does not match manifest protocol_ver {}",
1617 hello.protocol_ver, hello.manifest.protocol_ver
1618 ),
1619 )?]);
1620 }
1621
1622 if hello.manifest.module_id.trim().is_empty() {
1623 return Ok(vec![control_error_frame(
1624 &frame,
1625 "invalid_manifest",
1626 "manifest module_id must not be empty",
1627 )?]);
1628 }
1629
1630 let negotiated_ver = match negotiate_version(hello.protocol_ver) {
1631 Ok(negotiated_ver) => negotiated_ver,
1632 Err(message) => {
1633 return Ok(vec![control_error_frame(
1634 &frame,
1635 "version_unsupported",
1636 message,
1637 )?])
1638 }
1639 };
1640
1641 let swap_admission = self
1649 .supervisor
1650 .swap_hello_admission(&hello.manifest.module_id, hello.launch_nonce.as_deref());
1651 if swap_admission == SwapHelloAdmission::Refused {
1652 warn!(
1653 module_id = %hello.manifest.module_id,
1654 connection_id = connection_id.get(),
1655 "HELLO refused: a swap is open for this module_id and the launch nonce is not one the supervisor minted for it"
1656 );
1657 return Ok(vec![control_error_frame(
1658 &frame,
1659 "swap_token_invalid",
1660 format!(
1661 "module_id '{}' is being swapped; HELLO without the swap candidate's launch nonce is rejected",
1662 hello.manifest.module_id
1663 ),
1664 )?]);
1665 }
1666 let swap_candidate = swap_admission == SwapHelloAdmission::Candidate;
1667
1668 if let Some(rejection) = (!swap_candidate)
1676 .then(|| {
1677 self.supervisor.reserved_hello_rejection(
1678 &hello.manifest.module_id,
1679 hello.launch_nonce.as_deref(),
1680 )
1681 })
1682 .flatten()
1683 {
1684 let message = match rejection {
1685 ReservedHelloRejection::Exact { module_id } => format!(
1686 "module_id '{module_id}' is reserved; HELLO without a valid launch nonce is rejected"
1687 ),
1688 ReservedHelloRejection::Prefix {
1689 prefix,
1690 owner_module_id,
1691 } => format!(
1692 "module_id '{}' matches reserved prefix '{prefix}' owned by '{owner_module_id}'; HELLO without the owner launch nonce is rejected",
1693 hello.manifest.module_id
1694 ),
1695 };
1696 return Ok(vec![control_error_frame(
1697 &frame,
1698 "reserved_module",
1699 message,
1700 )?]);
1701 }
1702
1703 let reserved_capability_refusals = self.capability_evaluator.reserved_hello_refusals(
1704 &hello.manifest.module_id,
1705 hello.manifest.capabilities.as_ref(),
1706 );
1707 if let Some(refusal) = reserved_capability_refusals.first() {
1708 let capability = refusal.capability.clone();
1709 let bound_module = refusal.claimants[0].clone();
1710 log_duplicate_claim_events(reserved_capability_refusals);
1711 return Ok(vec![control_error_frame(
1712 &frame,
1713 "reserved_capability",
1714 format!(
1715 "capability '{}' is reserved for module_id '{}'; claimant '{}' was refused",
1716 capability, bound_module, hello.manifest.module_id
1717 ),
1718 )?]);
1719 }
1720
1721 if self
1724 .forwarding
1725 .connection_has_client_routes(connection_id)
1726 .map_err(RouterError::Forwarding)?
1727 {
1728 return Ok(vec![control_error_frame(
1729 &frame,
1730 "invalid_hello",
1731 "connection has open client routes and cannot also register as a module",
1732 )?]);
1733 }
1734
1735 let control_ops = effective_module_control_ops(hello.control_ops);
1736 if swap_candidate {
1737 return self.register_swap_candidate(
1738 connection_id,
1739 sink,
1740 &frame,
1741 hello.manifest,
1742 negotiated_ver,
1743 control_ops,
1744 );
1745 }
1746 let registration = match self.registry.register_with_control_ops(
1747 hello.manifest,
1748 negotiated_ver,
1749 connection_id,
1750 control_ops,
1751 ) {
1752 Ok(registration) => registration,
1753 Err(RegistryError::DuplicateModuleId { module_id }) => {
1754 return Ok(vec![control_error_frame(
1755 &frame,
1756 "duplicate_module_id",
1757 format!(
1758 "module_id '{module_id}' is already registered; duplicate HELLO rejected"
1759 ),
1760 )?])
1761 }
1762 Err(err @ RegistryError::PathHazardModuleId { .. }) => {
1763 return Ok(vec![control_error_frame(
1764 &frame,
1765 "invalid_module_id",
1766 err.to_string(),
1767 )?])
1768 }
1769 Err(err) => {
1770 return Ok(vec![control_error_frame(
1771 &frame,
1772 "registry_error",
1773 err.to_string(),
1774 )?])
1775 }
1776 };
1777
1778 if let Some(sink) = sink {
1779 let concurrency = manifest_concurrency(®istration.manifest);
1788 if let Err(err) = self.forwarding.register_module_connection(
1789 connection_id,
1790 registration.manifest.module_id.clone(),
1791 negotiated_ver,
1792 concurrency,
1793 sink,
1794 ) {
1795 if matches!(self.deregister_connection(connection_id), Ok(r) if !r.is_empty()) {
1799 crate::supervise::notify_registration_release();
1800 }
1801 return Ok(vec![control_error_frame(
1802 &frame,
1803 forwarding_error_code(&err),
1804 err.to_string(),
1805 )?]);
1806 }
1807 }
1808
1809 if manifest_concurrency_was_defaulted(&frame.body, ®istration.manifest) {
1818 info!(
1819 module_id = %registration.manifest.module_id,
1820 "management surface registered with DEFAULTED concurrency=module_managed (manifest predates the field; declare the real lane)"
1821 );
1822 }
1823
1824 self.apply_registration_capabilities(®istration);
1825
1826 info!(
1827 module_id = %registration.manifest.module_id,
1828 module_version = %registration.manifest.module_version,
1829 negotiated_ver,
1830 routable_provider = manifest_provides_routable_role(®istration.manifest),
1831 connection_id = connection_id.get(),
1832 "module registered"
1833 );
1834
1835 self.hello_ack(&frame, negotiated_ver, ®istration.manifest.module_id)
1836 }
1837
1838 fn register_swap_candidate(
1848 &self,
1849 connection_id: ConnectionId,
1850 sink: Option<crate::FrameSink>,
1851 frame: &Frame,
1852 manifest: ModuleManifest,
1853 negotiated_ver: u8,
1854 control_ops: Vec<String>,
1855 ) -> Result<Vec<Frame>, RouterError> {
1856 let module_id = manifest.module_id.clone();
1857 let registration = match self.registry.register_candidate_with_control_ops(
1858 manifest,
1859 negotiated_ver,
1860 connection_id,
1861 control_ops,
1862 ) {
1863 Ok(registration) => registration,
1864 Err(RegistryError::DuplicateModuleId { module_id }) => {
1865 return Ok(vec![control_error_frame(
1866 frame,
1867 "duplicate_module_id",
1868 format!(
1869 "module_id '{module_id}' already has a swap candidate registered; duplicate HELLO rejected"
1870 ),
1871 )?])
1872 }
1873 Err(err @ RegistryError::PathHazardModuleId { .. }) => {
1874 return Ok(vec![control_error_frame(
1875 frame,
1876 "invalid_module_id",
1877 err.to_string(),
1878 )?])
1879 }
1880 Err(err) => {
1881 return Ok(vec![control_error_frame(
1882 frame,
1883 "registry_error",
1884 err.to_string(),
1885 )?])
1886 }
1887 };
1888 if let Some(sink) = sink {
1889 let concurrency = manifest_concurrency(®istration.manifest);
1890 if let Err(err) = self.forwarding.register_candidate_module_connection(
1891 connection_id,
1892 module_id.clone(),
1893 negotiated_ver,
1894 concurrency,
1895 sink,
1896 ) {
1897 if matches!(self.deregister_connection(connection_id), Ok(r) if !r.is_empty()) {
1898 crate::supervise::notify_registration_release();
1899 }
1900 return Ok(vec![control_error_frame(
1901 frame,
1902 forwarding_error_code(&err),
1903 err.to_string(),
1904 )?]);
1905 }
1906 }
1907 self.supervisor.mark_swap_candidate_admitted(&module_id);
1908 info!(
1909 module_id = %module_id,
1910 module_version = %registration.manifest.module_version,
1911 negotiated_ver,
1912 ready = registration.ready,
1913 connection_id = connection_id.get(),
1914 "swap candidate registered; not routable until cutover"
1915 );
1916 self.hello_ack(frame, negotiated_ver, &module_id)
1917 }
1918
1919 fn hello_ack(
1920 &self,
1921 frame: &Frame,
1922 negotiated_ver: u8,
1923 module_id: &str,
1924 ) -> Result<Vec<Frame>, RouterError> {
1925 let ack = ModuleHelloAckBody {
1926 negotiated_ver,
1927 subc_ops: module_subc_ops(),
1928 subc_capabilities: self.subc_capabilities.as_ref().to_vec(),
1929 storage: self
1930 .storage_config
1931 .as_ref()
1932 .map(|cfg| cfg.descriptor_for(module_id)),
1933 machine_id: self.machine_id.as_ref().map(|id| id.as_str().to_owned()),
1934 };
1935 let body = serde_json::to_vec(&ack).map_err(|err| {
1936 RouterError::backend(
1937 0,
1938 frame.header.corr,
1939 format!("failed to encode HELLO_ACK: {err}"),
1940 )
1941 })?;
1942
1943 Ok(vec![Frame::build_with_version(
1944 negotiated_ver,
1945 FrameType::HelloAck,
1946 control_flags(),
1947 0,
1948 0,
1949 frame.header.corr,
1950 body,
1951 )
1952 .map_err(RouterError::FrameBuild)?])
1953 }
1954
1955 async fn handle_client_control_request(
1956 &self,
1957 ctx: &RouteCtx,
1958 frame: Frame,
1959 request: ClientControlRequest,
1960 ) -> Result<Vec<Frame>, RouterError> {
1961 match request {
1962 ClientControlRequest::ServerDescribe {} => self.handle_server_describe(frame),
1963 ClientControlRequest::CatalogList { module_id } => {
1964 self.handle_catalog_list(frame, module_id)
1965 }
1966 ClientControlRequest::RouteOpen {
1967 target,
1968 identity,
1969 consumer_identity,
1970 consumer_capabilities,
1971 admission_facts,
1972 } => {
1973 self.handle_route_open(
1974 ctx,
1975 frame,
1976 RouteOpenRequest {
1977 target,
1978 identity,
1979 consumer_identity,
1980 consumer_capabilities,
1981 admission_facts,
1982 },
1983 )
1984 .await
1985 }
1986 ClientControlRequest::RoutePoll {
1987 route_channel,
1988 route_epoch,
1989 kind,
1990 } => self.handle_route_poll(ctx, frame, route_channel, route_epoch, kind),
1991 ClientControlRequest::SupervisorList {} => self.handle_supervisor_list(frame),
1992 ClientControlRequest::SupervisorSpawnSnapshot {} => {
1993 self.handle_supervisor_spawn_snapshot(frame)
1994 }
1995 ClientControlRequest::SupervisorSpawnSubscribe { since } => {
1996 self.handle_supervisor_spawn_subscribe(ctx, frame, since)
1997 }
1998 ClientControlRequest::SupervisorRestart {
1999 module_id,
2000 drain_timeout_ms,
2001 } => {
2002 self.handle_supervisor_restart(frame, module_id, drain_timeout_ms)
2003 .await
2004 }
2005 ClientControlRequest::SupervisorSwap {
2006 module_id,
2007 ready_timeout_ms,
2008 } => {
2009 self.handle_supervisor_swap(frame, module_id, ready_timeout_ms)
2010 .await
2011 }
2012 ClientControlRequest::SupervisorReload { module_id } => {
2013 self.handle_supervisor_reload(frame, module_id).await
2014 }
2015 ClientControlRequest::SupervisorRescan { preview } => {
2016 self.handle_supervisor_rescan(frame, preview).await
2017 }
2018 ClientControlRequest::SupervisorReleaseReserved { module_id } => {
2019 self.handle_supervisor_release_reserved(frame, module_id)
2020 .await
2021 }
2022 ClientControlRequest::SupervisorSetEnabled { module_id, enabled } => {
2023 self.handle_supervisor_set_enabled(frame, module_id, enabled)
2024 .await
2025 }
2026 ClientControlRequest::SupervisorHealthProbe { module_id } => {
2027 self.handle_supervisor_health_probe(frame, module_id).await
2028 }
2029 ClientControlRequest::SupervisorHealth {} => self.handle_supervisor_health(frame),
2030 ClientControlRequest::SupervisorRoutes { module_id } => {
2031 self.handle_supervisor_routes(frame, module_id)
2032 }
2033 ClientControlRequest::SupervisorProvenance { module_id } => {
2034 self.handle_supervisor_provenance(frame, module_id).await
2035 }
2036 ClientControlRequest::SupervisorStderrTail {
2037 module_id,
2038 max_lines,
2039 max_bytes,
2040 } => self.handle_supervisor_stderr_tail(frame, module_id, max_lines, max_bytes),
2041 ClientControlRequest::SupervisorTerminals { module_id } => {
2042 self.handle_supervisor_terminals(frame, module_id).await
2043 }
2044 }
2045 }
2046
2047 fn handle_module_control_request(
2048 &self,
2049 connection_id: ConnectionId,
2050 frame: Frame,
2051 request: ModuleControlRequestFromModule,
2052 ) -> Result<Vec<Frame>, RouterError> {
2053 match request {
2054 ModuleControlRequestFromModule::CatalogUpdate {
2055 provides,
2056 capabilities,
2057 ready,
2058 } => self.handle_catalog_update(connection_id, frame, provides, capabilities, ready),
2059 ModuleControlRequestFromModule::LiveRoots {} => {
2060 let registered = self
2061 .registry
2062 .get_module_by_connection(connection_id)
2063 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
2064 let Some(registration) = registered else {
2065 return Ok(vec![control_error_frame(&frame, "not_registered", "supervisor.live_roots requires an active module registration owned by this connection")?]);
2066 };
2067 let response = self
2068 .forwarding
2069 .live_roots(®istration.manifest.module_id)
2070 .map_err(RouterError::Forwarding)?;
2071 Ok(vec![control_response_body_frame(
2072 &frame,
2073 &response,
2074 "ModuleControlResponseToModule::LiveRoots",
2075 )?])
2076 }
2077 }
2078 }
2079
2080 fn handle_catalog_update(
2081 &self,
2082 connection_id: ConnectionId,
2083 frame: Frame,
2084 provides: Vec<ProviderRole>,
2085 capabilities: Option<CapabilityDeclarations>,
2086 ready: Option<bool>,
2087 ) -> Result<Vec<Frame>, RouterError> {
2088 self.refresh_capability_requirements();
2089 let Some(registration) = self
2090 .registry
2091 .get_module_by_connection(connection_id)
2092 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
2093 else {
2094 return Ok(vec![control_error_frame(
2095 &frame,
2096 "not_registered",
2097 "catalog.update requires an active module registration owned by this connection",
2098 )?]);
2099 };
2100
2101 if let Some(message) =
2102 catalog_update_frozen_field_message(®istration.manifest, &provides)
2103 {
2104 return Ok(vec![control_error_frame(
2105 &frame,
2106 "catalog_update_frozen_field",
2107 message,
2108 )?]);
2109 }
2110
2111 let mut candidate = registration.manifest.clone();
2112 candidate.provides = provides.clone();
2113 candidate.capabilities = capabilities
2114 .clone()
2115 .or_else(|| registration.manifest.capabilities.clone());
2116 if let Err(err) = candidate.validate_capability_grammar() {
2117 return Ok(vec![control_error_frame(
2118 &frame,
2119 "invalid_capability_grammar",
2120 err.to_string(),
2121 )?]);
2122 }
2123
2124 let updated = self
2125 .registry
2126 .replace_catalog_for_connection(connection_id, provides, capabilities, ready)
2127 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
2128 if updated.is_none() {
2129 return Ok(vec![control_error_frame(
2130 &frame,
2131 "not_registered",
2132 "catalog.update requires an active module registration owned by this connection",
2133 )?]);
2134 }
2135 if let Ok((_, registrations)) = self.runtime_capability_snapshot() {
2136 log_duplicate_claim_events(
2137 self.capability_evaluator
2138 .duplicate_claims(DuplicateClaimSource::CatalogUpdate, ®istrations),
2139 );
2140 }
2141 if capability_census_trigger(
2142 registration.manifest.capabilities.as_ref(),
2143 updated
2144 .as_ref()
2145 .and_then(|entry| entry.manifest.capabilities.as_ref()),
2146 ) {
2147 self.enforce_capability_denies();
2148 }
2149 self.refresh_capability_requirements();
2150
2151 let response = ModuleControlResponseToModule::CatalogUpdate {};
2152 control_response_body_frame(
2153 &frame,
2154 &response,
2155 "ModuleControlResponseToModule::CatalogUpdate",
2156 )
2157 .map(|frame| vec![frame])
2158 }
2159
2160 fn handle_server_describe(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
2161 self.refresh_capability_requirements();
2162 let mut counters = self.counters.snapshot();
2168 if let (Ok((connections_with_routes, max)), Some(obj)) = (
2169 self.forwarding.client_route_concentration(),
2170 counters.as_object_mut(),
2171 ) {
2172 obj.insert(
2173 "client_connections_with_routes".into(),
2174 connections_with_routes.into(),
2175 );
2176 obj.insert("max_routes_on_one_connection".into(), max.into());
2177 }
2178 if let (Some(open_breakers), Some(obj)) = (
2184 self.route_bind_breakers.open_snapshot(),
2185 counters.as_object_mut(),
2186 ) {
2187 obj.insert("route_bind_breakers_open".into(), open_breakers);
2188 }
2189 let response = ClientControlResponse::ServerDescribe {
2190 protocol_ver: PROTOCOL_VERSION,
2191 subc_ops: subc_ops(),
2192 capabilities: self.subc_capabilities.as_ref().to_vec(),
2193 connected_clients: self.connected_clients.count(),
2194 counters: Some(counters),
2195 build_git_sha: Some(env!("SUBC_BUILD_GIT_SHA").to_string()),
2196 build_lock_digest: Some(env!("SUBC_BUILD_LOCK_DIGEST").to_string()),
2197 capability_requirements: self.capability_requirement_statuses(),
2198 machine_id: self.machine_id.as_ref().map(|id| id.as_str().to_owned()),
2199 };
2200 Ok(vec![control_response_body_frame(
2201 &frame,
2202 &response,
2203 "ClientControlResponse::ServerDescribe",
2204 )?])
2205 }
2206
2207 fn handle_catalog_list(
2208 &self,
2209 frame: Frame,
2210 module_id: Option<String>,
2211 ) -> Result<Vec<Frame>, RouterError> {
2212 let (generation, modules) = self.registry.list_modules().map_err(|err| {
2213 RouterError::backend(0, frame.header.corr, format!("registry error: {err}"))
2214 })?;
2215 let entries = modules
2216 .into_iter()
2217 .filter(|registration| {
2218 module_id
2219 .as_deref()
2220 .map(|wanted| registration.manifest.module_id == wanted)
2221 .unwrap_or(true)
2222 })
2223 .map(|registration| {
2224 let not_ready = self.not_ready_reason(®istration);
2225 let roles = registration.manifest.provides;
2226 CatalogEntry {
2227 module_id: registration.manifest.module_id,
2228 ready: not_ready.is_none(),
2229 not_ready,
2230 module_version: Some(registration.manifest.module_version),
2231 roles,
2232 control_ops: registration.control_ops,
2233 capabilities: registration.manifest.capabilities,
2234 self_signals: registration.manifest.self_signals,
2235 }
2236 })
2237 .collect();
2238 let response = ClientControlResponse::CatalogList {
2239 generation,
2240 modules: entries,
2241 subc_ops: subc_ops(),
2242 };
2243 Ok(vec![control_response_body_frame(
2244 &frame,
2245 &response,
2246 "ClientControlResponse::CatalogList",
2247 )?])
2248 }
2249
2250 fn route_open_principal(
2251 &self,
2252 frame: &Frame,
2253 consumer_identity: Option<ConsumerIdentity>,
2254 ) -> Result<Result<Principal, Frame>, RouterError> {
2255 let Some(consumer_identity) = consumer_identity else {
2256 return Ok(Ok(Principal::Direct));
2257 };
2258
2259 if self.supervisor.spawned_consumer_authorized(
2260 &consumer_identity.module_id,
2261 &consumer_identity.launch_nonce,
2262 ) {
2263 return Ok(Ok(Principal::Reserved {
2264 module_id: consumer_identity.module_id,
2265 }));
2266 }
2267
2268 Ok(Err(control_error_frame(
2269 frame,
2270 "bad_consumer_identity",
2271 format!(
2272 "consumer_identity for module_id '{}' did not match a supervised launch nonce",
2273 consumer_identity.module_id
2274 ),
2275 )?))
2276 }
2277
2278 fn route_open_refusal_frame(
2284 &self,
2285 ctx: &RouteCtx,
2286 frame: &Frame,
2287 module_id: &str,
2288 reason: &'static str,
2289 code: &'static str,
2290 message: impl Into<String>,
2291 ) -> Result<Frame, RouterError> {
2292 self.observe_route_open_refusal(ctx, module_id, reason, code);
2293 control_error_frame(frame, code, message.into())
2294 }
2295
2296 fn route_open_breaker_refusal_frame(
2314 &self,
2315 ctx: &RouteCtx,
2316 frame: &Frame,
2317 module_id: &str,
2318 consecutive_timeouts: u32,
2319 retry_in: Duration,
2320 probe_in_flight: bool,
2321 ) -> Result<Frame, RouterError> {
2322 self.counters
2323 .increment_route_open_refused(crate::observability::ROUTE_OPEN_REFUSED_BREAKER_OPEN);
2324 debug!(
2325 target: "control",
2326 code = "module_timeout",
2327 module_id = ?module_id,
2328 connection_id = ctx.connection_id.get(),
2329 consecutive_timeouts,
2330 retry_in_ms = retry_in.as_millis() as u64,
2331 probe_in_flight,
2332 "route.open refused by open bind-relay breaker"
2333 );
2334 let detail = if probe_in_flight {
2335 "one probe bind is already in flight; retry once it settles".to_string()
2336 } else {
2337 format!("not relaying for another {retry_in:?}")
2338 };
2339 control_error_frame(
2340 frame,
2341 "module_timeout",
2342 format!(
2343 "module_id '{module_id}' failed {consecutive_timeouts} consecutive route.bind \
2344 relays; {detail}"
2345 ),
2346 )
2347 }
2348
2349 fn observe_route_open_refusal(
2362 &self,
2363 ctx: &RouteCtx,
2364 module_id: &str,
2365 reason: &'static str,
2366 code: &'static str,
2367 ) {
2368 self.counters.increment_route_open_refused(code);
2369 info!(
2370 target: "control",
2371 code,
2372 reason,
2373 module_id = ?module_id,
2374 connection_id = ctx.connection_id.get(),
2375 "route.open refused"
2376 );
2377 }
2378
2379 fn observe_route_open_accept(&self, ctx: &RouteCtx, module_id: &str, principal: &str) {
2424 self.counters.increment_route_open_accepted(principal);
2425 info!(
2426 target: "control",
2427 principal,
2428 module_id,
2429 connection_id = ctx.connection_id.get(),
2430 "route.open accepted"
2431 );
2432 }
2433
2434 fn supervised_absent_route_open_refusal_frame(
2435 &self,
2436 ctx: &RouteCtx,
2437 frame: &Frame,
2438 module_id: &str,
2439 code: &'static str,
2440 status: &crate::supervise::ModuleStatus,
2441 ) -> Result<Frame, RouterError> {
2442 self.counters.increment_route_open_refused(code);
2443 info!(
2444 target: "control",
2445 code,
2446 reason = "supervised_not_registered",
2447 module_id = ?module_id,
2448 connection_id = ctx.connection_id.get(),
2449 state = %status.state,
2450 enabled = status.enabled,
2451 live = status.live,
2452 "route.open refused"
2453 );
2454 control_error_frame(
2455 frame,
2456 code,
2457 format!(
2458 "module_id '{module_id}' is supervised but not available (state={}, enabled={}, live={})",
2459 status.state, status.enabled, status.live
2460 ),
2461 )
2462 }
2463
2464 async fn handle_route_open(
2465 &self,
2466 ctx: &RouteCtx,
2467 frame: Frame,
2468 request: RouteOpenRequest,
2469 ) -> Result<Vec<Frame>, RouterError> {
2470 let RouteOpenRequest {
2471 target,
2472 mut identity,
2473 consumer_identity,
2474 consumer_capabilities,
2475 admission_facts,
2476 } = request;
2477 let target_module_id = target_module_id(&target).to_string();
2478 debug!(
2479 connection_id = ctx.connection_id.get(),
2480 corr = frame.header.corr,
2481 module_id = %target_module_id,
2482 "handling route.open"
2483 );
2484
2485 let Some(registration) = self
2504 .registry
2505 .get_module(&target_module_id)
2506 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
2507 else {
2508 if let Some((status, warming)) =
2509 self.supervisor_status(&target_module_id, frame.header.corr)?
2510 {
2511 if status.protocol == ModuleProtocol::None {
2518 return Ok(vec![self.route_open_refusal_frame(
2519 ctx,
2520 &frame,
2521 &target_module_id,
2522 "protocol_none",
2523 error_codes::MODULE_NO_PROTOCOL,
2524 format!(
2525 "module_id '{target_module_id}' is declared protocol: none; \
2526 it speaks no subc wire and serves no routes"
2527 ),
2528 )?]);
2529 }
2530 let code = if warming {
2531 "module_warming"
2532 } else {
2533 "target_unavailable"
2534 };
2535 return Ok(vec![self.supervised_absent_route_open_refusal_frame(
2536 ctx,
2537 &frame,
2538 &target_module_id,
2539 code,
2540 &status,
2541 )?]);
2542 }
2543 if let Some(removed_ago_ms) =
2544 self.supervisor.removal_tombstone_age_ms(&target_module_id)
2545 {
2546 return Ok(vec![self.route_open_refusal_frame(
2547 ctx,
2548 &frame,
2549 &target_module_id,
2550 "removed",
2551 error_codes::MODULE_REMOVED,
2552 format!("module_id '{target_module_id}' was removed {removed_ago_ms} ms ago"),
2553 )?]);
2554 }
2555 return Ok(vec![self.route_open_refusal_frame(
2556 ctx,
2557 &frame,
2558 &target_module_id,
2559 "not_registered",
2560 error_codes::UNKNOWN_MODULE,
2561 format!("module_id '{target_module_id}' is not registered"),
2562 )?]);
2563 };
2564
2565 if !registration.ready {
2569 self.counters
2570 .increment_route_open_refused(ROUTE_OPEN_REFUSED_DECLARED_NOT_READY);
2571 info!(
2572 target: "control",
2573 code = error_codes::MODULE_WARMING,
2574 module_id = ?target_module_id,
2575 connection_id = ctx.connection_id.get(),
2576 reason = "declared_not_ready",
2577 "route.open refused"
2578 );
2579 return Ok(vec![control_error_body_frame(
2580 &frame,
2581 ErrorBody {
2582 code: error_codes::MODULE_WARMING.to_string(),
2583 message: format!(
2584 "module_id '{target_module_id}' is registered and has declared itself not ready; retry"
2585 ),
2586 detail: Some(serde_json::json!({
2587 "reason": "declared_not_ready"
2588 })),
2589 },
2590 )?]);
2591 }
2592
2593 if let Some(capability) = self.first_unprovided_required_capability(®istration) {
2612 self.counters
2613 .increment_route_open_refused(ROUTE_OPEN_REFUSED_REQUIRED_CAPABILITY_UNPROVIDED);
2614 info!(
2615 target: "control",
2616 code = error_codes::MODULE_WARMING,
2617 module_id = ?target_module_id,
2618 connection_id = ctx.connection_id.get(),
2619 reason = NotReadyReason::REQUIRED_CAPABILITY_UNPROVIDED,
2620 capability = %capability,
2621 "route.open refused"
2622 );
2623 return Ok(vec![control_error_body_frame(
2624 &frame,
2625 ErrorBody {
2626 code: error_codes::MODULE_WARMING.to_string(),
2627 message: format!(
2628 "module_id '{target_module_id}' requires capability '{capability}', \
2629 which no registered module provides; retry"
2630 ),
2631 detail: Some(serde_json::json!({
2632 "reason": NotReadyReason::REQUIRED_CAPABILITY_UNPROVIDED,
2633 "capability": capability,
2634 })),
2635 },
2636 )?]);
2637 }
2638
2639 if !target_has_required_role(&target, ®istration.manifest.provides) {
2640 return Ok(vec![self.route_open_refusal_frame(
2641 ctx,
2642 &frame,
2643 &target_module_id,
2644 "role_not_provided",
2645 "target_unavailable",
2646 format!("module_id '{target_module_id}' does not provide the requested target"),
2647 )?]);
2648 }
2649
2650 if registration.state != ChannelState::Active {
2651 return Ok(vec![self.route_open_refusal_frame(
2652 ctx,
2653 &frame,
2654 &target_module_id,
2655 "registration_not_active",
2656 "target_unavailable",
2657 format!("module_id '{target_module_id}' is not active"),
2658 )?]);
2659 }
2660
2661 if self
2662 .forwarding
2663 .module_is_draining(&target_module_id)
2664 .map_err(RouterError::Forwarding)?
2665 {
2666 return Ok(vec![self.route_open_refusal_frame(
2667 ctx,
2668 &frame,
2669 &target_module_id,
2670 "reloading",
2671 "module_reloading",
2672 format!("module_id '{target_module_id}' is reloading"),
2673 )?]);
2674 }
2675
2676 if self
2677 .process_liveness
2678 .as_ref()
2679 .and_then(|process_liveness| process_liveness.process_live(&target_module_id))
2680 == Some(false)
2681 {
2682 return Ok(vec![self.route_open_refusal_frame(
2683 ctx,
2684 &frame,
2685 &target_module_id,
2686 "supervisor_not_live",
2687 "target_unavailable",
2688 format!("module_id '{target_module_id}' is not live"),
2689 )?]);
2690 }
2691
2692 if !self
2693 .forwarding
2694 .has_live_module_connection(&target_module_id)
2695 .map_err(RouterError::Forwarding)?
2696 {
2697 return Ok(vec![self.route_open_refusal_frame(
2698 ctx,
2699 &frame,
2700 &target_module_id,
2701 "no_forwarding_connection",
2702 "target_unavailable",
2703 format!("module_id '{target_module_id}' has no live forwarding connection"),
2704 )?]);
2705 }
2706
2707 if let Some(error) =
2708 self.guard_module_control_op(&frame, &target_module_id, "route.bind")?
2709 {
2710 self.observe_route_open_refusal(
2711 ctx,
2712 &target_module_id,
2713 "op_not_allowed",
2714 "op_not_allowed",
2715 );
2716 return Ok(vec![error]);
2717 }
2718
2719 let principal = match self.route_open_principal(&frame, consumer_identity)? {
2720 Ok(principal) => principal,
2721 Err(error) => {
2722 self.observe_route_open_refusal(
2723 ctx,
2724 &target_module_id,
2725 "bad_consumer_identity",
2726 "bad_consumer_identity",
2727 );
2728 return Ok(vec![error]);
2729 }
2730 };
2731
2732 if let Principal::Reserved {
2736 module_id: opening_module_id,
2737 } = &principal
2738 {
2739 if let Some(opening_registration) = self
2740 .registry
2741 .get_module(opening_module_id)
2742 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
2743 {
2744 if let Some(capability) =
2745 denied_capability(&opening_registration.manifest, ®istration.manifest)
2746 {
2747 warn!(
2748 opening_module_id,
2749 target_module_id,
2750 capability,
2751 "refusing route.open because an attested capability deny edge matches"
2752 );
2753 return Ok(vec![self.route_open_refusal_frame(
2754 ctx,
2755 &frame,
2756 &target_module_id,
2757 "capability_deny_edge",
2758 "capability_forbidden",
2759 format!(
2760 "module_id '{opening_module_id}' must never reach capability '{capability}' provided by '{target_module_id}'"
2761 ),
2762 )?]);
2763 }
2764 }
2765 }
2766
2767 if admission_facts.is_some() {
2768 let carrier_matches = matches!(
2769 &principal,
2770 Principal::Reserved { module_id }
2771 if self.admission_facts_carrier_module_id.as_deref() == Some(module_id)
2772 );
2773 if !carrier_matches {
2774 return Ok(vec![self.route_open_refusal_frame(
2775 ctx,
2776 &frame,
2777 &target_module_id,
2778 "admission_facts_carrier_not_permitted",
2779 "admission_facts_not_permitted",
2780 "admission facts may only be carried by the configured reserved module",
2781 )?]);
2782 }
2783
2784 let target_allowed = self
2785 .admission_facts_targets
2786 .as_ref()
2787 .is_some_and(|targets| targets.iter().any(|id| id == &target_module_id));
2788 if !target_allowed {
2789 return Ok(vec![self.route_open_refusal_frame(
2790 ctx,
2791 &frame,
2792 &target_module_id,
2793 "admission_facts_target_not_listed",
2794 "admission_facts_target_not_allowed",
2795 format!(
2796 "admission facts are not permitted for target module_id '{target_module_id}'"
2797 ),
2798 )?]);
2799 }
2800
2801 }
2805
2806 let project_root = match ProjectRootId::from_path_allowing_missing(&identity.project_root) {
2828 Ok(project_root) => project_root,
2829 Err(err) => {
2830 return Ok(vec![control_error_frame(
2831 &frame,
2832 "invalid_project_root",
2833 err.to_string(),
2834 )?])
2835 }
2836 };
2837 identity.project_root = project_root.as_path().to_path_buf();
2838
2839 let _concurrency_guard = match self
2850 .route_bind_concurrency
2851 .try_admit(&target_module_id, MAX_PENDING_ROUTE_BINDS_PER_TARGET)
2852 {
2853 Ok(guard) => guard,
2854 Err(in_flight) => {
2855 return Ok(vec![self.route_open_admission_refusal_frame(
2856 ctx,
2857 &frame,
2858 &target_module_id,
2859 format!(
2860 "module_id '{target_module_id}' already has {in_flight} route.bind relays in flight; retry after one settles"
2861 ),
2862 )?]);
2863 }
2864 };
2865
2866 let mut breaker = match self.route_bind_breakers.admit(&target_module_id) {
2869 RouteBindAdmission::Admitted { guard, probe } => {
2870 if probe {
2871 info!(
2872 module_id = %target_module_id,
2873 connection_id = ctx.connection_id.get(),
2874 "route.bind breaker half-open: admitting one probe"
2875 );
2876 }
2877 guard
2878 }
2879 RouteBindAdmission::Refused {
2880 consecutive_timeouts,
2881 retry_in,
2882 probe_in_flight,
2883 } => {
2884 return Ok(vec![self.route_open_breaker_refusal_frame(
2885 ctx,
2886 &frame,
2887 &target_module_id,
2888 consecutive_timeouts,
2889 retry_in,
2890 probe_in_flight,
2891 )?]);
2892 }
2893 };
2894
2895 let route_bind_relay_timeout = self.route_bind_relay_timeout_for(&target_module_id);
2900 let relay_deadline = Instant::now() + route_bind_relay_timeout;
2901 let pending = match self
2902 .forwarding
2903 .begin_route_bind_relay_for(
2904 ctx.connection_id,
2905 ctx.egress.clone(),
2906 response_version(&frame),
2907 frame.header.corr,
2908 &target_module_id,
2909 principal.clone(),
2910 Some(project_root),
2911 relay_deadline,
2912 )
2913 .await
2914 {
2915 Ok(pending) => pending,
2916 Err(err) => {
2917 return Ok(vec![self.route_open_refusal_frame(
2918 ctx,
2919 &frame,
2920 &target_module_id,
2921 "relay_reservation_failed",
2922 forwarding_error_code(&err),
2923 err.to_string(),
2924 )?])
2925 }
2926 };
2927 let crate::forwarding::PendingRouteBindRelay {
2928 endpoint,
2929 module_sink,
2930 negotiated_ver,
2931 client_channel,
2932 client_epoch,
2933 module_channel,
2934 module_epoch,
2935 corr: relay_corr,
2936 receiver,
2937 } = pending;
2938 let mut reservation =
2939 RouteBindReservationGuard::new(Arc::clone(&self.forwarding), endpoint, relay_corr);
2940
2941 debug!(
2942 connection_id = ctx.connection_id.get(),
2943 client_channel,
2944 client_epoch,
2945 module_channel,
2946 module_epoch,
2947 "reserved route handle pair"
2948 );
2949 let principal_label = match &principal {
2952 Principal::Reserved { module_id } => format!("reserved:{module_id}"),
2953 Principal::Direct => "direct".to_string(),
2954 other => format!("{other:?}"),
2955 };
2956 let relay = ModuleControlRequest::RouteBind {
2957 route_channel: module_channel,
2958 epoch: module_epoch,
2959 target,
2960 identity,
2961 principal: Some(principal),
2962 consumer_capabilities,
2963 admission_facts,
2964 };
2965 let relay_body = serde_json::to_vec(&relay).map_err(|err| {
2966 RouterError::backend(
2967 0,
2968 frame.header.corr,
2969 format!("failed to encode route.bind request: {err}"),
2970 )
2971 })?;
2972 let relay_frame = Frame::build_with_version(
2973 negotiated_ver,
2974 FrameType::Request,
2975 control_flags(),
2976 0,
2977 0,
2978 relay_corr,
2979 relay_body,
2980 )
2981 .map_err(RouterError::FrameBuild)?;
2982
2983 if let Err(err) = module_sink.send(relay_frame).await {
2984 reservation.release_and_disarm();
2985 return Ok(vec![self.route_open_refusal_frame(
2986 ctx,
2987 &frame,
2988 &target_module_id,
2989 "relay_send_failed",
2990 "target_unavailable",
2991 err.to_string(),
2992 )?]);
2993 }
2994
2995 if !self
2996 .forwarding
2997 .mark_route_bind_relay_enqueued(endpoint, relay_corr)
2998 .map_err(RouterError::Forwarding)?
2999 {
3000 self.send_abandoned_route_bind_goodbye(
3001 &module_sink,
3002 negotiated_ver,
3003 module_channel,
3004 module_epoch,
3005 );
3006 }
3007
3008 match timeout_at(relay_deadline, receiver).await {
3009 Ok(Ok(RouteBindRelayOutcome::Accepted)) => {
3010 reservation.disarm();
3011 if breaker.record_accepted() {
3012 info!(
3013 module_id = %target_module_id,
3014 "route.bind breaker closed: the probe was accepted"
3015 );
3016 }
3017 self.observe_route_open_accept(ctx, &target_module_id, &principal_label);
3018 Ok(Vec::new())
3019 }
3020 Ok(Ok(RouteBindRelayOutcome::Rejected(body))) => {
3021 reservation.release_and_disarm();
3022 breaker.record_inconclusive();
3026 self.counters
3027 .increment_route_open_refused("module_rejected");
3028 info!(
3029 target: "control",
3030 code = "module_rejected",
3031 module_code = ?body.code,
3032 module_id = ?target_module_id,
3033 connection_id = ctx.connection_id.get(),
3034 "route.open refused"
3035 );
3036 Ok(vec![control_error_body_frame(&frame, body)?])
3037 }
3038 Ok(Ok(RouteBindRelayOutcome::ModuleGone(message))) => {
3039 reservation.release_and_disarm();
3040 breaker.record_inconclusive();
3041 tracing::warn!(
3048 module_id = %target_module_id,
3049 "route.bind relay abandoned: {message}"
3050 );
3051 Ok(vec![self.route_open_refusal_frame(
3052 ctx,
3053 &frame,
3054 &target_module_id,
3055 "relay_abandoned",
3056 "target_unavailable",
3057 message,
3058 )?])
3059 }
3060 Ok(Err(_)) => {
3061 reservation.release_and_disarm();
3062 breaker.record_inconclusive();
3063 Ok(vec![self.route_open_refusal_frame(
3064 ctx,
3065 &frame,
3066 &target_module_id,
3067 "relay_waiter_canceled",
3068 "target_unavailable",
3069 "route.bind relay waiter was canceled before the module responded",
3070 )?])
3071 }
3072 Err(_) => {
3073 reservation.release_and_disarm();
3074 if let Some(opened) = breaker.record_timeout(
3078 self.route_bind_breaker_threshold,
3079 self.route_bind_breaker_cooldown,
3080 ) {
3081 warn!(
3082 module_id = %target_module_id,
3083 consecutive_timeouts = opened.consecutive_timeouts,
3084 cooldown_ms = self.route_bind_breaker_cooldown.as_millis() as u64,
3085 reopened_after_probe = opened.reopened_after_probe,
3086 "route.bind breaker open: refusing route.open for this module without relaying until one probe says it recovered"
3087 );
3088 }
3089 tracing::warn!(
3097 module_id = %target_module_id,
3098 timeout_ms = route_bind_relay_timeout.as_millis() as u64,
3099 "route.bind relay timed out: module did not ack within budget"
3100 );
3101 Ok(vec![self.route_open_refusal_frame(
3102 ctx,
3103 &frame,
3104 &target_module_id,
3105 "relay_timed_out",
3106 "module_timeout",
3107 format!(
3108 "module_id '{target_module_id}' did not answer route.bind within {:?}",
3109 route_bind_relay_timeout
3110 ),
3111 )?])
3112 }
3113 }
3114 }
3115
3116 fn handle_supervisor_spawn_snapshot(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
3117 let response = ClientControlResponse::SupervisorSpawnSnapshot {
3118 snapshot: self.supervisor.spawn_snapshot(),
3119 };
3120 Ok(vec![control_response_body_frame(
3121 &frame,
3122 &response,
3123 "ClientControlResponse::SupervisorSpawnSnapshot",
3124 )?])
3125 }
3126
3127 fn handle_supervisor_spawn_subscribe(
3128 &self,
3129 ctx: &RouteCtx,
3130 frame: Frame,
3131 since: Option<SpawnCursor>,
3132 ) -> Result<Vec<Frame>, RouterError> {
3133 match self.supervisor.subscribe_spawns(
3134 ctx.connection_id,
3135 frame.header.corr,
3136 response_version(&frame),
3137 since,
3138 ctx.egress.clone(),
3139 ) {
3140 Ok(()) => Ok(Vec::new()),
3141 Err(SpawnSubscribeRefusal::ForeignIncarnation { current }) => {
3142 Ok(vec![control_error_body_frame(
3143 &frame,
3144 ErrorBody {
3145 code: "spawn_cursor_incarnation_mismatch".to_string(),
3146 message: "spawn cursor belongs to a different daemon incarnation"
3147 .to_string(),
3148 detail: Some(serde_json::json!({
3149 "current_daemon_incarnation": current
3150 })),
3151 },
3152 )?])
3153 }
3154 Err(SpawnSubscribeRefusal::TooOld { oldest }) => Ok(vec![control_error_body_frame(
3155 &frame,
3156 ErrorBody {
3157 code: "spawn_cursor_too_old".to_string(),
3158 message: "spawn cursor predates the retained event ring".to_string(),
3159 detail: Some(serde_json::json!({
3160 "oldest_retained_cursor": oldest
3161 })),
3162 },
3163 )?]),
3164 Err(SpawnSubscribeRefusal::Frame(error)) => Err(RouterError::backend(
3165 0,
3166 frame.header.corr,
3167 format!("failed to open supervisor spawn subscription: {error}"),
3168 )),
3169 }
3170 }
3171
3172 fn handle_supervisor_list(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
3173 let generation = self
3174 .registry
3175 .generation()
3176 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
3177 let modules = self
3178 .supervisor
3179 .list()
3180 .into_iter()
3181 .map(|module| {
3182 let status = module.status_for_control("list").map_err(|err| {
3183 RouterError::backend(
3184 0,
3185 frame.header.corr,
3186 format!("failed to read supervisor status: {err}"),
3187 )
3188 })?;
3189 Ok(SupervisorEntry {
3190 module_id: status.module_id,
3191 state: status.state.to_string(),
3192 enabled: status.enabled,
3193 live: status.live,
3194 protocol: status.protocol,
3195 health: status.health.status,
3196 last_probe_ms: status.health.last_probe_ms,
3197 last_exit_code: status.last_exit.as_ref().and_then(|e| e.code),
3198 last_exit_signal: status.last_exit.as_ref().and_then(|e| e.signal),
3199 last_exit_ms: status.last_exit.as_ref().map(|e| e.at_ms),
3200 last_exit_kind: status.last_exit.as_ref().map(|e| e.kind.into()),
3201 restart_count: Some(status.restart_count),
3202 max_restarts: Some(status.max_restarts),
3203 lifetime_restarts: Some(status.lifetime_restarts),
3204 spawn_generation: Some(status.spawn_generation),
3205 restart_window_secs: Some(status.restart_window.as_secs()),
3206 drain_timeout_ms: Some(status.drain_timeout.as_millis() as u64),
3207 restart_backoff_ms: Some(status.restart_backoff.as_millis() as u64),
3208 restart_max_backoff_ms: Some(status.restart_max_backoff.as_millis() as u64),
3209 })
3210 })
3211 .collect::<Result<Vec<_>, RouterError>>()?;
3212 let response = ClientControlResponse::SupervisorList {
3213 generation,
3214 modules,
3215 };
3216 Ok(vec![control_response_body_frame(
3217 &frame,
3218 &response,
3219 "ClientControlResponse::SupervisorList",
3220 )?])
3221 }
3222
3223 fn handle_supervisor_stderr_tail(
3224 &self,
3225 frame: Frame,
3226 module_id: String,
3227 max_lines: Option<u32>,
3228 max_bytes: Option<u32>,
3229 ) -> Result<Vec<Frame>, RouterError> {
3230 let Some(module) = self.supervisor.get(&module_id) else {
3231 return Ok(vec![control_error_frame(
3232 &frame,
3233 "unknown_module",
3234 format!("module_id '{module_id}' is not supervised"),
3235 )?]);
3236 };
3237
3238 let snapshot = module.stderr_tail(
3239 max_lines.map(|value| value as usize),
3240 max_bytes.map(|value| value as usize),
3241 );
3242
3243 let response = ClientControlResponse::SupervisorStderrTail {
3244 module_id,
3245 tail: StderrTail {
3246 capture: match snapshot.capture {
3247 CaptureState::Captured => StderrCaptureState::Captured,
3248 CaptureState::Incomplete { reason } => {
3249 StderrCaptureState::Incomplete { reason }
3250 }
3251 CaptureState::NotCaptured { reason } => {
3252 StderrCaptureState::NotCaptured { reason }
3253 }
3254 },
3255 entries: snapshot
3256 .entries
3257 .into_iter()
3258 .map(|entry| match entry {
3259 TailEntry::Line { text, truncated } => {
3260 StderrTailEntry::Line { text, truncated }
3261 }
3262 TailEntry::ProcessStart => StderrTailEntry::ProcessStart,
3263 })
3264 .collect(),
3265 dropped_lines: snapshot.dropped_lines,
3266 },
3267 };
3268 Ok(vec![control_response_body_frame(
3269 &frame,
3270 &response,
3271 "ClientControlResponse::SupervisorStderrTail",
3272 )?])
3273 }
3274
3275 async fn handle_supervisor_terminals(
3276 &self,
3277 frame: Frame,
3278 module_id: String,
3279 ) -> Result<Vec<Frame>, RouterError> {
3280 let Some(module) = self.supervisor.get(&module_id) else {
3281 return Ok(vec![control_error_frame(
3282 &frame,
3283 "unknown_module",
3284 format!("module_id '{module_id}' is not supervised"),
3285 )?]);
3286 };
3287
3288 let terminals = module
3291 .read_durable_terminal_history()
3292 .await
3293 .map_err(|error| {
3294 RouterError::backend(
3295 0,
3296 frame.header.corr,
3297 format!("failed to read terminal history: {error}"),
3298 )
3299 })?;
3300 let response = ClientControlResponse::SupervisorTerminals {
3301 module_id,
3302 terminals,
3303 };
3304 Ok(vec![control_response_body_frame(
3305 &frame,
3306 &response,
3307 "ClientControlResponse::SupervisorTerminals",
3308 )?])
3309 }
3310
3311 fn handle_supervisor_routes(
3312 &self,
3313 frame: Frame,
3314 module_id: Option<String>,
3315 ) -> Result<Vec<Frame>, RouterError> {
3316 let modules = self
3317 .forwarding
3318 .route_census(module_id.as_deref())
3319 .map_err(RouterError::Forwarding)?
3320 .into_iter()
3321 .map(|(module_id, routes)| SupervisorRouteModule {
3322 module_id,
3323 routes: routes
3324 .into_iter()
3325 .map(|route| SupervisorRoute {
3326 consumer: match route.principal {
3327 Principal::Reserved { module_id } => {
3328 SupervisorRouteConsumer::Reserved { module_id }
3329 }
3330 Principal::Direct | Principal::Unverified => {
3331 SupervisorRouteConsumer::Direct {
3332 connection_id: route.goodbye_target.connection_id.get(),
3333 }
3334 }
3335 },
3336 age_ms: Instant::now()
3337 .saturating_duration_since(route.bound_at)
3338 .as_millis()
3339 .try_into()
3340 .unwrap_or(u64::MAX),
3341 draining: route.draining,
3342 drain_reason: route.drain_reason,
3343 })
3344 .collect(),
3345 })
3346 .collect();
3347 let response = ClientControlResponse::SupervisorRoutes { modules };
3348 Ok(vec![control_response_body_frame(
3349 &frame,
3350 &response,
3351 "ClientControlResponse::SupervisorRoutes",
3352 )?])
3353 }
3354
3355 async fn handle_supervisor_provenance(
3356 &self,
3357 frame: Frame,
3358 module_id: Option<String>,
3359 ) -> Result<Vec<Frame>, RouterError> {
3360 let mut selected = if let Some(module_id) = module_id {
3361 let Some(module) = self.supervisor.get(&module_id) else {
3362 return Ok(vec![control_error_frame(
3363 &frame,
3364 "unknown_module",
3365 format!("module_id '{module_id}' is not supervised"),
3366 )?]);
3367 };
3368 vec![module]
3369 } else {
3370 self.supervisor.list()
3371 };
3372
3373 let mut modules = Vec::with_capacity(selected.len());
3374 for module in selected.drain(..) {
3375 let status = module.status().map_err(|err| {
3376 RouterError::backend(
3377 0,
3378 frame.header.corr,
3379 format!("failed to read supervisor status: {err}"),
3380 )
3381 })?;
3382 let module_declared = self
3383 .registry
3384 .get_module(&status.module_id)
3385 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
3386 .and_then(|registration| registration.manifest.provenance)
3387 .map(|build| ModuleDeclaredProvenance::Reported { build })
3388 .unwrap_or(ModuleDeclaredProvenance::Unverifiable);
3389 #[cfg(test)]
3390 let running_image = match &self.provenance_probe_override {
3391 Some(result) => result.clone(),
3392 None => module.running_image_agreement().await,
3393 };
3394 #[cfg(not(test))]
3395 let running_image = module.running_image_agreement().await;
3396 modules.push(SupervisorModuleProvenance {
3397 module_id: status.module_id,
3398 module_declared,
3399 daemon_observed: SupervisorObservedProcess {
3400 pid: status.pid,
3401 spawned_at_ms: status.spawned_at_ms,
3402 spawned_from: status.spawned_from,
3403 running_image,
3404 },
3405 });
3406 }
3407 let daemon = SupervisorDaemonProvenance {
3408 daemon_build: self.daemon_provenance.build.clone(),
3409 daemon_observed: DaemonObservedProcess {
3410 pid: self.daemon_provenance.pid,
3411 started_at_ms: self
3412 .daemon_provenance
3413 .start_clock
3414 .map(|clock| clock.started_at_ms())
3415 .or(self.daemon_provenance.started_at_ms),
3416 running_image: self
3417 .daemon_provenance
3418 .probe
3419 .observe(
3420 self.daemon_provenance.pid,
3421 self.daemon_provenance.executable_path.as_deref(),
3422 self.daemon_provenance.executable_identity,
3423 self.daemon_provenance.process_start_time,
3424 )
3425 .await,
3426 },
3427 };
3428 let response = ClientControlResponse::SupervisorProvenance { daemon, modules };
3429 Ok(vec![control_response_body_frame(
3430 &frame,
3431 &response,
3432 "ClientControlResponse::SupervisorProvenance",
3433 )?])
3434 }
3435
3436 fn handle_supervisor_health(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
3437 self.refresh_capability_requirements();
3438 let generation = self
3439 .registry
3440 .generation()
3441 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
3442 let modules = self
3443 .supervisor
3444 .list()
3445 .into_iter()
3446 .map(|module| {
3447 let status = module.status_for_control("health").map_err(|err| {
3448 RouterError::backend(
3449 0,
3450 frame.header.corr,
3451 format!("failed to read supervisor health: {err}"),
3452 )
3453 })?;
3454 let module_id = status.module_id;
3455 let capability_detail = self
3456 .capability_evaluator
3457 .required_problem_detail(&module_id);
3458 Ok(SupervisorHealthEntry {
3459 module_id,
3460 status: status.health.status,
3461 detail: append_capability_problem_detail(
3462 status.health.detail,
3463 capability_detail,
3464 ),
3465 metrics: status.health.metrics,
3466 consecutive_failures: status.health.consecutive_failures,
3467 late_answer_count: status.health.late_answer_count,
3468 last_late_answer_latency_ms: status.health.last_late_answer_latency_ms,
3469 last_action: status.health.last_action,
3470 last_action_ms: status.health.last_action_ms,
3471 last_probe_ms: status.health.last_probe_ms,
3472 })
3473 })
3474 .collect::<Result<Vec<_>, RouterError>>()?;
3475 let response = ClientControlResponse::SupervisorHealth {
3476 generation,
3477 modules,
3478 };
3479 Ok(vec![control_response_body_frame(
3480 &frame,
3481 &response,
3482 "ClientControlResponse::SupervisorHealth",
3483 )?])
3484 }
3485
3486 async fn handle_supervisor_restart(
3487 &self,
3488 frame: Frame,
3489 module_id: String,
3490 drain_timeout_ms: Option<u64>,
3491 ) -> Result<Vec<Frame>, RouterError> {
3492 let operation_lock = self.supervisor.operation_lock();
3493 let _operation_guard = operation_lock.lock().await;
3494 let Some(module) = self.supervisor.get(&module_id) else {
3495 return Ok(vec![control_error_frame(
3496 &frame,
3497 "unknown_module",
3498 format!("module_id '{module_id}' is not supervised"),
3499 )?]);
3500 };
3501
3502 if let Err(err) = module.restart(drain_timeout_ms).await {
3503 let (code, message) = match err {
3504 crate::supervise::SuperviseError::Disabled { .. } => {
3505 ("module_disabled", err.to_string())
3506 }
3507 crate::supervise::SuperviseError::SwapInProgress { .. } => {
3508 ("swap_in_progress", err.to_string())
3509 }
3510 _ => (
3511 "target_unavailable",
3512 format!("failed to restart module_id '{module_id}': {err}"),
3513 ),
3514 };
3515 return Ok(vec![control_error_frame(&frame, code, message)?]);
3516 }
3517
3518 let response = ClientControlResponse::SupervisorAck {
3519 module_id,
3520 applied: true,
3521 };
3522 Ok(vec![control_response_body_frame(
3523 &frame,
3524 &response,
3525 "ClientControlResponse::SupervisorAck",
3526 )?])
3527 }
3528
3529 async fn handle_supervisor_swap(
3533 &self,
3534 frame: Frame,
3535 module_id: String,
3536 ready_timeout_ms: Option<u64>,
3537 ) -> Result<Vec<Frame>, RouterError> {
3538 let module = {
3545 let operation_lock = self.supervisor.operation_lock();
3546 let _operation_guard = operation_lock.lock().await;
3547 self.supervisor.get(&module_id)
3548 };
3549 let Some(module) = module else {
3550 return Ok(vec![control_error_frame(
3551 &frame,
3552 "unknown_module",
3553 format!("module_id '{module_id}' is not supervised"),
3554 )?]);
3555 };
3556
3557 if let Err(err) = module
3558 .swap(ready_timeout_ms.map(Duration::from_millis))
3559 .await
3560 {
3561 use crate::supervise::SuperviseError;
3562 let message = err.to_string();
3563 let error = match err {
3564 SuperviseError::Disabled { .. } => ErrorBody::new("module_disabled", message),
3565 SuperviseError::SwapRefused { reason, .. } => ErrorBody {
3566 code: "swap_refused".to_string(),
3567 message,
3568 detail: Some(serde_json::json!({ "reason": reason.as_str() })),
3569 },
3570 SuperviseError::SwapFailed {
3571 arm,
3572 candidate_exit,
3573 ..
3574 } => ErrorBody {
3575 code: "swap_failed".to_string(),
3576 message,
3577 detail: Some(serde_json::json!({
3578 "arm": arm.as_str(),
3579 "candidate_exit_code": candidate_exit.as_ref().and_then(|exit| exit.code),
3580 "candidate_exit_signal": candidate_exit.as_ref().and_then(|exit| exit.signal),
3581 })),
3582 },
3583 _ => ErrorBody::new(
3584 "target_unavailable",
3585 format!("failed to swap module_id '{module_id}': {message}"),
3586 ),
3587 };
3588 return Ok(vec![control_error_body_frame(&frame, error)?]);
3589 }
3590
3591 let response = ClientControlResponse::SupervisorAck {
3592 module_id,
3593 applied: true,
3594 };
3595 Ok(vec![control_response_body_frame(
3596 &frame,
3597 &response,
3598 "ClientControlResponse::SupervisorAck",
3599 )?])
3600 }
3601
3602 async fn handle_supervisor_reload(
3603 &self,
3604 frame: Frame,
3605 module_id: String,
3606 ) -> Result<Vec<Frame>, RouterError> {
3607 let operation_lock = self.supervisor.operation_lock();
3608 let _operation_guard = operation_lock.lock().await;
3609 let Some(module) = self.supervisor.get(&module_id) else {
3610 return Ok(vec![control_error_frame(
3611 &frame,
3612 "unknown_module",
3613 format!("module_id '{module_id}' is not supervised"),
3614 )?]);
3615 };
3616
3617 if let Err(err) = module.reload().await {
3618 let (code, message) = match err {
3619 crate::supervise::SuperviseError::Disabled { .. } => {
3620 ("module_disabled", err.to_string())
3621 }
3622 crate::supervise::SuperviseError::SwapInProgress { .. } => {
3623 ("swap_in_progress", err.to_string())
3624 }
3625 _ => (
3626 "reload_failed",
3627 format!("failed to reload module_id '{module_id}': {err}"),
3628 ),
3629 };
3630 return Ok(vec![control_error_frame(&frame, code, message)?]);
3631 }
3632
3633 let response = ClientControlResponse::SupervisorAck {
3634 module_id,
3635 applied: true,
3636 };
3637 Ok(vec![control_response_body_frame(
3638 &frame,
3639 &response,
3640 "ClientControlResponse::SupervisorAck",
3641 )?])
3642 }
3643
3644 async fn handle_supervisor_rescan(
3645 &self,
3646 frame: Frame,
3647 preview: bool,
3648 ) -> Result<Vec<Frame>, RouterError> {
3649 let Some(context) = self.rescan.clone() else {
3650 return Ok(vec![control_error_frame(
3651 &frame,
3652 "rescan_unavailable",
3653 "the daemon was not started with a reloadable config path".to_string(),
3654 )?]);
3655 };
3656
3657 let operation_lock = self.supervisor.operation_lock();
3658 let _operation_guard = operation_lock.lock().await;
3659 let loaded = match crate::daemon_config::load(&context.config_path) {
3660 Ok(config) => config,
3661 Err(err) => {
3662 return Ok(vec![control_error_frame(
3663 &frame,
3664 "invalid_daemon_config",
3665 format!("supervisor rescan rejected daemon config: {err}"),
3666 )?])
3667 }
3668 };
3669 let Some(config) = loaded else {
3677 return Ok(vec![control_error_frame(
3678 &frame,
3679 "invalid_daemon_config",
3680 format!(
3681 "daemon config not found at {}; refusing to rescan (an absent config would \
3682 retire every supervised module)",
3683 context.config_path.display()
3684 ),
3685 )?]);
3686 };
3687 let (
3688 configured_port,
3689 storage_config,
3690 admission_facts_carrier_module_id,
3691 admission_facts_targets,
3692 modules,
3693 reserved_capabilities,
3694 ) = (
3695 config.port,
3696 config.storage,
3697 config.admission_facts_carrier_module_id,
3698 config.admission_facts_targets,
3699 config.modules,
3700 config.reserved_capabilities,
3701 );
3702
3703 let mut restart_required = Vec::new();
3712 for section in RestartRequiredSection::ALL {
3713 let changed = match section {
3714 RestartRequiredSection::Port => configured_port != context.configured_port,
3715 RestartRequiredSection::Storage => storage_config != context.storage_config,
3716 RestartRequiredSection::AdmissionFactsCarrierModuleId => {
3717 admission_facts_carrier_module_id != context.admission_facts_carrier_module_id
3718 }
3719 RestartRequiredSection::AdmissionFactsTargets => {
3720 admission_facts_targets != context.admission_facts_targets
3721 }
3722 };
3723 if changed {
3724 restart_required.push(section.label().to_string());
3725 }
3726 }
3727 if !restart_required.is_empty() {
3728 warn!(
3729 config_path = %context.config_path.display(),
3730 sections = %restart_required.join(", "),
3731 "daemon config changed outside the modules section; restart the daemon to apply those changes"
3732 );
3733 }
3734
3735 for configured in &modules {
3736 if let Err(err) = validate_spec(&configured.module_spec()) {
3737 return Ok(vec![control_error_frame(
3738 &frame,
3739 "invalid_daemon_config",
3740 format!("supervisor rescan rejected daemon config: {err}"),
3741 )?]);
3742 }
3743 }
3744
3745 let configured_capabilities = modules
3746 .iter()
3747 .map(|module| (module.module_id.clone(), module.enabled))
3748 .collect::<Vec<_>>();
3749 let preview_capability_warnings = if preview {
3750 let (_, registrations) = self.runtime_capability_snapshot()?;
3751 let current_modules = self
3752 .supervisor
3753 .list()
3754 .into_iter()
3755 .map(|module| module.module_id().to_string())
3756 .collect::<BTreeSet<_>>();
3757 let resulting_modules = configured_capabilities.clone();
3758 let removed = current_modules
3759 .into_iter()
3760 .filter(|module_id| {
3761 !resulting_modules
3762 .iter()
3763 .any(|(configured_id, _)| configured_id == module_id)
3764 })
3765 .collect::<Vec<_>>();
3766 self.capability_evaluator.preview_removal_warnings(
3767 resulting_modules,
3768 &removed,
3769 ®istrations,
3770 )
3771 } else {
3772 Vec::new()
3773 };
3774 let result = match self
3775 .reconcile_supervised_modules(&context.supervisor, modules, preview)
3776 .await
3777 {
3778 Ok(result) => result,
3779 Err(message) => {
3780 return Ok(vec![control_error_frame(&frame, "rescan_failed", message)?])
3781 }
3782 };
3783 if !preview {
3784 self.capability_evaluator
3785 .configure(configured_capabilities, reserved_capabilities);
3786 self.capability_evaluator.wake_deadline_loop();
3787 self.refresh_capability_requirements();
3788 }
3789 let mut result = result;
3790 result.restart_required = restart_required;
3791 result.capability_warnings = preview_capability_warnings;
3792 let response = ClientControlResponse::SupervisorRescan { result };
3793 Ok(vec![control_response_body_frame(
3794 &frame,
3795 &response,
3796 "ClientControlResponse::SupervisorRescan",
3797 )?])
3798 }
3799
3800 async fn handle_supervisor_release_reserved(
3801 &self,
3802 frame: Frame,
3803 module_id: String,
3804 ) -> Result<Vec<Frame>, RouterError> {
3805 let Some(context) = self.rescan.clone() else {
3806 return Ok(vec![control_error_frame(
3807 &frame,
3808 "release_unavailable",
3809 "reserved-id release requires a daemon started with a reloadable config path",
3810 )?]);
3811 };
3812 let operation_lock = self.supervisor.operation_lock();
3813 let _operation_guard = operation_lock.lock().await;
3814 let loaded = match crate::daemon_config::load(&context.config_path) {
3815 Ok(Some(config)) => config,
3816 Ok(None) => {
3817 return Ok(vec![control_error_frame(
3818 &frame,
3819 "invalid_daemon_config",
3820 format!(
3821 "daemon config not found at {}; refusing to release reserved module_id '{module_id}'",
3822 context.config_path.display()
3823 ),
3824 )?])
3825 }
3826 Err(err) => {
3827 return Ok(vec![control_error_frame(
3828 &frame,
3829 "invalid_daemon_config",
3830 format!("unable to verify reserved-id release against daemon config: {err}"),
3831 )?])
3832 }
3833 };
3834 if loaded
3835 .modules
3836 .iter()
3837 .any(|configured| configured.module_id == module_id)
3838 {
3839 return Ok(vec![control_error_frame(
3840 &frame,
3841 "reserved_module_configured",
3842 format!(
3843 "module_id '{module_id}' remains configured; remove its config entry and rescan before releasing its reserved id"
3844 ),
3845 )?]);
3846 }
3847 if !self.supervisor.release_retained_reserved_gate(&module_id) {
3848 return Ok(vec![control_error_frame(
3849 &frame,
3850 "reserved_gate_not_retained",
3851 format!(
3852 "module_id '{module_id}' has no retired reserved-id gate to release; rescan its removed reserved configuration first"
3853 ),
3854 )?]);
3855 }
3856
3857 let response = ClientControlResponse::SupervisorAck {
3858 module_id,
3859 applied: true,
3860 };
3861 Ok(vec![control_response_body_frame(
3862 &frame,
3863 &response,
3864 "ClientControlResponse::SupervisorAck",
3865 )?])
3866 }
3867
3868 async fn reconcile_supervised_modules(
3877 &self,
3878 supervisor: &Supervisor,
3879 configured_modules: Vec<crate::daemon_config::ConfiguredModule>,
3880 preview: bool,
3881 ) -> Result<SupervisorRescanResult, String> {
3882 let mut current = BTreeMap::new();
3883 for module in self.supervisor.list() {
3884 let (spec, health) = module.configuration().map_err(|err| {
3885 format!(
3886 "failed to read configuration for module_id '{}': {err}",
3887 module.module_id()
3888 )
3889 })?;
3890 let enabled = module
3891 .status()
3892 .map_err(|err| {
3893 format!(
3894 "failed to read status for module_id '{}': {err}",
3895 module.module_id()
3896 )
3897 })?
3898 .enabled;
3899 current.insert(
3900 module.module_id().to_string(),
3901 (module, spec, health, enabled),
3902 );
3903 }
3904 let configured = configured_modules
3905 .into_iter()
3906 .map(|module| (module.module_id.clone(), module))
3907 .collect::<BTreeMap<_, _>>();
3908
3909 let added = configured
3910 .keys()
3911 .filter(|module_id| !current.contains_key(*module_id))
3912 .cloned()
3913 .collect::<Vec<_>>();
3914 let removed = current
3915 .keys()
3916 .filter(|module_id| !configured.contains_key(*module_id))
3917 .cloned()
3918 .collect::<Vec<_>>();
3919 let mut changed_pending_reload = Vec::new();
3920 let mut configuration_changes = BTreeSet::new();
3921 let mut enabled_changes = BTreeSet::new();
3922 let mut unchanged = 0_u32;
3923
3924 for (module_id, configured_module) in &configured {
3925 let Some((_, current_spec, current_health, current_enabled)) = current.get(module_id)
3926 else {
3927 continue;
3928 };
3929 let configuration_changed = *current_spec != configured_module.module_spec()
3930 || *current_health != configured_module.health;
3931 let enabled_changed = *current_enabled != configured_module.enabled;
3932 if configuration_changed {
3933 configuration_changes.insert(module_id.clone());
3934 changed_pending_reload.push(module_id.clone());
3935 }
3936 if enabled_changed {
3937 enabled_changes.insert(module_id.clone());
3938 }
3939 if !configuration_changed && !enabled_changed {
3940 unchanged = unchanged.saturating_add(1);
3941 }
3942 }
3943
3944 if preview {
3950 return Ok(SupervisorRescanResult {
3951 added,
3952 removed,
3953 changed_pending_reload,
3954 enabled_changes: enabled_changes.iter().cloned().collect(),
3955 unchanged,
3956 preview: true,
3957 restart_required: Vec::new(),
3961 capability_warnings: Vec::new(),
3962 });
3963 }
3964
3965 for module_id in &removed {
3966 let module = ¤t
3967 .get(module_id)
3968 .expect("removed module came from current supervisor state")
3969 .0;
3970 module.retire().await.map_err(|err| {
3971 format!("failed to retire module_id '{module_id}' during rescan: {err}")
3972 })?;
3973 self.supervisor.record_rescan_removal(module_id);
4007 self.supervisor.retire(module_id);
4008 }
4009
4010 for module_id in configured.keys() {
4011 let Some((module, _, _, _)) = current.get(module_id) else {
4012 continue;
4013 };
4014 let configured_module = configured
4015 .get(module_id)
4016 .expect("configured module id came from configured map");
4017 if configuration_changes.contains(module_id) {
4018 module
4019 .update_configuration(
4020 configured_module.module_spec(),
4021 configured_module.health,
4022 configured_module.drain_timeout_ms,
4023 )
4024 .await
4025 .map_err(|err| {
4026 format!(
4027 "failed to update module_id '{module_id}' configuration during rescan: {err}"
4028 )
4029 })?;
4030 }
4031 if enabled_changes.contains(module_id) {
4032 module
4033 .set_enabled(configured_module.enabled)
4034 .await
4035 .map_err(|err| {
4036 format!(
4037 "failed to apply module_id '{module_id}' enabled={} during rescan: {err}",
4038 configured_module.enabled
4039 )
4040 })?;
4041 }
4042 }
4043
4044 for module_id in &added {
4045 let configured_module = configured
4046 .get(module_id)
4047 .expect("added module id came from configured map");
4048 supervisor
4049 .supervise_configured_with_health(
4050 configured_module.module_spec(),
4051 configured_module.enabled,
4052 configured_module.health,
4053 configured_module.drain_timeout_ms,
4054 configured_module.restart,
4055 )
4056 .map_err(|err| {
4057 format!("failed to add module_id '{module_id}' during rescan: {err}")
4058 })?;
4059 }
4060
4061 Ok(SupervisorRescanResult {
4062 added,
4063 removed,
4064 changed_pending_reload,
4065 enabled_changes: enabled_changes.iter().cloned().collect(),
4066 unchanged,
4067 preview: false,
4068 restart_required: Vec::new(),
4071 capability_warnings: Vec::new(),
4072 })
4073 }
4074
4075 async fn handle_supervisor_set_enabled(
4076 &self,
4077 frame: Frame,
4078 module_id: String,
4079 enabled: bool,
4080 ) -> Result<Vec<Frame>, RouterError> {
4081 let operation_lock = self.supervisor.operation_lock();
4082 let _operation_guard = operation_lock.lock().await;
4083 let Some(module) = self.supervisor.get(&module_id) else {
4084 return Ok(vec![control_error_frame(
4085 &frame,
4086 "unknown_module",
4087 format!("module_id '{module_id}' is not supervised"),
4088 )?]);
4089 };
4090
4091 let applied = match module.set_enabled(enabled).await {
4092 Ok(applied) => applied,
4093 Err(err) => {
4094 return Ok(vec![control_error_frame(
4095 &frame,
4096 "target_unavailable",
4097 format!("failed to set module_id '{module_id}' enabled={enabled}: {err}"),
4098 )?])
4099 }
4100 };
4101
4102 self.capability_evaluator.wake_deadline_loop();
4103 self.refresh_capability_requirements();
4104 let response = ClientControlResponse::SupervisorAck { module_id, applied };
4105 Ok(vec![control_response_body_frame(
4106 &frame,
4107 &response,
4108 "ClientControlResponse::SupervisorAck",
4109 )?])
4110 }
4111
4112 async fn handle_supervisor_health_probe(
4113 &self,
4114 frame: Frame,
4115 module_id: String,
4116 ) -> Result<Vec<Frame>, RouterError> {
4117 self.refresh_capability_requirements();
4118 let Some(registration) = self
4119 .registry
4120 .get_module(&module_id)
4121 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
4122 else {
4123 return Ok(vec![control_error_frame(
4124 &frame,
4125 "unknown_module",
4126 format!("module_id '{module_id}' is not registered"),
4127 )?]);
4128 };
4129
4130 if !module_registration_grants_op(®istration.control_ops, MODULE_CONTROL_OP_HEALTH_CHECK)
4143 {
4144 return Ok(vec![control_error_frame(
4145 &frame,
4146 "health_not_advertised",
4147 format!("module_id '{module_id}' did not advertise health.check"),
4148 )?]);
4149 }
4150
4151 let deadline = Instant::now() + self.health_probe_timeout;
4152 let pending = match self.forwarding.begin_module_control_rpc_for(
4153 &module_id,
4154 MODULE_CONTROL_OP_HEALTH_CHECK,
4155 deadline,
4156 ) {
4157 Ok(pending) => pending,
4158 Err(err) => {
4159 return Ok(vec![control_error_frame(
4160 &frame,
4161 forwarding_error_code(&err),
4162 err.to_string(),
4163 )?])
4164 }
4165 };
4166
4167 let PendingModuleControlRpc {
4168 endpoint,
4169 module_sink,
4170 negotiated_ver,
4171 corr: probe_corr,
4172 receiver,
4173 } = pending;
4174 let mut guard =
4175 ModuleControlRpcGuard::new(Arc::clone(&self.forwarding), endpoint, probe_corr);
4176 let probe_body =
4177 serde_json::to_vec(&ModuleControlRequest::HealthCheck {}).map_err(|err| {
4178 RouterError::backend(
4179 0,
4180 frame.header.corr,
4181 format!("failed to encode health.check request: {err}"),
4182 )
4183 })?;
4184 let probe_frame = Frame::build_with_version(
4185 negotiated_ver,
4186 FrameType::Request,
4187 control_flags(),
4188 0,
4189 0,
4190 probe_corr,
4191 probe_body,
4192 )
4193 .map_err(RouterError::FrameBuild)?;
4194
4195 if let Err(err) = module_sink.send(probe_frame).await {
4196 return Ok(vec![control_error_frame(
4197 &frame,
4198 "target_unavailable",
4199 err.to_string(),
4200 )?]);
4201 }
4202
4203 match timeout_at(deadline, receiver).await {
4204 Ok(Ok(ModuleControlRpcOutcome::Response(response))) => {
4205 guard.disarm();
4206 let Some(report) = response.health_report() else {
4207 return Ok(vec![control_error_frame(
4208 &frame,
4209 "invalid_control_body",
4210 "health.check RPC returned a non-health response",
4211 )?]);
4212 };
4213 let HealthReport {
4218 status,
4219 detail,
4220 metrics,
4221 } = report;
4222 let capability_detail = self
4223 .capability_evaluator
4224 .required_problem_detail(&module_id);
4225 let response = ClientControlResponse::SupervisorHealthProbe {
4226 module_id,
4227 status,
4228 detail: append_capability_problem_detail(detail, capability_detail),
4229 metrics,
4230 };
4231 Ok(vec![control_response_body_frame(
4232 &frame,
4233 &response,
4234 "ClientControlResponse::SupervisorHealthProbe",
4235 )?])
4236 }
4237 Ok(Ok(ModuleControlRpcOutcome::Rejected(body))) => {
4238 guard.disarm();
4239 Ok(vec![control_error_body_frame(&frame, body)?])
4240 }
4241 Ok(Ok(ModuleControlRpcOutcome::ModuleGone(message))) => {
4242 guard.disarm();
4243 Ok(vec![control_error_frame(
4244 &frame,
4245 "target_unavailable",
4246 message,
4247 )?])
4248 }
4249 Ok(Ok(ModuleControlRpcOutcome::MalformedResponse(message))) => {
4250 guard.disarm();
4251 Ok(vec![control_error_frame(
4252 &frame,
4253 "invalid_control_body",
4254 message,
4255 )?])
4256 }
4257 Ok(Ok(ModuleControlRpcOutcome::UnexpectedOp { expected, actual })) => {
4258 guard.disarm();
4259 Ok(vec![control_error_frame(
4260 &frame,
4261 "invalid_control_body",
4262 format!("expected module-control op '{expected}', got '{actual}'"),
4263 )?])
4264 }
4265 Ok(Ok(ModuleControlRpcOutcome::DeadlineElapsed)) => {
4266 guard.disarm();
4267 Ok(vec![control_error_frame(
4268 &frame,
4269 "module_timeout",
4270 format!(
4271 "module_id '{module_id}' answered health.check after {:?}",
4272 self.health_probe_timeout
4273 ),
4274 )?])
4275 }
4276 Ok(Err(_)) => Ok(vec![control_error_frame(
4277 &frame,
4278 "target_unavailable",
4279 "health.check waiter was canceled before the module responded",
4280 )?]),
4281 Err(_) => Ok(vec![control_error_frame(
4282 &frame,
4283 "module_timeout",
4284 format!(
4285 "module_id '{module_id}' did not answer health.check within {:?}",
4286 self.health_probe_timeout
4287 ),
4288 )?]),
4289 }
4290 }
4291
4292 fn supervisor_status(
4293 &self,
4294 module_id: &str,
4295 corr: u64,
4296 ) -> Result<Option<(crate::supervise::ModuleStatus, bool)>, RouterError> {
4297 self.supervisor
4298 .get(module_id)
4299 .map(|module| {
4300 let warming = module.is_warming_for_control("status").map_err(|err| {
4301 RouterError::backend(
4302 0,
4303 corr,
4304 format!(
4305 "failed to read supervisor warming state for module_id '{module_id}': {err}"
4306 ),
4307 )
4308 })?;
4309 module.status_for_control("status").map_err(|err| {
4310 RouterError::backend(
4311 0,
4312 corr,
4313 format!(
4314 "failed to read supervisor status for module_id '{module_id}': {err}"
4315 ),
4316 )
4317 }).map(|status| (status, warming))
4318 })
4319 .transpose()
4320 }
4321
4322 fn guard_module_control_op(
4323 &self,
4324 frame: &Frame,
4325 module_id: &str,
4326 op: &str,
4327 ) -> Result<Option<Frame>, RouterError> {
4328 if self.module_grants_op(module_id, op, frame.header.corr)? {
4329 return Ok(None);
4330 }
4331
4332 Ok(Some(control_error_frame(
4333 frame,
4334 "op_not_allowed",
4335 format!("module_id '{module_id}' did not grant control op '{op}'"),
4336 )?))
4337 }
4338
4339 fn module_grants_op(&self, module_id: &str, op: &str, corr: u64) -> Result<bool, RouterError> {
4340 let Some(registration) = self
4341 .registry
4342 .get_module(module_id)
4343 .map_err(|err| RouterError::backend(0, corr, err.to_string()))?
4344 else {
4345 return Ok(false);
4346 };
4347 Ok(module_registration_grants_op(®istration.control_ops, op))
4348 }
4349
4350 fn handle_status_update(
4351 &self,
4352 endpoint: ModuleEndpointId,
4353 frame: Frame,
4354 ) -> Result<Vec<Frame>, RouterError> {
4355 let update = match serde_json::from_slice::<ModuleControlPush>(&frame.body) {
4356 Ok(update) => update,
4357 Err(err) => {
4358 if is_known_module_push_op(&frame.body) {
4363 return Ok(vec![control_error_frame(
4364 &frame,
4365 "invalid_control_body",
4366 format!("malformed module control push body: {err}"),
4367 )?]);
4368 }
4369 return Ok(Vec::new());
4370 }
4371 };
4372
4373 match update {
4374 ModuleControlPush::RouteStatus {
4375 route_channel,
4376 route_epoch,
4377 status,
4378 } => {
4379 self.forwarding
4380 .cache_status(endpoint, route_channel, route_epoch, status)
4381 .map_err(RouterError::Forwarding)?;
4382 }
4383 }
4384 Ok(Vec::new())
4385 }
4386
4387 fn handle_route_poll(
4388 &self,
4389 ctx: &RouteCtx,
4390 frame: Frame,
4391 route_channel: u16,
4392 route_epoch: u32,
4393 kind: PollKind,
4394 ) -> Result<Vec<Frame>, RouterError> {
4395 let snapshot = self
4396 .forwarding
4397 .route_poll_snapshot(ctx.connection_id, route_channel, route_epoch)
4398 .map_err(RouterError::Forwarding)?;
4399 let response = match (kind, snapshot) {
4400 (PollKind::Status, RoutePollSnapshot::Bound { status, .. }) => {
4401 ClientControlResponse::RoutePoll {
4402 route_channel,
4403 route_epoch,
4404 status,
4405 live: None,
4406 }
4407 }
4408 (PollKind::Status, RoutePollSnapshot::Absent) => ClientControlResponse::RoutePoll {
4409 route_channel,
4410 route_epoch,
4411 status: None,
4412 live: None,
4413 },
4414 (PollKind::Liveness, RoutePollSnapshot::Bound { module_id, .. }) => {
4415 let live = self
4441 .process_liveness
4442 .as_ref()
4443 .and_then(|source| source.process_live(&module_id))
4444 .unwrap_or(true);
4445 ClientControlResponse::RoutePoll {
4446 route_channel,
4447 route_epoch,
4448 status: None,
4449 live: Some(live),
4450 }
4451 }
4452 (PollKind::Liveness, RoutePollSnapshot::Absent) => ClientControlResponse::RoutePoll {
4453 route_channel,
4454 route_epoch,
4455 status: None,
4456 live: Some(false),
4457 },
4458 };
4459
4460 Ok(vec![control_response_body_frame(
4461 &frame,
4462 &response,
4463 "ClientControlResponse::RoutePoll",
4464 )?])
4465 }
4466
4467 pub(crate) fn observe_module_control_completion(
4468 &self,
4469 completion: ModuleControlRpcCompletion,
4470 ) -> bool {
4471 match completion {
4472 ModuleControlRpcCompletion::Unknown => false,
4473 ModuleControlRpcCompletion::Settled => true,
4474 ModuleControlRpcCompletion::LateHealthAnswer { module_id, latency } => {
4475 let latency_ms = latency.as_millis().min(u128::from(u64::MAX)) as u64;
4476 info!(
4477 module_id = %module_id,
4478 latency_ms,
4479 "late health.check answer proves the module is alive"
4480 );
4481 match self
4482 .supervisor
4483 .record_late_health_answer(&module_id, latency_ms)
4484 {
4485 Ok(true) => {}
4486 Ok(false) => debug!(
4487 module_id = %module_id,
4488 latency_ms,
4489 "late health.check answer has no active supervisor snapshot"
4490 ),
4491 Err(err) => warn!(
4492 module_id = %module_id,
4493 latency_ms,
4494 error = %err,
4495 "failed to record late health.check answer"
4496 ),
4497 }
4498 true
4499 }
4500 }
4501 }
4502
4503 fn refuse_to_end_module_connection_for_a_client(
4524 &self,
4525 module_connection_id: ConnectionId,
4526 corr: u64,
4527 err: ForwardingError,
4528 ) -> Result<(), RouterError> {
4529 if let ForwardingError::ConnectionClosing { connection_id } = err {
4530 if connection_id != module_connection_id {
4531 warn!(
4532 module_connection_id = module_connection_id.get(),
4533 client_connection_id = connection_id.get(),
4534 corr,
4535 "dropping a route.bind response for a closing client; the module connection keeps serving"
4536 );
4537 return Ok(());
4538 }
4539 }
4540 Err(RouterError::Forwarding(err))
4541 }
4542
4543 fn handle_module_relay_response(
4544 &self,
4545 connection_id: ConnectionId,
4546 frame: Frame,
4547 ) -> Result<Vec<Frame>, RouterError> {
4548 let mut secondary_error = None;
4549 let outcome = match frame.header.ty {
4550 FrameType::Response => match serde_json::from_slice::<ControlOpProbe>(&frame.body) {
4551 Ok(probe) if probe.op == "route.bind" => {
4552 match serde_json::from_slice::<ModuleControlResponse>(&frame.body) {
4553 Ok(ModuleControlResponse::RouteBindAck {}) => {
4554 RouteBindRelayOutcome::Accepted
4555 }
4556 Ok(other) => {
4557 let message =
4558 format!("route.bind response carried unexpected body: {other:?}");
4559 secondary_error = Some(control_error_frame(
4560 &frame,
4561 "invalid_control_body",
4562 message.clone(),
4563 )?);
4564 RouteBindRelayOutcome::ModuleGone(message)
4565 }
4566 Err(err) => {
4567 let message = format!("malformed route.bind response body: {err}");
4568 secondary_error = Some(control_error_frame(
4569 &frame,
4570 "invalid_control_body",
4571 message.clone(),
4572 )?);
4573 RouteBindRelayOutcome::ModuleGone(message)
4574 }
4575 }
4576 }
4577 Ok(probe) => {
4578 let outcome = match serde_json::from_slice::<ModuleControlResponse>(&frame.body)
4579 {
4580 Ok(response) => ModuleControlRpcOutcome::Response(response),
4581 Err(err) => ModuleControlRpcOutcome::MalformedResponse(format!(
4582 "malformed {} response body: {err}",
4583 probe.op
4584 )),
4585 };
4586 let completion = self
4587 .forwarding
4588 .complete_module_control_rpc(
4589 connection_id,
4590 frame.header.corr,
4591 Some(&probe.op),
4592 outcome,
4593 )
4594 .map_err(RouterError::Forwarding)?;
4595 if !self.observe_module_control_completion(completion) {
4596 debug!(
4597 connection_id = connection_id.get(),
4598 corr = frame.header.corr,
4599 op = %probe.op,
4600 "dropping late or unknown module-control RPC response"
4601 );
4602 }
4603 return Ok(Vec::new());
4604 }
4605 Err(err) => {
4606 if let Some(expected_op) = self
4607 .forwarding
4608 .pending_module_control_op(connection_id, frame.header.corr)
4609 .map_err(RouterError::Forwarding)?
4610 {
4611 let completion = self
4612 .forwarding
4613 .complete_module_control_rpc(
4614 connection_id,
4615 frame.header.corr,
4616 None,
4617 ModuleControlRpcOutcome::MalformedResponse(format!(
4618 "malformed {expected_op} response body: {err}"
4619 )),
4620 )
4621 .map_err(RouterError::Forwarding)?;
4622 if !self.observe_module_control_completion(completion) {
4623 debug!(
4624 connection_id = connection_id.get(),
4625 corr = frame.header.corr,
4626 "dropping late malformed module-control RPC response"
4627 );
4628 }
4629 return Ok(Vec::new());
4630 }
4631 let message = format!("malformed route.bind response body: {err}");
4632 secondary_error = Some(control_error_frame(
4633 &frame,
4634 "invalid_control_body",
4635 message.clone(),
4636 )?);
4637 RouteBindRelayOutcome::ModuleGone(message)
4638 }
4639 },
4640 FrameType::Error => {
4641 if self
4642 .forwarding
4643 .pending_module_control_op(connection_id, frame.header.corr)
4644 .map_err(RouterError::Forwarding)?
4645 .is_some()
4646 {
4647 let outcome = match serde_json::from_slice::<ErrorBody>(&frame.body) {
4648 Ok(body) => ModuleControlRpcOutcome::Rejected(body),
4649 Err(err) => ModuleControlRpcOutcome::MalformedResponse(format!(
4650 "malformed module-control ERROR body: {err}"
4651 )),
4652 };
4653 let completion = self
4654 .forwarding
4655 .complete_module_control_rpc(
4656 connection_id,
4657 frame.header.corr,
4658 None,
4659 outcome,
4660 )
4661 .map_err(RouterError::Forwarding)?;
4662 if !self.observe_module_control_completion(completion) {
4663 debug!(
4664 connection_id = connection_id.get(),
4665 corr = frame.header.corr,
4666 "dropping late or unknown module-control RPC error"
4667 );
4668 }
4669 return Ok(Vec::new());
4670 }
4671 match serde_json::from_slice::<ErrorBody>(&frame.body) {
4672 Ok(body) => RouteBindRelayOutcome::Rejected(body),
4673 Err(err) => {
4674 let message = format!("malformed route.bind ERROR body: {err}");
4675 secondary_error = Some(control_error_frame(
4676 &frame,
4677 "invalid_control_body",
4678 message.clone(),
4679 )?);
4680 RouteBindRelayOutcome::ModuleGone(message)
4681 }
4682 }
4683 }
4684 ty => {
4685 return Ok(vec![control_error_frame(
4686 &frame,
4687 "unsupported_control_frame",
4688 format!("unsupported module channel-0 frame {ty:?}"),
4689 )?])
4690 }
4691 };
4692
4693 let settled =
4694 self.forwarding
4695 .complete_pending_relay(connection_id, frame.header.corr, outcome);
4696 let completion = match settled {
4697 Ok(completion) => completion,
4698 Err(err) => {
4699 self.refuse_to_end_module_connection_for_a_client(
4700 connection_id,
4701 frame.header.corr,
4702 err,
4703 )?;
4704 return Ok(secondary_error.into_iter().collect());
4705 }
4706 };
4707 if let Some(target) = completion.abandoned.as_ref() {
4708 send_goodbye_target_best_effort(target, "late accepted route.bind");
4709 }
4710 if !completion.settled {
4711 debug!(
4712 connection_id = connection_id.get(),
4713 corr = frame.header.corr,
4714 frame_type = ?frame.header.ty,
4715 "dropping late or unknown route.bind relay response"
4716 );
4717 }
4718 Ok(secondary_error.into_iter().collect())
4719 }
4720
4721 fn handle_goodbye(&self, connection_id: ConnectionId) -> Result<Vec<Frame>, RouterError> {
4722 debug!(connection_id = connection_id.get(), "handling GOODBYE");
4723 let registrations = self
4724 .deregister_connection(connection_id)
4725 .map_err(|err| RouterError::backend(0, 0, err.to_string()))?;
4726 let released_routes = self
4727 .forwarding
4728 .cleanup_connection(connection_id)
4729 .map_err(RouterError::Forwarding)?;
4730 self.emit_route_goodbyes(released_routes);
4731 if !registrations.is_empty() {
4733 crate::supervise::notify_registration_release();
4734 }
4735 Ok(Vec::new())
4736 }
4737}
4738
4739impl Default for ControlHandler {
4740 fn default() -> Self {
4741 Self::new(Arc::new(Registry::default()))
4742 }
4743}
4744
4745impl crate::supervise::SwapPromotionObserver for ControlHandler {
4746 fn swap_promoted(&self, registration: &crate::registry::ModuleRegistration) {
4747 self.apply_registration_capabilities(registration);
4748 }
4749}
4750
4751fn capability_requirement_status(status: RequirementStatus) -> CapabilityRequirementStatus {
4752 CapabilityRequirementStatus {
4753 consumer: status.consumer,
4754 capability: status.capability,
4755 need: match status.need {
4756 subc_protocol::manifest::CapabilityNeed::Required => "required".to_string(),
4757 subc_protocol::manifest::CapabilityNeed::Optional => "optional".to_string(),
4758 },
4759 verdict: status.verdict.as_str().to_string(),
4760 episode_seq: status.episode_seq,
4761 config_satisfiable: status.config_satisfiable,
4762 runtime_available: status.runtime_available,
4763 detail: status.detail,
4764 }
4765}
4766
4767fn append_capability_problem_detail(
4768 detail: Option<String>,
4769 capability_detail: Option<String>,
4770) -> Option<String> {
4771 match (detail, capability_detail) {
4772 (Some(detail), Some(capability_detail)) => Some(format!("{detail}; {capability_detail}")),
4773 (Some(detail), None) => Some(detail),
4774 (None, Some(capability_detail)) => Some(capability_detail),
4775 (None, None) => None,
4776 }
4777}
4778
4779fn subc_ops() -> Vec<String> {
4780 SUBC_CONTROL_OPS
4781 .iter()
4782 .map(|op| (*op).to_string())
4783 .collect()
4784}
4785
4786fn module_subc_ops() -> Vec<String> {
4787 SUBC_CONTROL_OPS
4788 .iter()
4789 .chain(MODULE_TO_SUBC_CONTROL_OPS.iter())
4790 .map(|op| (*op).to_string())
4791 .collect()
4792}
4793
4794#[cfg(test)]
4795fn module_baseline_control_ops() -> Vec<String> {
4796 MODULE_BASELINE_CONTROL_OPS
4797 .iter()
4798 .map(|op| (*op).to_string())
4799 .collect()
4800}
4801
4802fn effective_module_control_ops(declared: Option<Vec<String>>) -> Vec<String> {
4803 let mut seen = HashSet::new();
4804 let mut effective = Vec::new();
4805 for op in MODULE_BASELINE_CONTROL_OPS {
4806 if seen.insert((*op).to_string()) {
4807 effective.push((*op).to_string());
4808 }
4809 }
4810 for op in declared.unwrap_or_default() {
4811 if seen.insert(op.clone()) {
4812 effective.push(op);
4813 }
4814 }
4815 effective
4816}
4817
4818fn module_registration_grants_op(control_ops: &[String], op: &str) -> bool {
4819 MODULE_BASELINE_CONTROL_OPS.contains(&op) || control_ops.iter().any(|granted| granted == op)
4820}
4821
4822fn target_module_id(target: &RouteTarget) -> &str {
4823 match target {
4824 RouteTarget::ToolProvider { module_id }
4825 | RouteTarget::ManagementSurface { module_id }
4826 | RouteTarget::InternalService { module_id, .. } => module_id,
4827 }
4828}
4829
4830fn target_has_required_role(target: &RouteTarget, roles: &[ProviderRole]) -> bool {
4831 roles.iter().any(|role| match (target, role) {
4832 (RouteTarget::ToolProvider { .. }, ProviderRole::ToolProvider { .. }) => true,
4833 (RouteTarget::ManagementSurface { .. }, ProviderRole::ManagementSurface { .. }) => true,
4834 (
4835 RouteTarget::InternalService { service_id, .. },
4836 ProviderRole::InternalService {
4837 service_id: provided,
4838 ..
4839 },
4840 ) => service_id == provided,
4841 _ => false,
4842 })
4843}
4844
4845fn is_routable_role(role: &ProviderRole) -> bool {
4846 matches!(
4847 role,
4848 ProviderRole::ToolProvider { .. }
4849 | ProviderRole::ManagementSurface { .. }
4850 | ProviderRole::InternalService { .. }
4851 )
4852}
4853
4854#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4855enum ControlRequestBodyError {
4856 UnknownOp,
4857 InvalidBody,
4858}
4859
4860#[derive(Debug, Deserialize)]
4861struct ControlOpProbe {
4862 op: String,
4863}
4864
4865const MODULE_PUSH_OPS: &[&str] = &["route.status"];
4868
4869fn is_known_module_push_op(body: &[u8]) -> bool {
4870 serde_json::from_slice::<ControlOpProbe>(body)
4871 .map(|probe| MODULE_PUSH_OPS.contains(&probe.op.as_str()))
4872 .unwrap_or(false)
4873}
4874
4875fn is_known_module_request_op(body: &[u8]) -> bool {
4876 serde_json::from_slice::<ControlOpProbe>(body)
4877 .map(|probe| MODULE_TO_SUBC_CONTROL_OPS.contains(&probe.op.as_str()))
4878 .unwrap_or(false)
4879}
4880
4881fn log_control_dispatch_arrival(op: &'static str, connection_id: ConnectionId, corr: u64) {
4882 debug!(
4883 op = %op,
4884 connection_id = connection_id.get(),
4885 corr,
4886 "control dispatch"
4887 );
4888}
4889
4890fn log_slow_control_dispatch(
4891 dispatch_started_at: Option<StdInstant>,
4892 op: &'static str,
4893 connection_id: ConnectionId,
4894 corr: u64,
4895) {
4896 let Some(dispatch_started_at) = dispatch_started_at else {
4897 return;
4898 };
4899 let elapsed = dispatch_started_at.elapsed();
4900 if elapsed >= SLOW_CONTROL_DISPATCH_THRESHOLD {
4901 warn!(
4902 op = %op,
4903 connection_id = connection_id.get(),
4904 corr,
4905 elapsed_ms = elapsed.as_millis() as u64,
4906 "slow control dispatch"
4907 );
4908 }
4909}
4910
4911fn client_control_request_op(request: &ClientControlRequest) -> &'static str {
4912 match request {
4913 ClientControlRequest::ServerDescribe {} => ops::SERVER_DESCRIBE,
4914 ClientControlRequest::SupervisorProvenance { .. } => ops::SUPERVISOR_PROVENANCE,
4915 ClientControlRequest::CatalogList { .. } => ops::CATALOG_LIST,
4916 ClientControlRequest::RouteOpen { .. } => ops::ROUTE_OPEN,
4917 ClientControlRequest::RoutePoll { .. } => ops::ROUTE_POLL,
4918 ClientControlRequest::SupervisorList {} => ops::SUPERVISOR_LIST,
4919 ClientControlRequest::SupervisorSpawnSnapshot {} => ops::SUPERVISOR_SPAWN_SNAPSHOT,
4920 ClientControlRequest::SupervisorSpawnSubscribe { .. } => ops::SUPERVISOR_SPAWN_SUBSCRIBE,
4921 ClientControlRequest::SupervisorRestart { .. } => ops::SUPERVISOR_RESTART,
4922 ClientControlRequest::SupervisorSwap { .. } => ops::SUPERVISOR_SWAP,
4923 ClientControlRequest::SupervisorReload { .. } => ops::SUPERVISOR_RELOAD,
4924 ClientControlRequest::SupervisorRescan { .. } => ops::SUPERVISOR_RESCAN,
4925 ClientControlRequest::SupervisorReleaseReserved { .. } => ops::SUPERVISOR_RELEASE_RESERVED,
4926 ClientControlRequest::SupervisorSetEnabled { .. } => ops::SUPERVISOR_SET_ENABLED,
4927 ClientControlRequest::SupervisorHealthProbe { .. } => ops::SUPERVISOR_HEALTH_PROBE,
4928 ClientControlRequest::SupervisorHealth {} => ops::SUPERVISOR_HEALTH,
4929 ClientControlRequest::SupervisorRoutes { .. } => ops::SUPERVISOR_ROUTES,
4930 ClientControlRequest::SupervisorStderrTail { .. } => ops::SUPERVISOR_STDERR_TAIL,
4931 ClientControlRequest::SupervisorTerminals { .. } => ops::SUPERVISOR_TERMINALS,
4932 }
4933}
4934
4935fn module_control_request_op(request: &ModuleControlRequestFromModule) -> &'static str {
4936 match request {
4937 ModuleControlRequestFromModule::CatalogUpdate { .. } => MODULE_TO_SUBC_OP_CATALOG_UPDATE,
4938 ModuleControlRequestFromModule::LiveRoots {} => "supervisor.live_roots",
4939 }
4940}
4941
4942fn parse_client_control_request(
4943 body: &[u8],
4944) -> Result<ClientControlRequest, (serde_json::Error, ControlRequestBodyError)> {
4945 serde_json::from_slice::<ClientControlRequest>(body).map_err(|err| {
4946 let classification = match serde_json::from_slice::<ControlOpProbe>(body) {
4947 Ok(probe) if SUBC_CONTROL_OPS.contains(&probe.op.as_str()) => {
4948 ControlRequestBodyError::InvalidBody
4949 }
4950 Ok(_) => ControlRequestBodyError::UnknownOp,
4951 Err(_) => ControlRequestBodyError::InvalidBody,
4952 };
4953 (err, classification)
4954 })
4955}
4956
4957fn parse_module_control_request_from_module(
4958 body: &[u8],
4959) -> Result<ModuleControlRequestFromModule, (serde_json::Error, ControlRequestBodyError)> {
4960 serde_json::from_slice::<ModuleControlRequestFromModule>(body).map_err(|err| {
4961 let classification = match serde_json::from_slice::<ControlOpProbe>(body) {
4962 Ok(probe) if MODULE_TO_SUBC_CONTROL_OPS.contains(&probe.op.as_str()) => {
4963 ControlRequestBodyError::InvalidBody
4964 }
4965 Ok(_) => ControlRequestBodyError::UnknownOp,
4966 Err(_) => ControlRequestBodyError::InvalidBody,
4967 };
4968 (err, classification)
4969 })
4970}
4971
4972#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
4973enum ProviderRoleKind {
4974 ToolProvider,
4975 PipelineStage,
4976 ManagementSurface,
4977 InternalService,
4978}
4979
4980fn provider_role_kind(role: &ProviderRole) -> ProviderRoleKind {
4981 match role {
4982 ProviderRole::ToolProvider { .. } => ProviderRoleKind::ToolProvider,
4983 ProviderRole::PipelineStage { .. } => ProviderRoleKind::PipelineStage,
4984 ProviderRole::ManagementSurface { .. } => ProviderRoleKind::ManagementSurface,
4985 ProviderRole::InternalService { .. } => ProviderRoleKind::InternalService,
4986 }
4987}
4988
4989fn provider_role_kind_set(roles: &[ProviderRole]) -> BTreeSet<ProviderRoleKind> {
4990 roles.iter().map(provider_role_kind).collect()
4991}
4992
4993fn capability_census_trigger(
4997 old: Option<&CapabilityDeclarations>,
4998 new: Option<&CapabilityDeclarations>,
4999) -> bool {
5000 let old_provides = old
5001 .map(|capabilities| capabilities.provides.iter().collect::<HashSet<_>>())
5002 .unwrap_or_default();
5003 let old_denies = old
5004 .map(|capabilities| capabilities.must_never_reach.iter().collect::<HashSet<_>>())
5005 .unwrap_or_default();
5006 let new = new.cloned().unwrap_or(CapabilityDeclarations {
5007 provides: Vec::new(),
5008 requires: Vec::new(),
5009 must_never_reach: Vec::new(),
5010 });
5011
5012 new.provides
5013 .iter()
5014 .any(|capability| !old_provides.contains(capability))
5015 || new
5016 .must_never_reach
5017 .iter()
5018 .any(|capability| !old_denies.contains(capability))
5019}
5020
5021fn denied_capability<'a>(
5024 opening_manifest: &'a ModuleManifest,
5025 target_manifest: &ModuleManifest,
5026) -> Option<&'a str> {
5027 let opening_capabilities = opening_manifest.capabilities.as_ref()?;
5028 let target_capabilities = target_manifest.capabilities.as_ref()?;
5029 opening_capabilities
5030 .must_never_reach
5031 .iter()
5032 .find(|denied| {
5033 target_capabilities
5034 .provides
5035 .iter()
5036 .any(|provided| provided == *denied)
5037 })
5038 .map(String::as_str)
5039}
5040
5041fn catalog_update_frozen_field_message(
5042 registered: &ModuleManifest,
5043 provides: &[ProviderRole],
5044) -> Option<String> {
5045 let old_has_provides = !registered.provides.is_empty();
5046 let new_has_provides = !provides.is_empty();
5047 if old_has_provides != new_has_provides {
5048 return Some(format!(
5049 "catalog.update cannot change module '{}' between supervision-only and routable; routability is fixed at HELLO",
5050 registered.module_id
5051 ));
5052 }
5053
5054 if provider_role_kind_set(®istered.provides) != provider_role_kind_set(provides) {
5055 return Some(format!(
5056 "catalog.update cannot change provider role kinds for module '{}'; role kinds are fixed at HELLO",
5057 registered.module_id
5058 ));
5059 }
5060
5061 let registered_concurrency = manifest_concurrency(registered);
5062 let mut candidate = registered.clone();
5063 candidate.provides = provides.to_vec();
5064 let candidate_concurrency = manifest_concurrency(&candidate);
5065 if candidate_concurrency != registered_concurrency {
5066 return Some(format!(
5067 "catalog.update cannot change module '{}' concurrency from {:?} to {:?}; concurrency is fixed at HELLO",
5068 registered.module_id, registered_concurrency, candidate_concurrency
5069 ));
5070 }
5071
5072 None
5075}
5076
5077fn manifest_provides_routable_role(manifest: &ModuleManifest) -> bool {
5078 manifest.provides.iter().any(is_routable_role)
5079}
5080
5081fn manifest_concurrency(manifest: &ModuleManifest) -> Concurrency {
5087 manifest
5088 .provides
5089 .iter()
5090 .find_map(|provider| match provider {
5091 ProviderRole::ToolProvider { concurrency, .. }
5092 | ProviderRole::ManagementSurface { concurrency, .. } => Some(concurrency.clone()),
5093 ProviderRole::PipelineStage { .. } | ProviderRole::InternalService { .. } => None,
5094 })
5095 .unwrap_or(Concurrency::ModuleManaged)
5096}
5097
5098fn manifest_concurrency_was_defaulted(raw_hello: &[u8], manifest: &ModuleManifest) -> bool {
5105 let has_management_surface = manifest
5106 .provides
5107 .iter()
5108 .any(|provider| matches!(provider, ProviderRole::ManagementSurface { .. }));
5109 if !has_management_surface {
5110 return false;
5111 }
5112 let Ok(raw) = serde_json::from_slice::<serde_json::Value>(raw_hello) else {
5113 return false;
5114 };
5115 let Some(provides) = raw
5116 .get("manifest")
5117 .and_then(|manifest| manifest.get("provides"))
5118 .and_then(serde_json::Value::as_array)
5119 else {
5120 return false;
5121 };
5122 provides.iter().any(|role| {
5127 role.get("role").and_then(serde_json::Value::as_str) == Some("management_surface")
5128 && role.get("concurrency").is_none()
5129 })
5130}
5131
5132fn negotiate_version(peer_version: u8) -> Result<u8, String> {
5133 if peer_version != PROTOCOL_VERSION {
5134 return Err(format!(
5135 "protocol_ver {peer_version} is unsupported; this daemon requires exactly {PROTOCOL_VERSION}"
5136 ));
5137 }
5138 Ok(PROTOCOL_VERSION)
5139}
5140
5141fn pong(frame: &Frame) -> Result<Frame, RouterError> {
5142 Frame::build_with_version(
5143 response_version(frame),
5144 FrameType::Pong,
5145 frame.header.flags,
5146 0,
5147 0,
5148 frame.header.corr,
5149 Vec::new(),
5150 )
5151 .map_err(RouterError::FrameBuild)
5152}
5153
5154fn control_error_frame(
5155 frame: &Frame,
5156 code: &'static str,
5157 message: impl Into<String>,
5158) -> Result<Frame, RouterError> {
5159 control_error_body_frame(
5160 frame,
5161 ErrorBody {
5162 code: code.to_string(),
5163 message: message.into(),
5164 detail: None,
5165 },
5166 )
5167}
5168
5169fn control_error_body_frame(frame: &Frame, error: ErrorBody) -> Result<Frame, RouterError> {
5170 let body = serde_json::to_vec(&error).map_err(|err| {
5171 RouterError::backend(
5172 0,
5173 frame.header.corr,
5174 format!("failed to encode control ERROR: {err}"),
5175 )
5176 })?;
5177
5178 Frame::build_with_version(
5179 response_version(frame),
5180 FrameType::Error,
5181 control_flags(),
5182 0,
5183 0,
5184 frame.header.corr,
5185 body,
5186 )
5187 .map_err(RouterError::FrameBuild)
5188}
5189
5190fn control_response_body_frame<T: Serialize>(
5191 frame: &Frame,
5192 reply: &T,
5193 label: &'static str,
5194) -> Result<Frame, RouterError> {
5195 let body = serde_json::to_vec(reply).map_err(|err| {
5196 RouterError::backend(
5197 0,
5198 frame.header.corr,
5199 format!("failed to encode {label}: {err}"),
5200 )
5201 })?;
5202
5203 Frame::build_with_version(
5204 response_version(frame),
5205 FrameType::Response,
5206 control_flags(),
5207 0,
5208 0,
5209 frame.header.corr,
5210 body,
5211 )
5212 .map_err(RouterError::FrameBuild)
5213}
5214
5215fn forwarding_error_code(err: &ForwardingError) -> &'static str {
5245 match err {
5246 ForwardingError::NoModuleConnection => "target_unavailable",
5247 ForwardingError::ModuleReloading { .. } => "module_reloading",
5248 ForwardingError::ClientRouteChannelExhausted { .. }
5249 | ForwardingError::ModuleRouteChannelExhausted { .. } => "route_limit",
5250 ForwardingError::StaleModuleEndpoint
5251 | ForwardingError::UnknownReservation { .. }
5252 | ForwardingError::ConnectionClosing { .. }
5253 | ForwardingError::ClientEgressClosed { .. } => "target_unavailable",
5254 ForwardingError::CandidateSlotOccupied { .. } => "duplicate_module_id",
5257 ForwardingError::RelayCorrelationExhausted
5258 | ForwardingError::RouteOpenBuild(_)
5259 | ForwardingError::Poisoned => "forwarding_error",
5260 }
5261}
5262
5263fn response_version(frame: &Frame) -> u8 {
5264 if (MIN_SUPPORTED_VERSION..=PROTOCOL_VERSION).contains(&frame.header.ver) {
5265 frame.header.ver
5266 } else {
5267 PROTOCOL_VERSION
5268 }
5269}
5270
5271fn control_flags() -> Flags {
5272 Flags::new(false, Priority::Passive, false)
5273}
5274
5275fn send_goodbye_target_best_effort(target: &GoodbyeTarget, context: &str) {
5276 let Ok(frame) = Frame::build_with_version(
5277 target.negotiated_ver,
5278 FrameType::Goodbye,
5279 control_flags(),
5280 target.channel,
5281 target.epoch,
5282 0,
5283 Vec::new(),
5284 ) else {
5285 return;
5286 };
5287 if let Err(err) = target.sink.try_send(frame) {
5288 warn!(
5289 route_channel = target.channel,
5290 route_epoch = target.epoch,
5291 error = %err,
5292 %context,
5293 "route GOODBYE dropped under backpressure"
5294 );
5295 }
5296}
5297
5298pub(crate) fn send_route_control_pushes(
5299 forwarding: &ForwardingTable,
5300 routes: Vec<EndpointRoute>,
5301 push: ClientControlPush,
5302) {
5303 let body = match serde_json::to_vec(&push) {
5304 Ok(body) => body,
5305 Err(err) => {
5306 warn!(error = %err, "failed to serialize route lifecycle control PUSH");
5307 return;
5308 }
5309 };
5310 let mut targets = Vec::new();
5311 for route in routes {
5312 let target = route.goodbye_target;
5313 if let Some(existing) = targets
5314 .iter()
5315 .find(|existing: &&GoodbyeTarget| existing.connection_id == target.connection_id)
5316 {
5317 debug_assert_eq!(
5318 existing.negotiated_ver, target.negotiated_ver,
5319 "one connection cannot negotiate multiple frame versions"
5320 );
5321 continue;
5322 }
5323 targets.push(target);
5324 }
5325 for target in targets {
5326 let frame = match Frame::build_with_version(
5327 target.negotiated_ver,
5328 FrameType::Push,
5329 control_flags(),
5330 0,
5331 0,
5332 0,
5333 body.clone(),
5334 ) {
5335 Ok(frame) => frame,
5336 Err(err) => {
5337 warn!(
5338 route_channel = target.channel,
5339 error = %err,
5340 "failed to build route lifecycle control PUSH frame"
5341 );
5342 continue;
5343 }
5344 };
5345 if let Err(err) = target.sink.try_send(frame) {
5346 if target.close_on_delivery_failure() {
5347 warn!(
5348 target_connection_id = target.connection_id.get(),
5349 route_channel = target.channel,
5350 error = %err,
5351 "route lifecycle control PUSH was not delivered to client; closing target connection"
5352 );
5353 let _ = forwarding.escalate_client_delivery_failure(
5354 target.connection_id,
5355 target.channel,
5356 target.epoch,
5357 CloseReason::new(
5358 "route_lifecycle_push_delivery_failed",
5359 format!(
5360 "failed to enqueue route lifecycle control PUSH for channel {}: {err}",
5361 target.channel
5362 ),
5363 ),
5364 crate::forwarding::UndeliveredFrame {
5365 module_id: target.module_id.as_deref(),
5366 sink: &target.sink,
5367 },
5368 );
5369 }
5370 }
5371 }
5372}
5373
5374#[cfg(test)]
5375mod tests {
5376 use std::{
5377 collections::BTreeMap,
5378 fmt,
5379 path::PathBuf,
5380 sync::{Arc, Mutex},
5381 time::Duration,
5382 };
5383
5384 use serde_json::{json, Value};
5385 use subc_protocol::{
5386 manifest::{
5387 Concurrency, ExecutionMode, IdentityScope, ManagementOperation,
5388 ManagementOperationKind, ObservabilityKind, ObservabilitySurface, ProviderRole, Tool,
5389 },
5390 session::HealthStatus,
5391 FrameType,
5392 };
5393
5394 use super::*;
5395 use crate::{
5396 forwarding::{DataRoute, DataRouteState},
5397 registry::ChannelState,
5398 router::FrameSink,
5399 stderr_tail::DEFAULT_MAX_LINE_BYTES,
5400 supervise::{ModuleSpec, ModuleState, RestartPolicy, Supervisor, SupervisorHandle},
5401 test_support::TestTempDir,
5402 RouteCtx, Router,
5403 };
5404 use tokio::{
5405 sync::mpsc,
5406 time::{sleep, Instant},
5407 };
5408 use tracing::{
5409 field::{Field, Visit},
5410 Event, Subscriber,
5411 };
5412 use tracing_subscriber::{layer::Context, prelude::*, Layer};
5413
5414 fn fake_aft_stub_path() -> PathBuf {
5433 let mut path = std::env::current_exe().expect("current_exe available in tests");
5434 path.pop(); path.pop(); path.push(if cfg!(windows) {
5437 "fake-aft-stub.exe"
5438 } else {
5439 "fake-aft-stub"
5440 });
5441 assert!(
5442 path.exists(),
5443 "fake-aft-stub not built at {}: run `cargo test -p subc-core` (which builds \
5444 [[bin]] targets) rather than `cargo test -p subc-core --lib` (which does not)",
5445 path.display()
5446 );
5447 path
5448 }
5449
5450 fn client_retries(code: &str) -> bool {
5455 subc_protocol::error_codes::is_retryable_route_open(code)
5456 }
5457
5458 #[test]
5469 fn retryability_of_forwarding_codes_matches_the_failure() {
5470 let transient = [
5473 ForwardingError::NoModuleConnection,
5474 ForwardingError::ModuleReloading {
5475 module_id: "m".into(),
5476 },
5477 ForwardingError::StaleModuleEndpoint,
5478 ForwardingError::UnknownReservation {
5479 client_channel: 1,
5480 module_channel: 1,
5481 },
5482 ForwardingError::ConnectionClosing {
5483 connection_id: ConnectionId::new(1),
5484 },
5485 ForwardingError::ClientEgressClosed {
5486 connection_id: ConnectionId::new(1),
5487 },
5488 ];
5489 for err in transient {
5490 let code = forwarding_error_code(&err);
5491 assert!(
5492 client_retries(code),
5493 "{err:?} is transient but publishes {code:?}, which clients treat as permanent"
5494 );
5495 }
5496
5497 let permanent = [
5502 ForwardingError::ClientRouteChannelExhausted {
5503 connection_id: ConnectionId::new(1),
5504 },
5505 ForwardingError::ModuleRouteChannelExhausted {
5506 endpoint: ModuleEndpointId {
5507 connection_id: ConnectionId::new(1),
5508 generation: 1,
5509 },
5510 },
5511 ForwardingError::RelayCorrelationExhausted,
5512 ForwardingError::RouteOpenBuild("x".into()),
5513 ForwardingError::Poisoned,
5514 ];
5515 for err in permanent {
5516 let code = forwarding_error_code(&err);
5517 assert!(
5518 !client_retries(code),
5519 "{err:?} cannot be fixed by retrying but publishes {code:?}, which clients retry"
5520 );
5521 }
5522 }
5523
5524 #[tokio::test]
5552 async fn an_unattested_caller_is_never_stamped_as_a_supervised_module() {
5553 let handler = ControlHandler::default();
5554 let frame =
5555 Frame::build(FrameType::Request, control_flags(), 0, 0, 900, Vec::new()).unwrap();
5556
5557 let stamped = handler.route_open_principal(&frame, None).unwrap().unwrap();
5561 assert_eq!(
5562 stamped,
5563 Principal::Direct,
5564 "a caller that proved nothing must not be stamped as a supervised module"
5565 );
5566
5567 let forged = handler
5572 .route_open_principal(
5573 &frame,
5574 Some(ConsumerIdentity {
5575 module_id: "aft".to_string(),
5576 launch_nonce: "not-a-real-nonce".to_string(),
5577 }),
5578 )
5579 .unwrap();
5580 let refusal = forged.expect_err("an unmatched launch nonce must not yield a principal");
5581 assert_eq!(parse_error(&refusal)["code"], "bad_consumer_identity");
5582 }
5583
5584 #[test]
5601 fn a_wire_body_actually_yields_the_consumer_identity_the_daemon_stamps_from() {
5602 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"}}"#;
5603 let parsed: ClientControlRequest = serde_json::from_slice(body).unwrap();
5604 let ClientControlRequest::RouteOpen {
5605 consumer_identity, ..
5606 } = parsed
5607 else {
5608 panic!("route.open body must parse as RouteOpen");
5609 };
5610 assert_eq!(
5611 consumer_identity,
5612 Some(ConsumerIdentity {
5613 module_id: "aft".to_string(),
5614 launch_nonce: "n".to_string(),
5615 }),
5616 "the wire field name must reach the value route_open_principal reads"
5617 );
5618 }
5619
5620 fn manifest(module_id: &str, protocol_ver: u8) -> ModuleManifest {
5621 ModuleManifest::builder(module_id, "0.1.0")
5622 .protocol_ver(protocol_ver)
5623 .provides(vec![ProviderRole::ToolProvider {
5624 tools: vec![Tool {
5625 name: "read".to_string(),
5626 description: None,
5627 execution_mode: ExecutionMode::Pure,
5628 schema: json!({"type": "object"}),
5629 }],
5630 identity_scope: vec![IdentityScope::Project, IdentityScope::Session],
5631 concurrency: Concurrency::ModuleManaged,
5632 emits_push: true,
5633 sub_supervises: true,
5634 }])
5635 .build()
5636 }
5637
5638 fn hello_frame(module_id: &str, protocol_ver: u8, corr: u64) -> Frame {
5639 hello_frame_with_control_ops(module_id, protocol_ver, corr, None)
5640 }
5641
5642 fn hello_frame_with_control_ops(
5643 module_id: &str,
5644 protocol_ver: u8,
5645 corr: u64,
5646 control_ops: Option<Vec<String>>,
5647 ) -> Frame {
5648 hello_frame_full(module_id, protocol_ver, corr, control_ops, None)
5649 }
5650
5651 fn hello_frame_with_nonce(
5652 module_id: &str,
5653 protocol_ver: u8,
5654 corr: u64,
5655 launch_nonce: Option<&str>,
5656 ) -> Frame {
5657 hello_frame_full(
5658 module_id,
5659 protocol_ver,
5660 corr,
5661 None,
5662 launch_nonce.map(ToOwned::to_owned),
5663 )
5664 }
5665
5666 fn hello_frame_full(
5667 module_id: &str,
5668 protocol_ver: u8,
5669 corr: u64,
5670 control_ops: Option<Vec<String>>,
5671 launch_nonce: Option<String>,
5672 ) -> Frame {
5673 let body = serde_json::to_vec(&ModuleHelloBody {
5674 manifest: manifest(module_id, protocol_ver),
5675 protocol_ver,
5676 control_ops,
5677 launch_nonce,
5678 })
5679 .unwrap();
5680 Frame::build(FrameType::Hello, control_flags(), 0, 0, corr, body).unwrap()
5681 }
5682
5683 fn non_routable_hello_frame_with_control_ops(
5684 module_id: &str,
5685 corr: u64,
5686 control_ops: Option<Vec<String>>,
5687 ) -> Frame {
5688 let mut manifest = manifest(module_id, PROTOCOL_VERSION);
5689 manifest.provides.clear();
5690 let body = serde_json::to_vec(&ModuleHelloBody {
5691 manifest,
5692 protocol_ver: PROTOCOL_VERSION,
5693 control_ops,
5694 launch_nonce: None,
5695 })
5696 .unwrap();
5697 Frame::build(FrameType::Hello, control_flags(), 0, 0, corr, body).unwrap()
5698 }
5699
5700 fn capability_grammar_hello_frame(
5701 capabilities: Value,
5702 runtime_computed: Option<Value>,
5703 corr: u64,
5704 ) -> Frame {
5705 let mut body = serde_json::to_value(ModuleHelloBody {
5706 manifest: manifest("capability-grammar-test", PROTOCOL_VERSION),
5707 protocol_ver: PROTOCOL_VERSION,
5708 control_ops: None,
5709 launch_nonce: None,
5710 })
5711 .expect("HELLO body serializes");
5712 body["manifest"]["capabilities"] = capabilities;
5713 if let Some(runtime_computed) = runtime_computed {
5714 body["runtime_computed"] = runtime_computed;
5715 }
5716 Frame::build(
5717 FrameType::Hello,
5718 control_flags(),
5719 0,
5720 0,
5721 corr,
5722 serde_json::to_vec(&body).expect("HELLO body reserializes"),
5723 )
5724 .expect("HELLO frame builds")
5725 }
5726
5727 fn channel_request(channel: u16, corr: u64) -> Frame {
5728 Frame::build(
5729 FrameType::Request,
5730 Flags::new(true, Priority::Interactive, false),
5731 channel,
5732 0,
5733 corr,
5734 b"opaque".to_vec(),
5735 )
5736 .unwrap()
5737 }
5738
5739 fn route_ctx(
5740 connection_id: ConnectionId,
5741 ) -> (RouteCtx, mpsc::Receiver<crate::router::OutboundFrame>) {
5742 let (tx, rx) = mpsc::channel(8);
5743 (
5744 RouteCtx {
5745 connection_id,
5746 egress: FrameSink::new(tx),
5747 },
5748 rx,
5749 )
5750 }
5751
5752 fn parse_ack(frame: &Frame) -> ModuleHelloAckBody {
5753 serde_json::from_slice(&frame.body).unwrap()
5754 }
5755
5756 fn parse_error(frame: &Frame) -> Value {
5757 serde_json::from_slice(&frame.body).unwrap()
5758 }
5759
5760 fn parse_route_poll(frame: &Frame) -> ClientControlResponse {
5761 serde_json::from_slice(&frame.body).unwrap()
5762 }
5763
5764 fn route_poll_frame(corr: u64, kind: PollKind, route_channel: u16) -> Frame {
5765 let body = serde_json::to_vec(&ClientControlRequest::RoutePoll {
5766 route_channel,
5767 route_epoch: 0,
5768 kind,
5769 })
5770 .unwrap();
5771 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
5772 }
5773
5774 fn supervisor_health_probe_frame(corr: u64, module_id: &str) -> Frame {
5775 let body = serde_json::to_vec(&ClientControlRequest::SupervisorHealthProbe {
5776 module_id: module_id.to_string(),
5777 })
5778 .unwrap();
5779 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
5780 }
5781
5782 fn route_open_frame(corr: u64, module_id: &str, project_root: TestTempDir) -> Frame {
5783 route_open_frame_with_consumer_capabilities(corr, module_id, project_root, None)
5784 }
5785
5786 fn route_open_frame_with_consumer_capabilities(
5787 corr: u64,
5788 module_id: &str,
5789 project_root: TestTempDir,
5790 consumer_capabilities: Option<Vec<String>>,
5791 ) -> Frame {
5792 let body = serde_json::to_vec(&ClientControlRequest::RouteOpen {
5793 target: RouteTarget::ToolProvider {
5794 module_id: module_id.to_string(),
5795 },
5796 identity: BindIdentity::new(
5797 project_root.path().to_path_buf(),
5798 "unit".to_string(),
5799 "session".to_string(),
5800 ),
5801 consumer_identity: None,
5802 consumer_capabilities,
5803 admission_facts: None,
5804 })
5805 .unwrap();
5806 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
5807 }
5808
5809 fn route_open_frame_with_admission_facts(
5810 corr: u64,
5811 module_id: &str,
5812 project_root: TestTempDir,
5813 consumer_identity: Option<subc_control::ConsumerIdentity>,
5814 facts: Option<Value>,
5815 ) -> Frame {
5816 let body = serde_json::to_vec(&ClientControlRequest::RouteOpen {
5817 target: RouteTarget::ToolProvider {
5818 module_id: module_id.to_string(),
5819 },
5820 identity: BindIdentity::new(
5821 project_root.path().to_path_buf(),
5822 "unit".to_string(),
5823 format!("session-{corr}"),
5824 ),
5825 consumer_identity,
5826 consumer_capabilities: None,
5827 admission_facts: facts,
5828 })
5829 .unwrap();
5830 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
5831 }
5832
5833 #[derive(Clone, Default)]
5834 struct EventCapture {
5835 events: Arc<Mutex<Vec<CapturedEvent>>>,
5836 }
5837
5838 #[derive(Clone, Debug)]
5839 struct CapturedEvent {
5840 target: String,
5841 fields: BTreeMap<String, String>,
5842 }
5843
5844 impl EventCapture {
5845 fn events(&self) -> Vec<CapturedEvent> {
5846 self.events.lock().unwrap().clone()
5847 }
5848 }
5849
5850 impl<S> Layer<S> for EventCapture
5851 where
5852 S: Subscriber,
5853 {
5854 fn on_event(&self, event: &Event<'_>, _context: Context<'_, S>) {
5855 let mut visitor = EventFieldVisitor::default();
5856 event.record(&mut visitor);
5857 self.events.lock().unwrap().push(CapturedEvent {
5858 target: event.metadata().target().to_string(),
5859 fields: visitor.fields,
5860 });
5861 }
5862 }
5863
5864 #[derive(Default)]
5865 struct EventFieldVisitor {
5866 fields: BTreeMap<String, String>,
5867 }
5868
5869 impl Visit for EventFieldVisitor {
5870 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
5871 self.fields
5872 .insert(field.name().to_string(), format!("{value:?}"));
5873 }
5874 }
5875
5876 fn health_response(corr: u64, status: HealthStatus) -> Frame {
5877 let body = serde_json::to_vec(&ModuleControlResponse::HealthCheck {
5878 status,
5879 detail: Some("warming".to_string()),
5880 metrics: Some(json!({"queue_depth": 3})),
5881 })
5882 .unwrap();
5883 Frame::build(FrameType::Response, control_flags(), 0, 0, corr, body).unwrap()
5884 }
5885
5886 fn route_bind_ack(corr: u64) -> Frame {
5887 let body = serde_json::to_vec(&ModuleControlResponse::RouteBindAck {}).unwrap();
5888 Frame::build(FrameType::Response, control_flags(), 0, 0, corr, body).unwrap()
5889 }
5890
5891 fn unique_project_root(label: &str) -> TestTempDir {
5892 TestTempDir::new(label)
5893 }
5894
5895 fn assert_route_poll_liveness(frame: &Frame, expected_live: bool) {
5896 match parse_route_poll(frame) {
5897 ClientControlResponse::RoutePoll {
5898 status: None,
5899 live: Some(live),
5900 ..
5901 } => assert_eq!(live, expected_live),
5902 other => panic!("unexpected route.poll response: {other:?}"),
5903 }
5904 }
5905
5906 fn bind_liveness_route(
5907 registry: &Registry,
5908 forwarding: &ForwardingTable,
5909 module_id: &str,
5910 ) -> (RouteCtx, u16, u32) {
5911 let module_connection = ConnectionId::new(101);
5912 let client_connection = ConnectionId::new(202);
5913 let registration = registry
5914 .register_with_control_ops(
5915 manifest(module_id, PROTOCOL_VERSION),
5916 PROTOCOL_VERSION,
5917 module_connection,
5918 module_baseline_control_ops(),
5919 )
5920 .unwrap();
5921 let (module_tx, _module_rx) = mpsc::channel(8);
5922 let endpoint = forwarding
5923 .register_module_connection(
5924 module_connection,
5925 module_id.to_string(),
5926 PROTOCOL_VERSION,
5927 manifest_concurrency(®istration.manifest),
5928 FrameSink::new(module_tx),
5929 )
5930 .unwrap();
5931 let (client_ctx, _client_rx) = route_ctx(client_connection);
5932 let pending = forwarding
5933 .begin_route_bind_relay_for_test(
5934 client_connection,
5935 client_ctx.egress.clone(),
5936 1,
5937 module_id,
5938 )
5939 .unwrap();
5940 assert_eq!(pending.endpoint, endpoint);
5941 let route_channel = pending.client_channel;
5942 let route_epoch = pending.client_epoch;
5943 forwarding
5944 .complete_pending_relay(
5945 module_connection,
5946 pending.corr,
5947 RouteBindRelayOutcome::Accepted,
5948 )
5949 .unwrap();
5950 (client_ctx, route_channel, route_epoch)
5951 }
5952
5953 struct FakeProcessLiveness {
5954 live: Option<bool>,
5955 }
5956
5957 impl ModuleProcessLiveness for FakeProcessLiveness {
5958 fn process_live(&self, _module_id: &str) -> Option<bool> {
5959 self.live
5960 }
5961 }
5962
5963 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
5964 async fn supervisor_stderr_tail_converts_a_real_truncated_ring_entry_to_prefix_only_wire_data()
5965 {
5966 let registry = Arc::new(Registry::default());
5967 let supervisor_handle = SupervisorHandle::new();
5968 let supervisor = Supervisor::new(
5969 Arc::clone(®istry),
5970 RestartPolicy::new(1, Duration::from_millis(10)),
5971 )
5972 .with_handle(supervisor_handle.clone());
5973 let source_line = format!("config error: {}", "x".repeat(DEFAULT_MAX_LINE_BYTES));
5974 let module = supervisor
5975 .spawn(ModuleSpec {
5976 module_id: "stderr-tail-wire".to_string(),
5977 program: fake_aft_stub_path(),
5978 args: Vec::new(),
5979 env: vec![
5980 ("FAKE_AFT_STDERR_LINE".to_string(), source_line.clone()),
5981 ("FAKE_AFT_EXIT_CODE".to_string(), "1".to_string()),
5982 ],
5983 reserved: false,
5984 reserved_prefixes: Vec::new(),
5985 protocol: ModuleProtocol::Subc,
5986 overlap: Default::default(),
5987 })
5988 .unwrap();
5989
5990 let deadline = Instant::now() + Duration::from_secs(5);
5991 loop {
5992 let tail = module.stderr_tail(None, None);
5993 if tail
5994 .entries
5995 .iter()
5996 .any(|entry| matches!(entry, TailEntry::ProcessStart))
5997 && tail.entries.iter().any(|entry| {
5998 matches!(
5999 entry,
6000 TailEntry::Line {
6001 truncated: true,
6002 ..
6003 }
6004 )
6005 })
6006 {
6007 break;
6008 }
6009 assert!(
6010 Instant::now() < deadline,
6011 "module did not produce a truncated line and restart boundary: {tail:?}"
6012 );
6013 sleep(Duration::from_millis(10)).await;
6014 }
6015
6016 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
6017 let request = ClientControlRequest::SupervisorStderrTail {
6018 module_id: "stderr-tail-wire".to_string(),
6019 max_lines: None,
6020 max_bytes: None,
6021 };
6022 let frame = Frame::build(
6023 FrameType::Request,
6024 control_flags(),
6025 0,
6026 0,
6027 1,
6028 serde_json::to_vec(&request).unwrap(),
6029 )
6030 .unwrap();
6031 let (ctx, _egress) = route_ctx(ConnectionId::new(1));
6032 let responses = handler.handle_control_frame(&ctx, frame).await.unwrap();
6033 let ClientControlResponse::SupervisorStderrTail { tail, .. } =
6034 serde_json::from_slice(&responses[0].body).unwrap()
6035 else {
6036 panic!("expected supervisor.stderr_tail response");
6037 };
6038
6039 assert!(
6040 tail.entries
6041 .iter()
6042 .any(|entry| matches!(entry, StderrTailEntry::ProcessStart)),
6043 "the control response lost the restart boundary"
6044 );
6045 let Some(StderrTailEntry::Line { text, truncated }) = tail.entries.iter().find(|entry| {
6046 matches!(
6047 entry,
6048 StderrTailEntry::Line {
6049 truncated: true,
6050 ..
6051 }
6052 )
6053 }) else {
6054 panic!("the control response lost the truncated line");
6055 };
6056 assert_eq!(text, &source_line[..DEFAULT_MAX_LINE_BYTES]);
6057 assert!(*truncated);
6058 }
6059
6060 #[tokio::test(flavor = "current_thread")]
6065 async fn supervisor_terminals_reads_the_journal_off_the_runtime_worker() {
6066 let dir = TestTempDir::new("terminals-off-worker");
6067 let journal_path = dir.join("terminals.jsonl");
6068 let registry = Arc::new(Registry::default());
6069 let supervisor_handle = SupervisorHandle::new();
6070 let supervisor =
6071 Supervisor::new(Arc::clone(®istry), RestartPolicy::new(1, Duration::ZERO))
6072 .with_handle(supervisor_handle.clone())
6073 .with_terminal_journal(journal_path.clone(), "off-worker-daemon".to_string());
6074 let module = supervisor
6075 .spawn(ModuleSpec {
6076 module_id: "terminal-off-worker".to_string(),
6077 program: fake_aft_stub_path(),
6078 args: Vec::new(),
6079 env: vec![("FAKE_AFT_EXIT_CODE".to_string(), "23".to_string())],
6080 reserved: false,
6081 reserved_prefixes: Vec::new(),
6082 protocol: ModuleProtocol::Subc,
6083 overlap: Default::default(),
6084 })
6085 .unwrap();
6086 let deadline = Instant::now() + Duration::from_secs(5);
6087 while module.terminal_history().entries.len() != 2 {
6088 assert!(Instant::now() < deadline, "module did not record two exits");
6089 sleep(Duration::from_millis(10)).await;
6090 }
6091
6092 let (started, release) = crate::terminal_journal::read_pause::install(&journal_path);
6093 let handler =
6094 Arc::new(ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle));
6095 let frame = Frame::build(
6096 FrameType::Request,
6097 control_flags(),
6098 0,
6099 0,
6100 1,
6101 serde_json::to_vec(&ClientControlRequest::SupervisorTerminals {
6102 module_id: "terminal-off-worker".to_string(),
6103 })
6104 .unwrap(),
6105 )
6106 .unwrap();
6107 let (ctx, _egress) = route_ctx(ConnectionId::new(1));
6108 let spawned_at = std::time::Instant::now();
6109 let read = tokio::spawn({
6110 let handler = Arc::clone(&handler);
6111 async move { handler.handle_control_frame(&ctx, frame).await }
6112 });
6113 tokio::task::spawn_blocking(move || started.recv_timeout(Duration::from_secs(5)))
6116 .await
6117 .unwrap()
6118 .expect("the history read reached its pause");
6119 let elapsed = spawned_at.elapsed();
6120 assert!(
6121 elapsed < Duration::from_secs(2) && !read.is_finished(),
6122 "this task could not run while the history read was paused \
6123 (resumed after {elapsed:?}, read finished: {})",
6124 read.is_finished()
6125 );
6126
6127 drop(release);
6128 let responses = read.await.unwrap().unwrap();
6129 let response: ClientControlResponse = serde_json::from_slice(&responses[0].body).unwrap();
6130 let ClientControlResponse::SupervisorTerminals { terminals, .. } = response else {
6131 panic!("expected supervisor.terminals response");
6132 };
6133 assert_eq!(terminals.entries.len(), 2);
6134 assert_eq!(terminals.journal_skipped_lines, 0);
6135 assert_eq!(terminals.journal_read_errors, 0);
6136 }
6137
6138 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6139 async fn supervisor_terminals_golden_is_generated_through_the_real_handler() {
6140 let registry = Arc::new(Registry::default());
6141 let supervisor_handle = SupervisorHandle::new();
6142 let supervisor =
6143 Supervisor::new(Arc::clone(®istry), RestartPolicy::new(1, Duration::ZERO))
6144 .with_handle(supervisor_handle.clone());
6145 let module = supervisor
6146 .spawn(ModuleSpec {
6147 module_id: "terminal-golden".to_string(),
6148 program: fake_aft_stub_path(),
6149 args: Vec::new(),
6150 env: vec![("FAKE_AFT_EXIT_CODE".to_string(), "23".to_string())],
6151 reserved: false,
6152 reserved_prefixes: Vec::new(),
6153 protocol: ModuleProtocol::Subc,
6154 overlap: Default::default(),
6155 })
6156 .unwrap();
6157
6158 let deadline = Instant::now() + Duration::from_secs(5);
6159 while module.terminal_history().entries.len() != 2 {
6160 assert!(
6161 Instant::now() < deadline,
6162 "module did not retain two terminal exits: {:?}",
6163 module.terminal_history()
6164 );
6165 sleep(Duration::from_millis(10)).await;
6166 }
6167
6168 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
6169 let request = ClientControlRequest::SupervisorTerminals {
6170 module_id: "terminal-golden".to_string(),
6171 };
6172 let frame = Frame::build(
6173 FrameType::Request,
6174 control_flags(),
6175 0,
6176 0,
6177 1,
6178 serde_json::to_vec(&request).unwrap(),
6179 )
6180 .unwrap();
6181 let (ctx, _egress) = route_ctx(ConnectionId::new(1));
6182 let responses = handler.handle_control_frame(&ctx, frame).await.unwrap();
6183 let response: ClientControlResponse = serde_json::from_slice(&responses[0].body).unwrap();
6184 let ClientControlResponse::SupervisorTerminals { terminals, .. } = &response else {
6185 panic!("expected supervisor.terminals response");
6186 };
6187 assert_eq!(terminals.entries.len(), 2);
6188 assert_eq!(terminals.dropped, 0);
6189
6190 let mut rendered = serde_json::to_value(response).unwrap();
6191 rendered["daemon_started_at_ms"] = json!(1_700_000_000_000u64);
6194 for (index, entry) in rendered["entries"]
6195 .as_array_mut()
6196 .expect("terminal response entries array")
6197 .iter_mut()
6198 .enumerate()
6199 {
6200 entry["at_ms"] = json!(1_700_000_000_001u64 + index as u64);
6201 }
6202
6203 let golden_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6204 .join("../subc-control/tests/golden/client_control_response_supervisor_terminals.json");
6205 let serialized = serde_json::to_string_pretty(&rendered).unwrap() + "\n";
6206 if std::env::var_os("UPDATE_GOLDEN").is_some() {
6207 std::fs::write(&golden_path, &serialized).unwrap();
6208 }
6209 let expected: Value =
6210 serde_json::from_str(&std::fs::read_to_string(&golden_path).unwrap()).unwrap();
6211 assert_eq!(rendered, expected);
6212 }
6213
6214 #[test]
6215 fn hello_registers_manifest_and_returns_ack() {
6216 let registry = Arc::new(Registry::default());
6217 let handler = ControlHandler::new(Arc::clone(®istry));
6218 let conn = ConnectionId::new(1);
6219
6220 let responses = handler
6221 .handle_control(conn, hello_frame("aft", PROTOCOL_VERSION, 7))
6222 .unwrap();
6223
6224 assert_eq!(responses.len(), 1);
6225 assert_eq!(responses[0].header.ty, FrameType::HelloAck);
6226 assert_eq!(responses[0].header.channel, 0);
6227 assert_eq!(responses[0].header.corr, 7);
6228 let ack = parse_ack(&responses[0]);
6229 assert_eq!(ack.negotiated_ver, PROTOCOL_VERSION);
6230 assert!(ack
6231 .subc_capabilities
6232 .contains(&CAP_MANIFEST_REGISTRATION.to_string()));
6233 assert!(ack.subc_ops.contains(&ops::SUPERVISOR_LIST.to_string()));
6234 assert!(ack.subc_ops.contains(&ops::SUPERVISOR_RESTART.to_string()));
6235 assert!(ack
6236 .subc_ops
6237 .contains(&ops::SUPERVISOR_SET_ENABLED.to_string()));
6238 assert!(ack
6239 .subc_ops
6240 .contains(&MODULE_TO_SUBC_OP_CATALOG_UPDATE.to_string()));
6241
6242 let registration = registry.get_module("aft").unwrap().unwrap();
6243 assert_eq!(registration.negotiated_ver, PROTOCOL_VERSION);
6244 assert_eq!(registration.state, ChannelState::Active);
6245 assert_eq!(registration.connection_id, conn);
6246 assert_eq!(registration.control_ops, module_baseline_control_ops());
6247 }
6248
6249 #[test]
6250 fn capability_grammar_refusals_name_the_field_and_leave_no_catalog_entry() {
6251 let invalid_identifiers = [
6252 ("case_change", "credentials-Provider/v1"),
6253 ("leading_zero", "credentials-provider/v01"),
6254 ("trailing_hyphen", "credentials-provider-/v1"),
6255 ("consecutive_hyphens", "credentials--provider/v1"),
6256 ("uppercase", "Credentials-provider/v1"),
6257 ("missing_v", "credentials-provider/1"),
6258 ("whitespace", "credentials provider/v1"),
6259 ("zero_version", "credentials-provider/v0"),
6260 ("out_of_range_version", "credentials-provider/v4294967296"),
6261 (
6262 "overlength_name",
6263 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/v1",
6264 ),
6265 ];
6266 let mut cases = invalid_identifiers
6267 .into_iter()
6268 .map(|(name, identifier)| {
6269 (
6270 format!("identifier_{name}"),
6271 "capabilities.provides[0]".to_string(),
6272 identifier.to_string(),
6273 json!({ "provides": [identifier] }),
6274 None,
6275 )
6276 })
6277 .collect::<Vec<_>>();
6278 cases.extend([
6279 (
6280 "unknown_need".to_string(),
6281 "capabilities.requires[0].need".to_string(),
6282 "deferred".to_string(),
6283 json!({ "requires": [{ "capability": "credentials-provider/v1", "need": "deferred" }] }),
6284 None,
6285 ),
6286 (
6287 "duplicate_provides".to_string(),
6288 "capabilities.provides[1]".to_string(),
6289 "credentials-provider/v1".to_string(),
6290 json!({ "provides": ["credentials-provider/v1", "credentials-provider/v1"] }),
6291 None,
6292 ),
6293 (
6294 "duplicate_must_never_reach".to_string(),
6295 "capabilities.must_never_reach[1]".to_string(),
6296 "credentials-provider/v1".to_string(),
6297 json!({ "must_never_reach": ["credentials-provider/v1", "credentials-provider/v1"] }),
6298 None,
6299 ),
6300 (
6301 "duplicate_requires_same_need".to_string(),
6302 "capabilities.requires[1]".to_string(),
6303 "credentials-provider/v1".to_string(),
6304 json!({ "requires": [
6305 { "capability": "credentials-provider/v1", "need": "required" },
6306 { "capability": "credentials-provider/v1", "need": "required" }
6307 ] }),
6308 None,
6309 ),
6310 (
6311 "duplicate_requires_conflicting_need".to_string(),
6312 "capabilities.requires[1]".to_string(),
6313 "credentials-provider/v1".to_string(),
6314 json!({ "requires": [
6315 { "capability": "credentials-provider/v1", "need": "required" },
6316 { "capability": "credentials-provider/v1", "need": "optional" }
6317 ] }),
6318 None,
6319 ),
6320 (
6321 "capabilities_root_pointer".to_string(),
6322 "runtime_computed[0]".to_string(),
6323 "/capabilities".to_string(),
6324 json!({}),
6325 Some(json!(["/capabilities"])),
6326 ),
6327 (
6328 "capabilities_descendant_pointer".to_string(),
6329 "runtime_computed[0]".to_string(),
6330 "/capabilities/provides".to_string(),
6331 json!({}),
6332 Some(json!(["/capabilities/provides"])),
6333 ),
6334 (
6335 "malformed_pointer_without_leading_slash".to_string(),
6336 "runtime_computed[0]".to_string(),
6337 "capabilities".to_string(),
6338 json!({}),
6339 Some(json!(["capabilities"])),
6340 ),
6341 (
6342 "malformed_pointer_escape".to_string(),
6343 "runtime_computed[0]".to_string(),
6344 "/roles/~2/tools".to_string(),
6345 json!({}),
6346 Some(json!(["/roles/~2/tools"])),
6347 ),
6348 (
6349 "unknown_capabilities_field".to_string(),
6350 "capabilities.future".to_string(),
6351 "<array>".to_string(),
6352 json!({ "future": [] }),
6353 None,
6354 ),
6355 ]);
6356
6357 for (index, (name, field, value, capabilities, runtime_computed)) in
6358 cases.into_iter().enumerate()
6359 {
6360 let registry = Arc::new(Registry::default());
6361 let handler = ControlHandler::new(Arc::clone(®istry));
6362 let response = handler
6363 .handle_control(
6364 ConnectionId::new((index + 1) as u64),
6365 capability_grammar_hello_frame(
6366 capabilities,
6367 runtime_computed,
6368 index as u64 + 1,
6369 ),
6370 )
6371 .expect("invalid HELLO returns a refusal");
6372
6373 assert_eq!(response.len(), 1, "{name} must emit one refusal");
6374 let error = parse_error(&response[0]);
6375 assert_eq!(error["code"], "invalid_capability_grammar", "{name}");
6376 let message = error["message"]
6377 .as_str()
6378 .expect("error message is a string");
6379 assert!(
6380 message.contains(&field),
6381 "{name}: field missing from {message}"
6382 );
6383 assert!(
6384 message.contains(&value),
6385 "{name}: value missing from {message}"
6386 );
6387 assert_eq!(
6388 registry
6389 .active_registration_count()
6390 .expect("registry reads"),
6391 0,
6392 "{name}: refused HELLO must not create a catalog entry"
6393 );
6394 }
6395 }
6396
6397 #[test]
6398 fn legal_runtime_pointer_and_capabilities_are_mirrored_in_catalog_list() {
6399 let registry = Arc::new(Registry::default());
6400 let handler = ControlHandler::new(Arc::clone(®istry));
6401 let capabilities = json!({
6402 "provides": ["credentials-provider/v1"],
6403 "requires": [{ "capability": "context-transform/v1", "need": "optional" }],
6404 "must_never_reach": ["federation-transport/v1"]
6405 });
6406 let response = handler
6407 .handle_control(
6408 ConnectionId::new(99),
6409 capability_grammar_hello_frame(
6410 capabilities.clone(),
6411 Some(json!(["/roles/0/tools"])),
6412 99,
6413 ),
6414 )
6415 .expect("valid HELLO registers");
6416 assert_eq!(response[0].header.ty, FrameType::HelloAck);
6417
6418 let request = Frame::build(
6419 FrameType::Request,
6420 control_flags(),
6421 0,
6422 0,
6423 100,
6424 serde_json::to_vec(&ClientControlRequest::CatalogList { module_id: None })
6425 .expect("catalog request serializes"),
6426 )
6427 .expect("catalog request frame builds");
6428 let response = handler
6429 .handle_catalog_list(request, None)
6430 .expect("catalog list succeeds");
6431 let ClientControlResponse::CatalogList { modules, .. } =
6432 serde_json::from_slice(&response[0].body).expect("catalog response decodes")
6433 else {
6434 panic!("catalog request must return catalog.list");
6435 };
6436 assert_eq!(modules.len(), 1);
6437 assert_eq!(
6438 serde_json::to_value(&modules[0].capabilities).expect("catalog capabilities serialize"),
6439 capabilities
6440 );
6441 }
6442
6443 #[test]
6444 fn catalog_list_mirrors_management_operation_description() {
6445 let registry = Arc::new(Registry::default());
6446 let handler = ControlHandler::new(Arc::clone(®istry));
6447 let description = "List managed records and return their identifiers and metadata.";
6448 let mut manifest = manifest("described-management", PROTOCOL_VERSION);
6449 manifest.provides = vec![ProviderRole::ManagementSurface {
6450 operations: vec![ManagementOperation {
6451 name: "records.list".to_string(),
6452 kind: ManagementOperationKind::Query,
6453 description: Some(description.to_string()),
6454 }],
6455 config_schema: json!({"type": "object"}),
6456 observability: vec![ObservabilitySurface {
6457 name: "records.stats".to_string(),
6458 kind: ObservabilityKind::Snapshot,
6459 }],
6460 identity_scope: vec![IdentityScope::Project],
6461 concurrency: Concurrency::ModuleManaged,
6462 }];
6463 registry
6464 .register_with_control_ops(
6465 manifest,
6466 PROTOCOL_VERSION,
6467 ConnectionId::new(99),
6468 Vec::new(),
6469 )
6470 .expect("described management manifest registers");
6471
6472 let request = Frame::build(
6473 FrameType::Request,
6474 control_flags(),
6475 0,
6476 0,
6477 100,
6478 serde_json::to_vec(&ClientControlRequest::CatalogList { module_id: None })
6479 .expect("catalog request serializes"),
6480 )
6481 .expect("catalog request frame builds");
6482 let response = handler
6483 .handle_catalog_list(request, None)
6484 .expect("catalog list succeeds");
6485 let body: Value = serde_json::from_slice(&response[0].body).expect("catalog response JSON");
6486 assert_eq!(
6487 body["modules"][0]["roles"][0]["operations"][0]["description"], description,
6488 "catalog.list must preserve the declared operation description verbatim"
6489 );
6490 }
6491
6492 #[test]
6493 fn reserved_capability_refusal_mutation_proof_leaves_no_catalog_entry() {
6494 let registry = Arc::new(Registry::default());
6495 let handler = ControlHandler::new(Arc::clone(®istry)).with_capability_config(
6496 [("vault".to_string(), true), ("squatter".to_string(), true)],
6497 BTreeMap::from([("credentials-provider/v1".to_string(), "vault".to_string())]),
6498 );
6499 let mut squatter = manifest("squatter", PROTOCOL_VERSION);
6500 squatter.capabilities = Some(subc_protocol::manifest::CapabilityDeclarations {
6501 provides: vec!["credentials-provider/v1".to_string()],
6502 requires: Vec::new(),
6503 must_never_reach: Vec::new(),
6504 });
6505 let frame = Frame::build(
6506 FrameType::Hello,
6507 control_flags(),
6508 0,
6509 0,
6510 77,
6511 serde_json::to_vec(&ModuleHelloBody {
6512 manifest: squatter,
6513 protocol_ver: PROTOCOL_VERSION,
6514 control_ops: None,
6515 launch_nonce: None,
6516 })
6517 .expect("HELLO serializes"),
6518 )
6519 .expect("HELLO frame builds");
6520 let response = handler
6521 .handle_control(ConnectionId::new(77), frame)
6522 .expect("reserved claim receives a typed refusal");
6523 assert_eq!(parse_error(&response[0])["code"], "reserved_capability");
6524 assert_eq!(
6525 registry
6526 .active_registration_count()
6527 .expect("registry reads"),
6528 0,
6529 "a reserved capability refusal must not leave a catalog entry"
6530 );
6531 }
6532
6533 #[test]
6534 fn server_describe_surfaces_required_capability_verdict_fields() {
6535 let registry = Arc::new(Registry::default());
6536 let handler = ControlHandler::new(Arc::clone(®istry)).with_capability_config(
6537 [
6538 ("consumer".to_string(), true),
6539 ("provider".to_string(), false),
6540 ],
6541 BTreeMap::new(),
6542 );
6543 let mut consumer = manifest("consumer", PROTOCOL_VERSION);
6544 consumer.capabilities = Some(subc_protocol::manifest::CapabilityDeclarations {
6545 provides: Vec::new(),
6546 requires: vec![subc_protocol::manifest::CapabilityRequirement {
6547 capability: "credentials-provider/v1".to_string(),
6548 need: subc_protocol::manifest::CapabilityNeed::Required,
6549 }],
6550 must_never_reach: Vec::new(),
6551 });
6552 let hello = Frame::build(
6553 FrameType::Hello,
6554 control_flags(),
6555 0,
6556 0,
6557 78,
6558 serde_json::to_vec(&ModuleHelloBody {
6559 manifest: consumer,
6560 protocol_ver: PROTOCOL_VERSION,
6561 control_ops: None,
6562 launch_nonce: None,
6563 })
6564 .expect("HELLO serializes"),
6565 )
6566 .expect("HELLO frame builds");
6567 handler
6568 .handle_control(ConnectionId::new(78), hello)
6569 .expect("consumer registers");
6570 let describe = Frame::build(
6571 FrameType::Request,
6572 control_flags(),
6573 0,
6574 0,
6575 79,
6576 serde_json::to_vec(&ClientControlRequest::ServerDescribe {})
6577 .expect("request serializes"),
6578 )
6579 .expect("describe frame builds");
6580 let response = handler
6581 .handle_server_describe(describe)
6582 .expect("server.describe succeeds");
6583 let rendered: Value = serde_json::from_slice(&response[0].body).expect("response JSON");
6584 let requirement = &rendered["capability_requirements"][0];
6585 assert_eq!(requirement["consumer"], "consumer");
6586 assert_eq!(requirement["verdict"], "never_provided");
6587 assert_eq!(requirement["episode_seq"], 1);
6588 assert_eq!(requirement["config_satisfiable"], false);
6589 assert_eq!(requirement["runtime_available"], false);
6590 assert!(requirement["detail"]
6591 .as_str()
6592 .expect("detail string")
6593 .contains("credentials-provider/v1"));
6594 }
6595
6596 #[test]
6597 fn catalog_list_omits_capabilities_for_legacy_manifest() {
6598 let registry = Arc::new(Registry::default());
6599 let handler = ControlHandler::new(Arc::clone(®istry));
6600 let hello = handler
6601 .handle_control(
6602 ConnectionId::new(101),
6603 hello_frame("legacy-capability-manifest", PROTOCOL_VERSION, 101),
6604 )
6605 .expect("legacy HELLO registers");
6606 assert_eq!(hello[0].header.ty, FrameType::HelloAck);
6607
6608 let request = Frame::build(
6609 FrameType::Request,
6610 control_flags(),
6611 0,
6612 0,
6613 102,
6614 serde_json::to_vec(&ClientControlRequest::CatalogList { module_id: None })
6615 .expect("catalog request serializes"),
6616 )
6617 .expect("catalog request frame builds");
6618 let response = handler
6619 .handle_catalog_list(request, None)
6620 .expect("catalog list succeeds");
6621 let body: Value = serde_json::from_slice(&response[0].body).expect("catalog response JSON");
6622 assert!(
6623 body["modules"][0].get("capabilities").is_none(),
6624 "legacy manifest must retain an absent capabilities field on catalog.list"
6625 );
6626 }
6627
6628 #[test]
6629 fn hello_ack_omits_storage_when_no_storage_config() {
6630 let registry = Arc::new(Registry::default());
6631 let handler = ControlHandler::new(Arc::clone(®istry));
6632 let responses = handler
6633 .handle_control(
6634 ConnectionId::new(1),
6635 hello_frame("aft", PROTOCOL_VERSION, 7),
6636 )
6637 .unwrap();
6638 let ack = parse_ack(&responses[0]);
6639 assert_eq!(ack.storage, None, "no storage config -> no descriptor");
6640 assert_eq!(ack.machine_id, None, "no machine id configured -> no field");
6641 }
6642
6643 #[tokio::test]
6644 async fn hello_ack_and_server_describe_carry_the_configured_machine_id() {
6645 let id = crate::machine_id::MachineId::parse("0123456789abcdef0123456789abcdef").unwrap();
6646 let registry = Arc::new(Registry::default());
6647 let handler = ControlHandler::new(Arc::clone(®istry)).with_machine_id(Some(id.clone()));
6648 let responses = handler
6649 .handle_control(
6650 ConnectionId::new(1),
6651 hello_frame("aft", PROTOCOL_VERSION, 7),
6652 )
6653 .unwrap();
6654 let ack = parse_ack(&responses[0]);
6655 assert_eq!(ack.machine_id.as_deref(), Some(id.as_str()));
6656
6657 let described = handler
6658 .handle_control_frame(
6659 &route_ctx(ConnectionId::new(2)).0,
6660 Frame::build(
6661 FrameType::Request,
6662 control_flags(),
6663 0,
6664 0,
6665 9,
6666 serde_json::to_vec(&ClientControlRequest::ServerDescribe {}).unwrap(),
6667 )
6668 .unwrap(),
6669 )
6670 .await
6671 .unwrap();
6672 let ClientControlResponse::ServerDescribe { machine_id, .. } =
6673 serde_json::from_slice(&described[0].body).unwrap()
6674 else {
6675 panic!("server.describe answered with another shape");
6676 };
6677 assert_eq!(machine_id.as_deref(), Some(id.as_str()));
6678 }
6679
6680 #[test]
6681 fn hello_ack_delivers_resolved_storage_descriptor_per_module() {
6682 let registry = Arc::new(Registry::default());
6685 let handler = ControlHandler::new(Arc::clone(®istry)).with_storage_config(Some(
6686 crate::daemon_config::StorageConfig::Sqlite {
6687 data_home: std::path::PathBuf::from("/data"),
6688 },
6689 ));
6690
6691 let responses = handler
6692 .handle_control(
6693 ConnectionId::new(1),
6694 hello_frame("alfonso-routing", PROTOCOL_VERSION, 7),
6695 )
6696 .unwrap();
6697 let ack = parse_ack(&responses[0]);
6698 assert_eq!(
6699 ack.storage,
6700 Some(serde_json::json!({
6701 "module_id": "alfonso-routing",
6702 "storage_namespace": "default",
6703 "isolation": { "kind": "module" },
6704 "backend": {
6705 "backend": "sqlite",
6706 "path": "/data/cortexkit/alfonso-routing/store.db"
6707 }
6708 })),
6709 "the delivered descriptor is the module's own sqlite store path"
6710 );
6711 }
6712
6713 #[test]
6714 fn hello_control_ops_none_is_baseline_and_guard_rejects_synthetic_gated_op() {
6715 let registry = Arc::new(Registry::default());
6716 let handler = ControlHandler::new(Arc::clone(®istry));
6717 let conn = ConnectionId::new(1);
6718 let responses = handler
6719 .handle_control(
6720 conn,
6721 hello_frame_with_control_ops("aft", PROTOCOL_VERSION, 7, None),
6722 )
6723 .unwrap();
6724 assert_eq!(responses[0].header.ty, FrameType::HelloAck);
6725 let registration = registry.get_module("aft").unwrap().unwrap();
6726 assert_eq!(registration.control_ops, module_baseline_control_ops());
6727
6728 let frame =
6729 Frame::build(FrameType::Request, control_flags(), 0, 0, 77, Vec::new()).unwrap();
6730 assert!(handler
6731 .guard_module_control_op(&frame, "aft", "route.bind")
6732 .unwrap()
6733 .is_none());
6734 let error = handler
6735 .guard_module_control_op(&frame, "aft", "test.synthetic")
6736 .unwrap()
6737 .expect("synthetic ungranted op should be rejected");
6738 assert_eq!(error.header.ty, FrameType::Error);
6739 assert_eq!(parse_error(&error)["code"], "op_not_allowed");
6740 }
6741
6742 #[test]
6743 fn hello_control_ops_some_adds_optional_grants() {
6744 let registry = Arc::new(Registry::default());
6745 let handler = ControlHandler::new(Arc::clone(®istry));
6746 handler
6747 .handle_control(
6748 ConnectionId::new(1),
6749 hello_frame_with_control_ops(
6750 "aft",
6751 PROTOCOL_VERSION,
6752 7,
6753 Some(vec![
6754 "future.synthetic".to_string(),
6755 "route.bind".to_string(),
6756 ]),
6757 ),
6758 )
6759 .unwrap();
6760 let registration = registry.get_module("aft").unwrap().unwrap();
6761 assert_eq!(
6762 registration.control_ops,
6763 vec![
6764 "route.bind".to_string(),
6765 "route.status".to_string(),
6766 "future.synthetic".to_string(),
6767 ]
6768 );
6769 let frame =
6770 Frame::build(FrameType::Request, control_flags(), 0, 0, 78, Vec::new()).unwrap();
6771 assert!(handler
6772 .guard_module_control_op(&frame, "aft", "future.synthetic")
6773 .unwrap()
6774 .is_none());
6775 }
6776
6777 #[tokio::test]
6778 async fn health_probe_refuses_unadvertised_module_without_sending_frame() {
6779 let registry = Arc::new(Registry::default());
6780 let forwarding = Arc::new(ForwardingTable::default());
6781 let handler =
6782 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
6783 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(10));
6784 let responses = handler
6785 .handle_control_frame(
6786 &module_ctx,
6787 hello_frame_with_control_ops("aft", PROTOCOL_VERSION, 7, None),
6788 )
6789 .await
6790 .unwrap();
6791 assert_eq!(responses[0].header.ty, FrameType::HelloAck);
6792
6793 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(20));
6794 let responses = handler
6795 .handle_control_frame(&client_ctx, supervisor_health_probe_frame(77, "aft"))
6796 .await
6797 .unwrap();
6798 assert_eq!(responses.len(), 1);
6799 assert_eq!(responses[0].header.ty, FrameType::Error);
6800 assert_eq!(parse_error(&responses[0])["code"], "health_not_advertised");
6801 assert!(module_rx.try_recv().is_err());
6802 }
6803
6804 #[tokio::test]
6805 async fn health_probe_demuxes_while_route_bind_relay_is_in_flight() {
6806 let registry = Arc::new(Registry::default());
6807 let forwarding = Arc::new(ForwardingTable::default());
6808 let handler =
6809 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
6810 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(30));
6811 handler
6812 .handle_control_frame(
6813 &module_ctx,
6814 hello_frame_with_control_ops(
6815 "aft",
6816 PROTOCOL_VERSION,
6817 7,
6818 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
6819 ),
6820 )
6821 .await
6822 .unwrap();
6823
6824 let project_root = unique_project_root("demux");
6825 let (route_client_ctx, mut route_client_rx) = route_ctx(ConnectionId::new(31));
6826 let route_handler = handler.clone();
6827 let route_task = tokio::spawn(async move {
6828 route_handler
6829 .handle_control_frame(
6830 &route_client_ctx,
6831 route_open_frame(100, "aft", project_root),
6832 )
6833 .await
6834 .unwrap()
6835 });
6836 let bind_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
6837 .await
6838 .unwrap()
6839 .unwrap();
6840 assert!(matches!(
6841 serde_json::from_slice::<ModuleControlRequest>(&bind_frame.body).unwrap(),
6842 ModuleControlRequest::RouteBind { .. }
6843 ));
6844
6845 let (health_client_ctx, _health_client_rx) = route_ctx(ConnectionId::new(32));
6846 let health_handler = handler.clone();
6847 let health_task = tokio::spawn(async move {
6848 health_handler
6849 .handle_control_frame(
6850 &health_client_ctx,
6851 supervisor_health_probe_frame(101, "aft"),
6852 )
6853 .await
6854 .unwrap()
6855 });
6856 let health_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
6857 .await
6858 .unwrap()
6859 .unwrap();
6860 assert_eq!(
6861 serde_json::from_slice::<ModuleControlRequest>(&health_frame.body).unwrap(),
6862 ModuleControlRequest::HealthCheck {}
6863 );
6864
6865 handler
6866 .handle_control_frame(
6867 &module_ctx,
6868 health_response(health_frame.header.corr, HealthStatus::Degraded),
6869 )
6870 .await
6871 .unwrap();
6872 let health_response = health_task.await.unwrap();
6873 assert_eq!(health_response.len(), 1);
6874 match serde_json::from_slice::<ClientControlResponse>(&health_response[0].body).unwrap() {
6875 ClientControlResponse::SupervisorHealthProbe {
6876 module_id,
6877 status,
6878 detail,
6879 metrics,
6880 } => {
6881 assert_eq!(module_id, "aft");
6882 assert_eq!(status, HealthStatus::Degraded);
6883 assert_eq!(detail.as_deref(), Some("warming"));
6884 assert_eq!(metrics, Some(json!({"queue_depth": 3})));
6885 }
6886 other => panic!("unexpected health response: {other:?}"),
6887 }
6888
6889 handler
6890 .handle_control_frame(&module_ctx, route_bind_ack(bind_frame.header.corr))
6891 .await
6892 .unwrap();
6893 let route_response = route_task.await.unwrap();
6894 assert!(route_response.is_empty());
6895 let published = route_client_rx.recv().await.unwrap();
6896 assert!(matches!(
6897 serde_json::from_slice::<ClientControlResponse>(&published.body).unwrap(),
6898 ClientControlResponse::RouteOpen { .. }
6899 ));
6900 }
6901
6902 async fn relay_route_open(
6907 handler: &ControlHandler,
6908 client_connection: ConnectionId,
6909 client_egress: &FrameSink,
6910 module_rx: &mut mpsc::Receiver<crate::router::OutboundFrame>,
6911 corr: u64,
6912 module_id: &str,
6913 project_root_label: &str,
6914 ) -> (tokio::task::JoinHandle<Vec<Frame>>, Frame) {
6915 let ctx = RouteCtx {
6916 connection_id: client_connection,
6917 egress: client_egress.clone(),
6918 };
6919 let handler = handler.clone();
6920 let project_root = unique_project_root(project_root_label);
6921 let module_id = module_id.to_string();
6922 let dispatch = tracing::dispatcher::get_default(|dispatch| dispatch.clone());
6923 let task = tokio::spawn(async move {
6924 let _guard = tracing::dispatcher::set_default(&dispatch);
6925 handler
6926 .handle_control_frame(&ctx, route_open_frame(corr, &module_id, project_root))
6927 .await
6928 .unwrap()
6929 });
6930 let bind = tokio::time::timeout(Duration::from_secs(2), module_rx.recv())
6931 .await
6932 .expect("module receives the relayed route.bind")
6933 .expect("module egress is open");
6934 (task, bind.frame)
6935 }
6936
6937 fn route_bind_channel(frame: &Frame) -> (u16, u32) {
6938 match serde_json::from_slice::<ModuleControlRequest>(&frame.body).unwrap() {
6939 ModuleControlRequest::RouteBind {
6940 route_channel,
6941 epoch,
6942 ..
6943 } => (route_channel, epoch),
6944 other => panic!("expected a route.bind request, got {other:?}"),
6945 }
6946 }
6947
6948 fn published_route(frame: &Frame) -> (u16, u32) {
6949 match serde_json::from_slice::<ClientControlResponse>(&frame.body).unwrap() {
6950 ClientControlResponse::RouteOpen {
6951 route_channel,
6952 route_epoch,
6953 } => (route_channel, route_epoch),
6954 other => panic!("expected a route.open response, got {other:?}"),
6955 }
6956 }
6957
6958 #[tokio::test]
6978 async fn late_bind_ack_for_a_closing_client_keeps_the_module_connection_serving() {
6979 let registry = Arc::new(Registry::default());
6980 let forwarding = Arc::new(ForwardingTable::default());
6981 let handler =
6982 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
6983
6984 let module_connection = ConnectionId::new(30);
6985 let (module_ctx, mut module_rx) = route_ctx(module_connection);
6986 handler
6987 .handle_control_frame(&module_ctx, hello_frame("aft", PROTOCOL_VERSION, 7))
6988 .await
6989 .unwrap();
6990
6991 let dying_client = ConnectionId::new(31);
6992 let (dying_ctx, mut dying_rx) = route_ctx(dying_client);
6993
6994 let (first_task, first_bind) = relay_route_open(
6997 &handler,
6998 dying_client,
6999 &dying_ctx.egress,
7000 &mut module_rx,
7001 100,
7002 "aft",
7003 "closing-first",
7004 )
7005 .await;
7006 handler
7007 .handle_control_frame(&module_ctx, route_bind_ack(first_bind.header.corr))
7008 .await
7009 .unwrap();
7010 assert!(first_task.await.unwrap().is_empty());
7011 let (first_channel, first_epoch) = published_route(&dying_rx.recv().await.unwrap());
7012
7013 let (second_task, second_bind) = relay_route_open(
7015 &handler,
7016 dying_client,
7017 &dying_ctx.egress,
7018 &mut module_rx,
7019 101,
7020 "aft",
7021 "closing-second",
7022 )
7023 .await;
7024 let (abandoned_channel, abandoned_epoch) = route_bind_channel(&second_bind);
7025
7026 assert!(forwarding
7029 .escalate_client_delivery_failure(
7030 dying_client,
7031 first_channel,
7032 first_epoch,
7033 CloseReason::new(
7034 "module_to_client_delivery_failed",
7035 "client egress refused a module frame",
7036 ),
7037 crate::forwarding::UndeliveredFrame {
7038 module_id: None,
7039 sink: &dying_ctx.egress,
7040 },
7041 )
7042 .unwrap());
7043 assert!(!dying_ctx.egress.is_closed());
7044
7045 let ack = handler
7047 .handle_control_frame(&module_ctx, route_bind_ack(second_bind.header.corr))
7048 .await;
7049 let module_loop_error = ack.as_ref().err().map(ToString::to_string);
7050 if module_loop_error.is_some() {
7051 handler.cleanup_connection(module_connection).unwrap();
7055 }
7056 let post_ack_module_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
7060 .await
7061 .ok()
7062 .flatten();
7063
7064 assert!(
7066 registry
7067 .get_module_by_connection(module_connection)
7068 .unwrap()
7069 .is_some(),
7070 "one client's closing connection ended the shared module connection: \
7071 {module_loop_error:?}"
7072 );
7073 let cotenant = ConnectionId::new(32);
7075 let (cotenant_ctx, mut cotenant_rx) = route_ctx(cotenant);
7076 let (cotenant_task, cotenant_bind) = relay_route_open(
7077 &handler,
7078 cotenant,
7079 &cotenant_ctx.egress,
7080 &mut module_rx,
7081 102,
7082 "aft",
7083 "closing-cotenant",
7084 )
7085 .await;
7086 handler
7087 .handle_control_frame(&module_ctx, route_bind_ack(cotenant_bind.header.corr))
7088 .await
7089 .unwrap();
7090 assert!(cotenant_task.await.unwrap().is_empty());
7091 let (cotenant_channel, cotenant_epoch) =
7092 published_route(&cotenant_rx.recv().await.unwrap());
7093 assert!(matches!(
7094 forwarding
7095 .lookup_data_route(cotenant, cotenant_channel, cotenant_epoch)
7096 .unwrap(),
7097 DataRoute::Client(DataRouteState::Bound(_))
7098 ));
7099
7100 let goodbye = post_ack_module_frame
7103 .expect("module receives a GOODBYE for the abandoned route channel");
7104 assert_eq!(goodbye.header.ty, FrameType::Goodbye);
7105 assert_eq!(goodbye.header.channel, abandoned_channel);
7106 assert_eq!(goodbye.header.epoch, abandoned_epoch);
7107
7108 assert!(dying_rx.try_recv().is_err());
7112 let second_response = second_task.await.unwrap();
7113 assert_eq!(second_response.len(), 1);
7114 assert_eq!(
7115 parse_error(&second_response[0])["code"],
7116 "target_unavailable"
7117 );
7118 }
7119
7120 #[test]
7127 fn only_the_modules_own_closing_connection_ends_the_module_loop() {
7128 let handler = ControlHandler::default();
7129 let module_connection = ConnectionId::new(30);
7130 let client_connection = ConnectionId::new(31);
7131
7132 handler
7133 .refuse_to_end_module_connection_for_a_client(
7134 module_connection,
7135 77,
7136 ForwardingError::ConnectionClosing {
7137 connection_id: client_connection,
7138 },
7139 )
7140 .expect("a closing client must never end the module connection");
7141
7142 assert!(matches!(
7143 handler.refuse_to_end_module_connection_for_a_client(
7144 module_connection,
7145 78,
7146 ForwardingError::ConnectionClosing {
7147 connection_id: module_connection,
7148 },
7149 ),
7150 Err(RouterError::Forwarding(ForwardingError::ConnectionClosing {
7151 connection_id
7152 })) if connection_id == module_connection
7153 ));
7154 assert!(matches!(
7155 handler.refuse_to_end_module_connection_for_a_client(
7156 module_connection,
7157 79,
7158 ForwardingError::Poisoned,
7159 ),
7160 Err(RouterError::Forwarding(ForwardingError::Poisoned))
7161 ));
7162 assert!(matches!(
7163 handler.refuse_to_end_module_connection_for_a_client(
7164 module_connection,
7165 80,
7166 ForwardingError::StaleModuleEndpoint,
7167 ),
7168 Err(RouterError::Forwarding(
7169 ForwardingError::StaleModuleEndpoint
7170 ))
7171 ));
7172 }
7173
7174 #[tokio::test]
7187 async fn route_open_refuses_consumer_identity_that_fails_spawn_attestation() {
7188 let registry = Arc::new(Registry::default());
7189 let forwarding = Arc::new(ForwardingTable::default());
7190 let supervisor = SupervisorHandle::new();
7191 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
7192 let handler =
7193 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
7194 .with_supervisor(supervisor);
7195
7196 let (target_ctx, _target_rx) = route_ctx(ConnectionId::new(90));
7197 handler
7198 .handle_control_frame(&target_ctx, hello_frame("target", PROTOCOL_VERSION, 1))
7199 .await
7200 .unwrap();
7201
7202 let wrong_nonce = handler
7206 .handle_control_frame(
7207 &route_ctx(ConnectionId::new(91)).0,
7208 route_open_frame_with_admission_facts(
7209 20,
7210 "target",
7211 unique_project_root("admission-facts"),
7212 Some(subc_control::ConsumerIdentity {
7213 module_id: "fed".to_string(),
7214 launch_nonce: "not-the-real-nonce".to_string(),
7215 }),
7216 None,
7217 ),
7218 )
7219 .await
7220 .unwrap();
7221 assert_eq!(
7222 parse_error(&wrong_nonce[0])["code"],
7223 "bad_consumer_identity",
7224 "a mismatched launch nonce must be refused, not stamped Reserved"
7225 );
7226
7227 let never_spawned = handler
7231 .handle_control_frame(
7232 &route_ctx(ConnectionId::new(92)).0,
7233 route_open_frame_with_admission_facts(
7234 21,
7235 "target",
7236 unique_project_root("admission-facts"),
7237 Some(subc_control::ConsumerIdentity {
7238 module_id: "never-spawned".to_string(),
7239 launch_nonce: "any-nonce".to_string(),
7240 }),
7241 None,
7242 ),
7243 )
7244 .await
7245 .unwrap();
7246 assert_eq!(
7247 parse_error(&never_spawned[0])["code"],
7248 "bad_consumer_identity",
7249 "an unspawned module_id must be refused rather than accepted for lack of a record"
7250 );
7251 }
7252
7253 #[tokio::test]
7276 async fn route_open_stamps_reserved_for_a_correctly_attested_consumer() {
7277 let registry = Arc::new(Registry::default());
7278 let forwarding = Arc::new(ForwardingTable::default());
7279 let supervisor = SupervisorHandle::new();
7280 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
7281 let handler =
7282 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
7283 .with_supervisor(supervisor);
7284
7285 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(95));
7286 handler
7287 .handle_control_frame(&target_ctx, hello_frame("target", PROTOCOL_VERSION, 1))
7288 .await
7289 .unwrap();
7290
7291 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(96));
7292 let route_handler = handler.clone();
7293 let route_task = tokio::spawn(async move {
7294 route_handler
7295 .handle_control_frame(
7296 &client_ctx,
7297 route_open_frame_with_admission_facts(
7298 30,
7299 "target",
7300 unique_project_root("admission-facts"),
7301 Some(subc_control::ConsumerIdentity {
7302 module_id: "fed".to_string(),
7303 launch_nonce: "fed-nonce".to_string(),
7304 }),
7305 None,
7306 ),
7307 )
7308 .await
7309 .unwrap()
7310 });
7311
7312 let bind_frame = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
7319 .await
7320 .expect("no route.bind within 5s: the consumer-identity guard refused a correctly attested consumer")
7321 .expect("module control channel closed before route.bind");
7322 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
7323 let ModuleControlRequest::RouteBind { principal, .. } = bind else {
7324 panic!("expected route.bind")
7325 };
7326 assert_eq!(
7327 principal,
7328 Some(Principal::Reserved {
7329 module_id: "fed".to_string()
7330 }),
7331 "a correctly attested consumer must be stamped Reserved for its own id"
7332 );
7333
7334 handler
7335 .handle_control_frame(&target_ctx, route_bind_ack(bind_frame.header.corr))
7336 .await
7337 .unwrap();
7338 assert!(route_task.await.unwrap().is_empty());
7339 assert!(
7340 matches!(
7341 serde_json::from_slice::<ClientControlResponse>(
7342 &client_rx.recv().await.unwrap().body
7343 )
7344 .unwrap(),
7345 ClientControlResponse::RouteOpen { .. }
7346 ),
7347 "the route must actually open, not merely avoid an error"
7348 );
7349 }
7350
7351 #[tokio::test(start_paused = true)]
7352 async fn supervisor_routes_serializes_live_draining_bindings_from_the_real_handler() {
7353 let registry = Arc::new(Registry::default());
7354 let forwarding = Arc::new(ForwardingTable::default());
7355 let supervisor = SupervisorHandle::new();
7356 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
7357 let handler =
7358 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
7359 .with_supervisor(supervisor);
7360
7361 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(101));
7362 handler
7363 .handle_control_frame(&target_ctx, hello_frame("target", PROTOCOL_VERSION, 1))
7364 .await
7365 .unwrap();
7366
7367 let (direct_ctx, mut direct_rx) = route_ctx(ConnectionId::new(102));
7368 let direct_handler = handler.clone();
7369 let direct_open = tokio::spawn(async move {
7370 direct_handler
7371 .handle_control_frame(
7372 &direct_ctx,
7373 route_open_frame(2, "target", unique_project_root("route-census-direct")),
7374 )
7375 .await
7376 .unwrap()
7377 });
7378 let direct_bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
7379 .await
7380 .expect("no direct route.bind within 5s")
7381 .expect("target control channel closed before direct route.bind");
7382 handler
7383 .handle_control_frame(&target_ctx, route_bind_ack(direct_bind.header.corr))
7384 .await
7385 .unwrap();
7386 assert!(direct_open.await.unwrap().is_empty());
7387 let _ = direct_rx.recv().await.unwrap();
7388
7389 let (reserved_ctx, mut reserved_rx) = route_ctx(ConnectionId::new(103));
7390 let reserved_handler = handler.clone();
7391 let reserved_open = tokio::spawn(async move {
7392 reserved_handler
7393 .handle_control_frame(
7394 &reserved_ctx,
7395 route_open_frame_with_admission_facts(
7396 3,
7397 "target",
7398 unique_project_root("admission-facts"),
7399 Some(ConsumerIdentity {
7400 module_id: "fed".to_string(),
7401 launch_nonce: "fed-nonce".to_string(),
7402 }),
7403 None,
7404 ),
7405 )
7406 .await
7407 .unwrap()
7408 });
7409 let reserved_bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
7410 .await
7411 .expect("no reserved route.bind within 5s")
7412 .expect("target control channel closed before reserved route.bind");
7413 handler
7414 .handle_control_frame(&target_ctx, route_bind_ack(reserved_bind.header.corr))
7415 .await
7416 .unwrap();
7417 assert!(reserved_open.await.unwrap().is_empty());
7418 let _ = reserved_rx.recv().await.unwrap();
7419
7420 forwarding
7421 .begin_module_drain("target", subc_control::RouteCloseReason::Reload)
7422 .unwrap();
7423 let (census_ctx, _census_rx) = route_ctx(ConnectionId::new(104));
7424 let census_body = serde_json::to_vec(&ClientControlRequest::SupervisorRoutes {
7425 module_id: Some("target".to_string()),
7426 })
7427 .unwrap();
7428 let census_frame =
7429 Frame::build(FrameType::Request, control_flags(), 0, 0, 4, census_body).unwrap();
7430 let response = handler
7431 .handle_control_frame(&census_ctx, census_frame)
7432 .await
7433 .unwrap()
7434 .pop()
7435 .unwrap();
7436 let actual: Value = serde_json::from_slice(&response.body).unwrap();
7437 let decoded: ClientControlResponse = serde_json::from_value(actual.clone()).unwrap();
7438 assert!(matches!(
7439 decoded,
7440 ClientControlResponse::SupervisorRoutes { .. }
7441 ));
7442 let routes = actual["modules"][0]["routes"].as_array().unwrap();
7443 assert_eq!(routes.len(), 2);
7444 assert!(routes.iter().all(|route| route["draining"] == true));
7445 assert!(
7448 routes.iter().all(|route| route["drain_reason"] == "reload"),
7449 "draining routes must name the drain's reason: {routes:?}"
7450 );
7451 assert!(routes.iter().any(|route| {
7452 route["consumer"] == serde_json::json!({"kind": "direct", "connection_id": 102})
7453 }));
7454 assert!(routes.iter().any(|route| {
7455 route["consumer"] == serde_json::json!({"kind": "reserved", "module_id": "fed"})
7456 }));
7457
7458 let golden_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7459 .join("../subc-control/tests/golden/client_control_response_supervisor_routes.json");
7460 if std::env::var_os("UPDATE_GOLDEN").is_some() {
7461 std::fs::write(
7462 &golden_path,
7463 format!("{}\n", serde_json::to_string_pretty(&actual).unwrap()),
7464 )
7465 .unwrap();
7466 }
7467 let expected: Value =
7468 serde_json::from_str(&std::fs::read_to_string(golden_path).unwrap()).unwrap();
7469 assert_eq!(actual, expected);
7470 }
7471
7472 async fn query_live_roots(
7473 handler: &ControlHandler,
7474 module_ctx: &RouteCtx,
7475 ) -> ModuleControlResponseToModule {
7476 let body = serde_json::to_vec(&ModuleControlRequestFromModule::LiveRoots {}).unwrap();
7477 let frame = Frame::build(FrameType::Request, control_flags(), 0, 0, 900, body).unwrap();
7478 let response = handler
7479 .handle_control_frame(module_ctx, frame)
7480 .await
7481 .unwrap()
7482 .pop()
7483 .unwrap();
7484 serde_json::from_slice(&response.body).unwrap()
7485 }
7486
7487 #[tokio::test(start_paused = true)]
7488 async fn supervisor_live_roots_root_known_arm_counts_bound_and_pending_from_real_handler() {
7489 let registry = Arc::new(Registry::default());
7490 let forwarding = Arc::new(ForwardingTable::default());
7491 let handler = ControlHandler::with_forwarding(registry, forwarding);
7492 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(301));
7493 handler
7494 .handle_control_frame(&target_ctx, hello_frame("target", PROTOCOL_VERSION, 1))
7495 .await
7496 .unwrap();
7497 let root = unique_project_root("live-roots-known");
7498 let path = ProjectRootId::from_path_allowing_missing(root.path())
7499 .unwrap()
7500 .as_path()
7501 .to_path_buf();
7502 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(302));
7503 let open_handler = handler.clone();
7504 let opened = tokio::spawn(async move {
7505 open_handler
7506 .handle_control_frame(&client_ctx, route_open_frame(2, "target", root))
7507 .await
7508 .unwrap()
7509 });
7510 let bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
7511 .await
7512 .unwrap()
7513 .unwrap();
7514 handler
7515 .handle_control_frame(&target_ctx, route_bind_ack(bind.header.corr))
7516 .await
7517 .unwrap();
7518 assert!(opened.await.unwrap().is_empty());
7519 let _ = client_rx.recv().await.unwrap();
7520
7521 let root = unique_project_root("live-roots-pending");
7522 let pending_path = ProjectRootId::from_path_allowing_missing(root.path())
7523 .unwrap()
7524 .as_path()
7525 .to_path_buf();
7526 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(303));
7527 let open_handler = handler.clone();
7528 let pending = tokio::spawn(async move {
7529 open_handler
7530 .handle_control_frame(&client_ctx, route_open_frame(3, "target", root))
7531 .await
7532 .unwrap()
7533 });
7534 let pending_bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
7535 .await
7536 .unwrap()
7537 .unwrap();
7538 let actual = query_live_roots(&handler, &target_ctx).await;
7539 let ModuleControlResponseToModule::LiveRoots {
7540 roots,
7541 unknown_root_bindings,
7542 total_bindings,
7543 } = actual
7544 else {
7545 panic!("expected live roots")
7546 };
7547 assert_eq!(total_bindings, 2, "root-known arm must count live routes");
7548 assert_eq!(unknown_root_bindings, 0);
7549 assert_eq!(
7550 roots.len(),
7551 2,
7552 "root-known arm must retain each canonical root"
7553 );
7554 assert_eq!(
7555 total_bindings,
7556 roots.iter().map(|r| r.bound + r.pending).sum::<u64>() + unknown_root_bindings
7557 );
7558 let counts = roots
7559 .iter()
7560 .map(|root| (root.project_root.clone(), root.bound, root.pending))
7561 .collect::<Vec<_>>();
7562 let mut expected = vec![(path, 1, 0), (pending_path, 0, 1)];
7563 expected.sort_by(|a, b| a.0.cmp(&b.0));
7564 assert_eq!(
7565 counts, expected,
7566 "roots must sort by path and count pending separately"
7567 );
7568 handler
7569 .handle_control_frame(&target_ctx, route_bind_ack(pending_bind.header.corr))
7570 .await
7571 .unwrap();
7572 assert!(pending.await.unwrap().is_empty());
7573 }
7574
7575 #[tokio::test(start_paused = true)]
7576 async fn supervisor_live_roots_unknown_root_arm_is_not_no_bindings() {
7577 let forwarding = Arc::new(ForwardingTable::default());
7578 let handler =
7579 ControlHandler::with_forwarding(Arc::new(Registry::default()), Arc::clone(&forwarding));
7580 let (target_ctx, _target_rx) = route_ctx(ConnectionId::new(311));
7581 handler
7582 .handle_control_frame(&target_ctx, hello_frame("target", PROTOCOL_VERSION, 1))
7583 .await
7584 .unwrap();
7585 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(312));
7586 let pending = forwarding
7587 .begin_route_bind_relay_for_test(
7588 client_ctx.connection_id,
7589 client_ctx.egress.clone(),
7590 2,
7591 "target",
7592 )
7593 .unwrap();
7594 forwarding
7595 .complete_pending_relay(
7596 target_ctx.connection_id,
7597 pending.corr,
7598 RouteBindRelayOutcome::Accepted,
7599 )
7600 .unwrap();
7601 let actual = query_live_roots(&handler, &target_ctx).await;
7602 let ModuleControlResponseToModule::LiveRoots {
7603 roots,
7604 unknown_root_bindings,
7605 total_bindings,
7606 } = actual
7607 else {
7608 panic!("expected live roots")
7609 };
7610 assert!(roots.is_empty(), "unknown-root arm must not invent a root");
7611 assert_eq!(
7612 unknown_root_bindings, 1,
7613 "unknown-root arm must not read as no bindings"
7614 );
7615 assert_eq!(total_bindings, 1, "unknown-root arm has a live binding");
7616 assert_eq!(
7617 total_bindings,
7618 roots.iter().map(|r| r.bound + r.pending).sum::<u64>() + unknown_root_bindings
7619 );
7620 }
7621
7622 #[tokio::test(start_paused = true)]
7623 async fn supervisor_live_roots_cross_module_scope_uses_requesting_connection() {
7624 let handler = ControlHandler::with_forwarding(
7625 Arc::new(Registry::default()),
7626 Arc::new(ForwardingTable::default()),
7627 );
7628 let (first_ctx, _first_rx) = route_ctx(ConnectionId::new(315));
7629 let (second_ctx, mut second_rx) = route_ctx(ConnectionId::new(316));
7630 handler
7631 .handle_control_frame(&first_ctx, hello_frame("first", PROTOCOL_VERSION, 1))
7632 .await
7633 .unwrap();
7634 handler
7635 .handle_control_frame(&second_ctx, hello_frame("second", PROTOCOL_VERSION, 2))
7636 .await
7637 .unwrap();
7638 let root = unique_project_root("second-only");
7639 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(317));
7640 let cloned = handler.clone();
7641 let open = tokio::spawn(async move {
7642 cloned
7643 .handle_control_frame(&client_ctx, route_open_frame(3, "second", root))
7644 .await
7645 .unwrap()
7646 });
7647 let bind = tokio::time::timeout(Duration::from_secs(5), second_rx.recv())
7648 .await
7649 .unwrap()
7650 .unwrap();
7651 let first = query_live_roots(&handler, &first_ctx).await;
7652 let second = query_live_roots(&handler, &second_ctx).await;
7653 assert!(
7654 matches!(
7655 first,
7656 ModuleControlResponseToModule::LiveRoots {
7657 total_bindings: 0,
7658 ..
7659 }
7660 ),
7661 "cross-module scope must not expose another module's roots"
7662 );
7663 assert!(
7664 matches!(
7665 second,
7666 ModuleControlResponseToModule::LiveRoots {
7667 total_bindings: 1,
7668 ..
7669 }
7670 ),
7671 "second module must see its pending route"
7672 );
7673 handler
7674 .handle_control_frame(&second_ctx, route_bind_ack(bind.header.corr))
7675 .await
7676 .unwrap();
7677 assert!(open.await.unwrap().is_empty());
7678 }
7679
7680 #[tokio::test(start_paused = true)]
7681 async fn supervisor_live_roots_no_bindings_arm_is_empty() {
7682 let handler = ControlHandler::with_forwarding(
7683 Arc::new(Registry::default()),
7684 Arc::new(ForwardingTable::default()),
7685 );
7686 let (target_ctx, _target_rx) = route_ctx(ConnectionId::new(321));
7687 handler
7688 .handle_control_frame(&target_ctx, hello_frame("target", PROTOCOL_VERSION, 1))
7689 .await
7690 .unwrap();
7691 let actual = query_live_roots(&handler, &target_ctx).await;
7692 let ModuleControlResponseToModule::LiveRoots {
7693 roots,
7694 unknown_root_bindings,
7695 total_bindings,
7696 } = actual
7697 else {
7698 panic!("expected live roots")
7699 };
7700 assert!(roots.is_empty());
7701 assert_eq!(unknown_root_bindings, 0);
7702 assert_eq!(total_bindings, 0);
7703 assert_eq!(
7704 total_bindings,
7705 roots.iter().map(|r| r.bound + r.pending).sum::<u64>() + unknown_root_bindings
7706 );
7707 }
7708
7709 fn fed_admission_facts_vectors() -> Vec<(String, Value)> {
7716 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7717 .join("tests/fixtures/fed/admission-facts-emit.jsonl");
7718 let text = std::fs::read_to_string(&path)
7719 .unwrap_or_else(|err| panic!("vendored fed corpus unreadable at {path:?}: {err}"));
7720 let vectors: Vec<(String, Value)> = text
7721 .lines()
7722 .filter(|line| !line.trim().is_empty())
7723 .map(|line| {
7724 let entry: Value = serde_json::from_str(line).expect("corpus line must be JSON");
7725 let id = entry["corpus_id"]
7726 .as_str()
7727 .expect("every vector carries a corpus_id")
7728 .to_string();
7729 (id, entry["package"].clone())
7730 })
7731 .collect();
7732 assert_eq!(
7736 vectors.len(),
7737 3,
7738 "vendored fed corpus changed size; re-sync from subc-federation"
7739 );
7740
7741 const COMMONLY_MODELLED: [&str; 3] = ["schema", "verified_class", "org"];
7756 let richest = vectors
7757 .iter()
7758 .filter_map(|(_, package)| package.as_object())
7759 .map(|object| {
7760 object
7761 .keys()
7762 .filter(|key| !COMMONLY_MODELLED.contains(&key.as_str()))
7763 .count()
7764 })
7765 .max()
7766 .unwrap_or(0);
7767 assert!(
7768 richest >= 2,
7769 "vendored corpus no longer carries a package with unmodelled fields, \
7770 so the relay test can no longer distinguish a verbatim relay from a lossy one"
7771 );
7772
7773 vectors
7774 }
7775
7776 #[tokio::test]
7784 async fn admission_facts_relay_carries_vendored_packages_verbatim() {
7785 for (corpus_id, package) in fed_admission_facts_vectors() {
7786 let registry = Arc::new(Registry::default());
7787 let forwarding = Arc::new(ForwardingTable::default());
7788 let supervisor = SupervisorHandle::new();
7789 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
7790 let handler =
7791 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
7792 .with_supervisor(supervisor)
7793 .with_admission_facts_config(
7794 Some("fed".to_string()),
7795 Some(vec!["target".to_string()]),
7796 );
7797
7798 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(90));
7799 handler
7800 .handle_control_frame(&target_ctx, hello_frame("target", PROTOCOL_VERSION, 1))
7801 .await
7802 .unwrap();
7803
7804 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(91));
7805 let route_handler = handler.clone();
7806 let expected = package.clone();
7807 let route_task = tokio::spawn(async move {
7808 route_handler
7809 .handle_control_frame(
7810 &client_ctx,
7811 route_open_frame_with_admission_facts(
7812 20,
7813 "target",
7814 unique_project_root("admission-facts"),
7815 Some(subc_control::ConsumerIdentity {
7816 module_id: "fed".to_string(),
7817 launch_nonce: "fed-nonce".to_string(),
7818 }),
7819 Some(package),
7820 ),
7821 )
7822 .await
7823 .unwrap()
7824 });
7825
7826 let bind_frame = target_rx.recv().await.unwrap();
7827 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
7828 let ModuleControlRequest::RouteBind {
7829 admission_facts, ..
7830 } = bind
7831 else {
7832 panic!("{corpus_id}: expected route.bind")
7833 };
7834 assert_eq!(
7835 admission_facts,
7836 Some(expected),
7837 "{corpus_id}: relay must not add, drop or reshape any field"
7838 );
7839
7840 handler
7841 .handle_control_frame(&target_ctx, route_bind_ack(bind_frame.header.corr))
7842 .await
7843 .unwrap();
7844 route_task.await.unwrap();
7845 }
7846 }
7847
7848 #[tokio::test]
7849 async fn admission_facts_gate_checks_carrier_target_and_precedence() {
7850 let registry = Arc::new(Registry::default());
7851 let forwarding = Arc::new(ForwardingTable::default());
7852 let supervisor = SupervisorHandle::new();
7853 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
7854 supervisor.set_spawn_nonce("other", "other-nonce".to_string());
7855 let handler =
7856 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
7857 .with_supervisor(supervisor)
7858 .with_admission_facts_config(
7859 Some("fed".to_string()),
7860 Some(vec!["target".to_string()]),
7861 );
7862
7863 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(70));
7864 handler
7865 .handle_control_frame(&target_ctx, hello_frame("target", PROTOCOL_VERSION, 1))
7866 .await
7867 .unwrap();
7868 let (other_ctx, _other_rx) = route_ctx(ConnectionId::new(71));
7869 handler
7870 .handle_control_frame(&other_ctx, hello_frame("other", PROTOCOL_VERSION, 2))
7871 .await
7872 .unwrap();
7873
7874 let facts = json!({"schema": 1, "verified_class": "member", "org": "01H"});
7875 let expected_facts = facts.clone();
7876 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(72));
7877 let route_handler = handler.clone();
7878 let route_task = tokio::spawn(async move {
7879 route_handler
7880 .handle_control_frame(
7881 &client_ctx,
7882 route_open_frame_with_admission_facts(
7883 10,
7884 "target",
7885 unique_project_root("admission-facts"),
7886 Some(subc_control::ConsumerIdentity {
7887 module_id: "fed".to_string(),
7888 launch_nonce: "fed-nonce".to_string(),
7889 }),
7890 Some(facts.clone()),
7891 ),
7892 )
7893 .await
7894 .unwrap()
7895 });
7896 let bind_frame = target_rx.recv().await.unwrap();
7897 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
7898 let ModuleControlRequest::RouteBind {
7899 admission_facts, ..
7900 } = bind
7901 else {
7902 panic!("expected route.bind")
7903 };
7904 assert_eq!(admission_facts, Some(expected_facts));
7905 handler
7906 .handle_control_frame(&target_ctx, route_bind_ack(bind_frame.header.corr))
7907 .await
7908 .unwrap();
7909 assert!(route_task.await.unwrap().is_empty());
7910 assert!(matches!(
7911 serde_json::from_slice::<ClientControlResponse>(&client_rx.recv().await.unwrap().body)
7912 .unwrap(),
7913 ClientControlResponse::RouteOpen { .. }
7914 ));
7915
7916 let direct = handler
7917 .handle_control_frame(
7918 &route_ctx(ConnectionId::new(73)).0,
7919 route_open_frame_with_admission_facts(
7920 11,
7921 "target",
7922 unique_project_root("admission-facts"),
7923 None,
7924 Some(json!({"x": 1})),
7925 ),
7926 )
7927 .await
7928 .unwrap();
7929 assert_eq!(
7930 parse_error(&direct[0])["code"],
7931 "admission_facts_not_permitted"
7932 );
7933
7934 let different_reserved = handler
7935 .handle_control_frame(
7936 &route_ctx(ConnectionId::new(77)).0,
7937 route_open_frame_with_admission_facts(
7938 15,
7939 "target",
7940 unique_project_root("admission-facts"),
7941 Some(subc_control::ConsumerIdentity {
7942 module_id: "other".to_string(),
7943 launch_nonce: "other-nonce".to_string(),
7944 }),
7945 Some(json!({"x": 1})),
7946 ),
7947 )
7948 .await
7949 .unwrap();
7950 assert_eq!(
7951 parse_error(&different_reserved[0])["code"],
7952 "admission_facts_not_permitted"
7953 );
7954
7955 let other_target = handler
7956 .handle_control_frame(
7957 &route_ctx(ConnectionId::new(74)).0,
7958 route_open_frame_with_admission_facts(
7959 12,
7960 "other",
7961 unique_project_root("admission-facts"),
7962 Some(subc_control::ConsumerIdentity {
7963 module_id: "fed".to_string(),
7964 launch_nonce: "fed-nonce".to_string(),
7965 }),
7966 Some(json!({"x": 1})),
7967 ),
7968 )
7969 .await
7970 .unwrap();
7971 assert_eq!(
7972 parse_error(&other_target[0])["code"],
7973 "admission_facts_target_not_allowed"
7974 );
7975
7976 let nonexistent = handler
7977 .handle_control_frame(
7978 &route_ctx(ConnectionId::new(75)).0,
7979 route_open_frame_with_admission_facts(
7980 13,
7981 "missing",
7982 unique_project_root("admission-facts"),
7983 None,
7984 Some(json!({"x": 1})),
7985 ),
7986 )
7987 .await
7988 .unwrap();
7989 assert_eq!(parse_error(&nonexistent[0])["code"], "unknown_module");
7990
7991 let described = handler
7992 .handle_control_frame(
7993 &route_ctx(ConnectionId::new(76)).0,
7994 Frame::build(
7995 FrameType::Request,
7996 control_flags(),
7997 0,
7998 0,
7999 14,
8000 serde_json::to_vec(&ClientControlRequest::ServerDescribe {}).unwrap(),
8001 )
8002 .unwrap(),
8003 )
8004 .await
8005 .unwrap();
8006 let ClientControlResponse::ServerDescribe { capabilities, .. } =
8007 serde_json::from_slice(&described[0].body).unwrap()
8008 else {
8009 panic!("expected server.describe response")
8010 };
8011 assert!(capabilities
8012 .iter()
8013 .any(|cap| cap == "admission_facts_relay_v1"));
8014 }
8015
8016 #[tokio::test]
8017 async fn admission_facts_without_configured_carrier_are_rejected() {
8018 let registry = Arc::new(Registry::default());
8019 let forwarding = Arc::new(ForwardingTable::default());
8020 let handler = ControlHandler::with_forwarding(registry, forwarding);
8021 let (target_ctx, _) = route_ctx(ConnectionId::new(78));
8022 handler
8023 .handle_control_frame(&target_ctx, hello_frame("target", PROTOCOL_VERSION, 1))
8024 .await
8025 .unwrap();
8026
8027 let responses = handler
8028 .handle_control_frame(
8029 &route_ctx(ConnectionId::new(79)).0,
8030 route_open_frame_with_admission_facts(
8031 16,
8032 "target",
8033 unique_project_root("admission-facts"),
8034 None,
8035 Some(json!({"x": 1})),
8036 ),
8037 )
8038 .await
8039 .unwrap();
8040 assert_eq!(
8041 parse_error(&responses[0])["code"],
8042 "admission_facts_not_permitted"
8043 );
8044 }
8045
8046 #[tokio::test]
8047 async fn route_open_relays_consumer_capabilities_verbatim() {
8048 let registry = Arc::new(Registry::default());
8049 let forwarding = Arc::new(ForwardingTable::default());
8050 let handler =
8051 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
8052 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(37));
8053 handler
8054 .handle_control_frame(&module_ctx, hello_frame("aft", PROTOCOL_VERSION, 7))
8055 .await
8056 .unwrap();
8057
8058 let expected = vec!["elicitation".to_string(), "roots".to_string()];
8059 let expected_for_request = expected.clone();
8060 let project_root = unique_project_root("consumer-capabilities-present");
8061 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(38));
8062 let route_handler = handler.clone();
8063 let route_task = tokio::spawn(async move {
8064 route_handler
8065 .handle_control_frame(
8066 &client_ctx,
8067 route_open_frame_with_consumer_capabilities(
8068 401,
8069 "aft",
8070 project_root,
8071 Some(expected_for_request),
8072 ),
8073 )
8074 .await
8075 .unwrap()
8076 });
8077 let bind_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
8078 .await
8079 .unwrap()
8080 .unwrap();
8081 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
8082 let ModuleControlRequest::RouteBind {
8083 consumer_capabilities,
8084 ..
8085 } = bind
8086 else {
8087 panic!("expected route.bind request, got {bind:?}");
8088 };
8089 assert_eq!(consumer_capabilities, Some(expected.clone()));
8090
8091 handler
8092 .handle_control_frame(&module_ctx, route_bind_ack(bind_frame.header.corr))
8093 .await
8094 .unwrap();
8095 let route_response = route_task.await.unwrap();
8096 assert!(route_response.is_empty());
8097 let published = client_rx.recv().await.unwrap();
8098 assert!(matches!(
8099 serde_json::from_slice::<ClientControlResponse>(&published.body).unwrap(),
8100 ClientControlResponse::RouteOpen { .. }
8101 ));
8102 }
8103
8104 #[tokio::test]
8105 async fn route_open_without_consumer_capabilities_relays_none() {
8106 let registry = Arc::new(Registry::default());
8107 let forwarding = Arc::new(ForwardingTable::default());
8108 let handler =
8109 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
8110 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(39));
8111 handler
8112 .handle_control_frame(&module_ctx, hello_frame("aft", PROTOCOL_VERSION, 7))
8113 .await
8114 .unwrap();
8115
8116 let project_root = unique_project_root("consumer-capabilities-absent");
8117 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(40));
8118 let route_handler = handler.clone();
8119 let route_task = tokio::spawn(async move {
8120 route_handler
8121 .handle_control_frame(&client_ctx, route_open_frame(402, "aft", project_root))
8122 .await
8123 .unwrap()
8124 });
8125 let bind_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
8126 .await
8127 .unwrap()
8128 .unwrap();
8129 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
8130 let ModuleControlRequest::RouteBind {
8131 consumer_capabilities,
8132 ..
8133 } = bind
8134 else {
8135 panic!("expected route.bind request, got {bind:?}");
8136 };
8137 assert_eq!(consumer_capabilities, None);
8138
8139 handler
8140 .handle_control_frame(&module_ctx, route_bind_ack(bind_frame.header.corr))
8141 .await
8142 .unwrap();
8143 let route_response = route_task.await.unwrap();
8144 assert!(route_response.is_empty());
8145 let published = client_rx.recv().await.unwrap();
8146 assert!(matches!(
8147 serde_json::from_slice::<ClientControlResponse>(&published.body).unwrap(),
8148 ClientControlResponse::RouteOpen { .. }
8149 ));
8150 }
8151
8152 #[tokio::test]
8153 async fn supervision_only_module_health_probe_does_not_enable_route_open_and_cleans_up() {
8154 let registry = Arc::new(Registry::default());
8155 let forwarding = Arc::new(ForwardingTable::default());
8156 let handler =
8157 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
8158 .with_health_probe_timeout(Duration::from_secs(5));
8159 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(35));
8160 let responses = handler
8161 .handle_control_frame(
8162 &module_ctx,
8163 non_routable_hello_frame_with_control_ops(
8164 "mcp",
8165 300,
8166 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
8167 ),
8168 )
8169 .await
8170 .unwrap();
8171 assert_eq!(responses[0].header.ty, FrameType::HelloAck);
8172 assert!(registry
8173 .get_module("mcp")
8174 .unwrap()
8175 .unwrap()
8176 .manifest
8177 .provides
8178 .is_empty());
8179
8180 let (route_client_ctx, _route_client_rx) = route_ctx(ConnectionId::new(36));
8181 let route_response = handler
8182 .handle_control_frame(
8183 &route_client_ctx,
8184 route_open_frame(301, "mcp", unique_project_root("non-routable-mcp")),
8185 )
8186 .await
8187 .unwrap();
8188 assert_eq!(route_response[0].header.ty, FrameType::Error);
8189 assert_eq!(
8190 parse_error(&route_response[0])["code"],
8191 "target_unavailable"
8192 );
8193 assert!(parse_error(&route_response[0])["message"]
8194 .as_str()
8195 .unwrap()
8196 .contains("does not provide the requested target"));
8197 assert!(module_rx.try_recv().is_err());
8198
8199 let (health_client_ctx, _health_client_rx) = route_ctx(ConnectionId::new(37));
8200 let health_handler = handler.clone();
8201 let health_task = tokio::spawn(async move {
8202 health_handler
8203 .handle_control_frame(
8204 &health_client_ctx,
8205 supervisor_health_probe_frame(302, "mcp"),
8206 )
8207 .await
8208 .unwrap()
8209 });
8210 let health_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
8211 .await
8212 .unwrap()
8213 .unwrap();
8214 assert_eq!(
8215 serde_json::from_slice::<ModuleControlRequest>(&health_frame.body).unwrap(),
8216 ModuleControlRequest::HealthCheck {}
8217 );
8218 handler
8219 .handle_control_frame(
8220 &module_ctx,
8221 health_response(health_frame.header.corr, HealthStatus::Ok),
8222 )
8223 .await
8224 .unwrap();
8225 let health_response = health_task.await.unwrap();
8226 assert_eq!(health_response[0].header.ty, FrameType::Response);
8227 match serde_json::from_slice::<ClientControlResponse>(&health_response[0].body).unwrap() {
8228 ClientControlResponse::SupervisorHealthProbe {
8229 module_id, status, ..
8230 } => {
8231 assert_eq!(module_id, "mcp");
8232 assert_eq!(status, HealthStatus::Ok);
8233 }
8234 other => panic!("unexpected health response: {other:?}"),
8235 }
8236
8237 forwarding
8242 .cleanup_connection(module_ctx.connection_id)
8243 .unwrap();
8244 let (cleanup_probe_ctx, _cleanup_probe_rx) = route_ctx(ConnectionId::new(38));
8245 let cleanup_response = tokio::time::timeout(
8246 Duration::from_millis(200),
8247 handler.handle_control_frame(
8248 &cleanup_probe_ctx,
8249 supervisor_health_probe_frame(303, "mcp"),
8250 ),
8251 )
8252 .await
8253 .expect("probe should fail immediately when the control lane is gone")
8254 .unwrap();
8255 assert_eq!(cleanup_response[0].header.ty, FrameType::Error);
8256 assert_eq!(
8257 parse_error(&cleanup_response[0])["code"],
8258 "target_unavailable"
8259 );
8260 assert!(parse_error(&cleanup_response[0])["message"]
8261 .as_str()
8262 .unwrap()
8263 .contains("no module connection"));
8264
8265 handler
8266 .cleanup_connection(module_ctx.connection_id)
8267 .unwrap();
8268 }
8269
8270 #[tokio::test]
8271 async fn route_open_classifies_unregistered_running_supervised_module_as_warming() {
8272 let registry = Arc::new(Registry::default());
8273 let supervisor_handle = SupervisorHandle::new();
8274 let supervisor =
8275 Supervisor::new(Arc::clone(®istry), RestartPolicy::new(0, Duration::ZERO))
8276 .with_handle(supervisor_handle.clone())
8277 .with_connection_file_path(
8278 std::env::temp_dir()
8279 .join(format!("subc-route-open-warming-{}", std::process::id())),
8280 );
8281 let module = supervisor
8282 .supervise_configured(
8283 ModuleSpec {
8284 module_id: "warming".to_string(),
8285 program: fake_aft_stub_path(),
8286 args: Vec::new(),
8287 env: Vec::new(),
8288 reserved: false,
8289 reserved_prefixes: Vec::new(),
8290 protocol: ModuleProtocol::Subc,
8291 overlap: Default::default(),
8292 },
8293 true,
8294 )
8295 .unwrap();
8296 assert_eq!(module.state().unwrap(), ModuleState::Running);
8297
8298 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
8299 let (ctx, _rx) = route_ctx(ConnectionId::new(39));
8300 let response = handler
8301 .handle_control_frame(
8302 &ctx,
8303 route_open_frame(304, "warming", unique_project_root("warming")),
8304 )
8305 .await
8306 .unwrap();
8307 module.stop().await.unwrap();
8308
8309 assert_eq!(response[0].header.ty, FrameType::Error);
8310 let error = parse_error(&response[0]);
8311 assert_eq!(error["code"], "module_warming");
8312 assert!(error["message"]
8313 .as_str()
8314 .unwrap()
8315 .contains("state=running, enabled=true, live=false"));
8316 }
8317
8318 #[tokio::test]
8323 async fn route_open_refusal_names_the_check_that_refused() {
8324 let handler = ControlHandler::new(Arc::new(Registry::default()));
8325 let capture = EventCapture::default();
8326 let _subscriber =
8327 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
8328 let (ctx, _rx) = route_ctx(ConnectionId::new(95));
8329 let response = handler
8330 .handle_control_frame(
8331 &ctx,
8332 route_open_frame(395, "nobody", unique_project_root("refusal-reason")),
8333 )
8334 .await
8335 .unwrap();
8336
8337 assert_eq!(parse_error(&response[0])["code"], "unknown_module");
8338 let event = capture
8339 .events()
8340 .into_iter()
8341 .find(|event| {
8342 event.target == "control"
8343 && event.fields.get("code") == Some(&"\"unknown_module\"".to_string())
8344 })
8345 .expect("route.open refusal event");
8346 assert_eq!(
8347 event.fields.get("reason"),
8348 Some(&"\"not_registered\"".to_string())
8349 );
8350 }
8351
8352 #[tokio::test]
8353 async fn route_open_supervised_absence_emits_refusal_fields_and_counts_code() {
8354 let registry = Arc::new(Registry::default());
8355 let supervisor_handle = SupervisorHandle::new();
8356 let supervisor =
8357 Supervisor::new(Arc::clone(®istry), RestartPolicy::new(0, Duration::ZERO))
8358 .with_handle(supervisor_handle.clone())
8359 .with_connection_file_path(std::env::temp_dir().join(format!(
8360 "subc-route-open-refusal-info-{}",
8361 std::process::id()
8362 )));
8363 let module = supervisor
8364 .supervise_configured(
8365 ModuleSpec {
8366 module_id: "warming".to_string(),
8367 program: fake_aft_stub_path(),
8368 args: Vec::new(),
8369 env: Vec::new(),
8370 reserved: false,
8371 reserved_prefixes: Vec::new(),
8372 protocol: ModuleProtocol::Subc,
8373 overlap: Default::default(),
8374 },
8375 true,
8376 )
8377 .unwrap();
8378 assert_eq!(module.state().unwrap(), ModuleState::Running);
8379
8380 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
8381 assert!(handler
8382 .counters()
8383 .snapshot()
8384 .get("route_open_refused_by_code")
8385 .is_none());
8386 let capture = EventCapture::default();
8387 let _subscriber =
8388 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
8389 let (ctx, _rx) = route_ctx(ConnectionId::new(94));
8390 let response = handler
8391 .handle_control_frame(
8392 &ctx,
8393 route_open_frame(394, "warming", unique_project_root("refusal-info")),
8394 )
8395 .await
8396 .unwrap();
8397 module.stop().await.unwrap();
8398
8399 assert_eq!(parse_error(&response[0])["code"], "module_warming");
8400 let event = capture
8401 .events()
8402 .into_iter()
8403 .find(|event| {
8404 event.target == "control"
8405 && event.fields.get("code") == Some(&"\"module_warming\"".to_string())
8406 })
8407 .expect("route.open refusal event");
8408 assert_eq!(
8409 event.fields.get("module_id"),
8410 Some(&"\"warming\"".to_string())
8411 );
8412 assert_eq!(event.fields.get("connection_id"), Some(&"94".to_string()));
8413 assert_eq!(
8414 event.fields.get("reason"),
8415 Some(&"\"supervised_not_registered\"".to_string())
8416 );
8417 assert_eq!(event.fields.get("state"), Some(&"running".to_string()));
8418 assert_eq!(event.fields.get("enabled"), Some(&"true".to_string()));
8419 assert_eq!(event.fields.get("live"), Some(&"false".to_string()));
8420 assert_eq!(
8421 handler.counters().snapshot()["route_open_refused_by_code"],
8422 json!({ "module_warming": 1 })
8423 );
8424 }
8425
8426 #[tokio::test(flavor = "current_thread")]
8427 async fn route_open_unknown_module_escapes_target_module_id() {
8428 let handler = ControlHandler::new(Arc::new(Registry::default()));
8429 let capture = EventCapture::default();
8430 let _subscriber =
8431 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
8432 let hostile_module_id = "\u{1b}]52;c;AAAA\u{07}";
8433 let (ctx, _rx) = route_ctx(ConnectionId::new(95));
8434 let response = handler
8435 .handle_control_frame(
8436 &ctx,
8437 route_open_frame(
8438 395,
8439 hostile_module_id,
8440 unique_project_root("hostile-target-module-id"),
8441 ),
8442 )
8443 .await
8444 .unwrap();
8445
8446 assert_eq!(parse_error(&response[0])["code"], "unknown_module");
8447 let event = capture
8448 .events()
8449 .into_iter()
8450 .find(|event| {
8451 event.target == "control"
8452 && event.fields.get("code") == Some(&"\"unknown_module\"".to_string())
8453 })
8454 .expect("route.open unknown-module refusal event");
8455 let logged = event.fields.get("module_id").expect("module_id field");
8456 assert!(!logged.bytes().any(|byte| byte < 0x20));
8457 assert_eq!(logged, r#""\u{1b}]52;c;AAAA\u{7}""#);
8458 }
8459
8460 #[tokio::test(flavor = "current_thread")]
8461 async fn route_open_module_rejection_uses_daemon_counter_key() {
8462 let registry = Arc::new(Registry::default());
8463 let forwarding = Arc::new(ForwardingTable::default());
8464 let handler =
8465 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
8466 let module_connection = ConnectionId::new(95);
8467 let (module_ctx, mut module_rx) = route_ctx(module_connection);
8468 handler
8469 .handle_control_frame(&module_ctx, hello_frame("aft", PROTOCOL_VERSION, 395))
8470 .await
8471 .unwrap();
8472
8473 let client_connection = ConnectionId::new(96);
8474 let (client_ctx, _client_rx) = route_ctx(client_connection);
8475 let capture = EventCapture::default();
8476 let _subscriber =
8477 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
8478 let (route_task, bind) = relay_route_open(
8479 &handler,
8480 client_connection,
8481 &client_ctx.egress,
8482 &mut module_rx,
8483 396,
8484 "aft",
8485 "hostile-module-code",
8486 )
8487 .await;
8488 let hostile_code = "\u{1b}]52;c;AAAA\u{07}";
8489 let rejection = Frame::build(
8490 FrameType::Error,
8491 control_flags(),
8492 0,
8493 0,
8494 bind.header.corr,
8495 serde_json::to_vec(&ErrorBody::new(hostile_code, "module refused route.bind")).unwrap(),
8496 )
8497 .unwrap();
8498 handler
8499 .handle_control_frame(&module_ctx, rejection)
8500 .await
8501 .unwrap();
8502
8503 let response = route_task.await.unwrap();
8504 assert_eq!(parse_error(&response[0])["code"], hostile_code);
8505 let counters = handler.counters().snapshot();
8506 assert_eq!(
8507 counters["route_open_refused_by_code"],
8508 json!({ "module_rejected": 1 })
8509 );
8510 assert!(counters["route_open_refused_by_code"]
8511 .get(hostile_code)
8512 .is_none());
8513
8514 let event = capture
8515 .events()
8516 .into_iter()
8517 .find(|event| {
8518 event.target == "control"
8519 && event.fields.get("code") == Some(&"\"module_rejected\"".to_string())
8520 })
8521 .expect("route.open module-rejection refusal event");
8522 let logged = event.fields.get("module_code").expect("module_code field");
8523 assert!(!logged.bytes().any(|byte| byte < 0x20));
8524 assert_eq!(logged, r#""\u{1b}]52;c;AAAA\u{7}""#);
8525 }
8526
8527 #[tokio::test]
8528 async fn route_open_keeps_failed_unregistered_supervised_module_unavailable() {
8529 let registry = Arc::new(Registry::default());
8530 let supervisor_handle = SupervisorHandle::new();
8531 let missing_program = std::env::temp_dir().join(format!(
8532 "subc-route-open-missing-program-{}",
8533 std::process::id()
8534 ));
8535 let supervisor =
8536 Supervisor::new(Arc::clone(®istry), RestartPolicy::new(0, Duration::ZERO))
8537 .with_handle(supervisor_handle.clone());
8538 let module = supervisor
8539 .supervise_configured(
8540 ModuleSpec {
8541 module_id: "failed".to_string(),
8542 program: missing_program,
8543 args: Vec::new(),
8544 env: Vec::new(),
8545 reserved: false,
8546 reserved_prefixes: Vec::new(),
8547 protocol: ModuleProtocol::Subc,
8548 overlap: Default::default(),
8549 },
8550 true,
8551 )
8552 .unwrap();
8553 assert_eq!(module.state().unwrap(), ModuleState::Failed);
8554
8555 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
8556 let (ctx, _rx) = route_ctx(ConnectionId::new(40));
8557 let response = handler
8558 .handle_control_frame(
8559 &ctx,
8560 route_open_frame(305, "failed", unique_project_root("failed")),
8561 )
8562 .await
8563 .unwrap();
8564
8565 assert_eq!(response[0].header.ty, FrameType::Error);
8566 let error = parse_error(&response[0]);
8567 assert_eq!(error["code"], "target_unavailable");
8568 assert!(error["message"]
8569 .as_str()
8570 .unwrap()
8571 .contains("state=failed, enabled=true, live=false"));
8572 }
8573
8574 #[tokio::test]
8575 async fn route_open_role_mismatch_remains_target_unavailable() {
8576 let registry = Arc::new(Registry::default());
8577 let handler = ControlHandler::new(Arc::clone(®istry));
8578 handler
8579 .handle_control(
8580 ConnectionId::new(41),
8581 non_routable_hello_frame_with_control_ops("health-only", 306, None),
8582 )
8583 .unwrap();
8584
8585 let (ctx, _rx) = route_ctx(ConnectionId::new(42));
8586 let response = handler
8587 .handle_control_frame(
8588 &ctx,
8589 route_open_frame(307, "health-only", unique_project_root("role-mismatch")),
8590 )
8591 .await
8592 .unwrap();
8593
8594 assert_eq!(parse_error(&response[0])["code"], "target_unavailable");
8595 assert!(parse_error(&response[0])["message"]
8596 .as_str()
8597 .unwrap()
8598 .contains("does not provide the requested target"));
8599 }
8600
8601 #[tokio::test]
8602 async fn route_open_inactive_registration_remains_target_unavailable() {
8603 let registry = Arc::new(Registry::default());
8604 let handler = ControlHandler::new(Arc::clone(®istry));
8605 handler
8606 .handle_control(
8607 ConnectionId::new(43),
8608 hello_frame("inactive", PROTOCOL_VERSION, 308),
8609 )
8610 .unwrap();
8611 assert!(registry
8612 .set_module_state_for_test("inactive", ChannelState::Closed)
8613 .unwrap());
8614
8615 let (ctx, _rx) = route_ctx(ConnectionId::new(44));
8616 let response = handler
8617 .handle_control_frame(
8618 &ctx,
8619 route_open_frame(309, "inactive", unique_project_root("inactive")),
8620 )
8621 .await
8622 .unwrap();
8623
8624 assert_eq!(parse_error(&response[0])["code"], "target_unavailable");
8625 assert!(parse_error(&response[0])["message"]
8626 .as_str()
8627 .unwrap()
8628 .contains("is not active"));
8629 }
8630
8631 #[tokio::test]
8632 async fn late_health_reply_is_recorded_through_the_module_response_path() {
8633 let registry = Arc::new(Registry::default());
8634 let forwarding = Arc::new(ForwardingTable::default());
8635 let supervisor_handle = SupervisorHandle::new();
8636 let supervisor = Supervisor::new(Arc::clone(®istry), crate::RestartPolicy::default())
8637 .with_forwarding(Arc::clone(&forwarding))
8638 .with_handle(supervisor_handle.clone());
8639 let module = supervisor
8640 .supervise_configured(
8641 crate::ModuleSpec {
8642 module_id: "late-health-response".to_string(),
8643 program: PathBuf::from("disabled-module"),
8644 args: Vec::new(),
8645 env: Vec::new(),
8646 reserved: false,
8647 reserved_prefixes: Vec::new(),
8648 protocol: ModuleProtocol::Subc,
8649 overlap: Default::default(),
8650 },
8651 false,
8652 )
8653 .unwrap();
8654 let handler =
8655 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
8656 .with_supervisor(supervisor_handle);
8657 let (module_ctx, _module_rx) = route_ctx(ConnectionId::new(39));
8658 handler
8659 .handle_control_frame(
8660 &module_ctx,
8661 hello_frame_with_control_ops(
8662 "late-health-response",
8663 PROTOCOL_VERSION,
8664 7,
8665 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
8666 ),
8667 )
8668 .await
8669 .unwrap();
8670 let probe_started_at = Instant::now() - Duration::from_millis(80);
8671 let pending = forwarding
8672 .begin_health_probe_rpc_for(
8673 "late-health-response",
8674 MODULE_CONTROL_OP_HEALTH_CHECK,
8675 probe_started_at,
8676 Instant::now() - Duration::from_millis(1),
8677 )
8678 .unwrap();
8679 assert!(forwarding
8680 .tombstone_health_probe_rpc(pending.endpoint, pending.corr)
8681 .unwrap());
8682
8683 let responses = handler
8684 .handle_control_frame(&module_ctx, health_response(pending.corr, HealthStatus::Ok))
8685 .await
8686 .unwrap();
8687
8688 assert!(responses.is_empty());
8689 let health = module.status().unwrap().health;
8690 assert_eq!(health.late_answer_count, 1);
8691 assert!(health.last_late_answer_latency_ms.unwrap() >= 80);
8692 }
8693
8694 #[tokio::test]
8695 async fn health_probe_timeout_and_module_death_are_typed() {
8696 let registry = Arc::new(Registry::default());
8697 let forwarding = Arc::new(ForwardingTable::default());
8698 let handler =
8699 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
8700 .with_health_probe_timeout(Duration::from_millis(50));
8701 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(40));
8702 handler
8703 .handle_control_frame(
8704 &module_ctx,
8705 hello_frame_with_control_ops(
8706 "aft",
8707 PROTOCOL_VERSION,
8708 7,
8709 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
8710 ),
8711 )
8712 .await
8713 .unwrap();
8714
8715 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(41));
8716 let responses = handler
8717 .handle_control_frame(&client_ctx, supervisor_health_probe_frame(201, "aft"))
8718 .await
8719 .unwrap();
8720 assert_eq!(responses[0].header.ty, FrameType::Error);
8721 assert_eq!(parse_error(&responses[0])["code"], "module_timeout");
8722 let _ = module_rx.try_recv();
8723
8724 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(42));
8725 let health_handler = handler.clone();
8726 let death_task = tokio::spawn(async move {
8727 health_handler
8728 .handle_control_frame(&client_ctx, supervisor_health_probe_frame(202, "aft"))
8729 .await
8730 .unwrap()
8731 });
8732 tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
8733 .await
8734 .unwrap()
8735 .unwrap();
8736 handler
8737 .cleanup_connection(module_ctx.connection_id)
8738 .unwrap();
8739 let responses = death_task.await.unwrap();
8740 assert_eq!(responses[0].header.ty, FrameType::Error);
8741 assert_eq!(parse_error(&responses[0])["code"], "target_unavailable");
8742 }
8743
8744 #[test]
8745 fn hello_requires_exact_protocol_version() {
8746 for (connection, offered) in [(1, PROTOCOL_VERSION - 1), (2, PROTOCOL_VERSION + 1)] {
8747 let registry = Arc::new(Registry::default());
8748 let handler = ControlHandler::new(Arc::clone(®istry));
8749 let responses = handler
8750 .handle_control(
8751 ConnectionId::new(connection),
8752 hello_frame("aft", offered, 9),
8753 )
8754 .unwrap();
8755
8756 assert_eq!(responses.len(), 1);
8757 assert_eq!(responses[0].header.ty, FrameType::Error);
8758 let error = parse_error(&responses[0]);
8759 assert_eq!(error["code"], "version_unsupported");
8760 assert!(registry.get_module("aft").unwrap().is_none());
8761 assert_eq!(registry.active_registration_count().unwrap(), 0);
8762 }
8763 }
8764
8765 #[test]
8766 fn unknown_module_push_op_is_ignored_but_malformed_known_op_errors() {
8767 let registry = Arc::new(Registry::default());
8768 let forwarding = Arc::new(ForwardingTable::default());
8769 let handler =
8770 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
8771 let module_connection = ConnectionId::new(301);
8772 let registration = registry
8773 .register_with_control_ops(
8774 manifest("aft-push", PROTOCOL_VERSION),
8775 PROTOCOL_VERSION,
8776 module_connection,
8777 module_baseline_control_ops(),
8778 )
8779 .unwrap();
8780 let (module_tx, _module_rx) = mpsc::channel(8);
8781 let endpoint = forwarding
8782 .register_module_connection(
8783 module_connection,
8784 "aft-push".to_string(),
8785 PROTOCOL_VERSION,
8786 manifest_concurrency(®istration.manifest),
8787 FrameSink::new(module_tx),
8788 )
8789 .unwrap();
8790
8791 let unknown = Frame::build(
8793 FrameType::Push,
8794 control_flags(),
8795 0,
8796 0,
8797 5,
8798 serde_json::to_vec(&json!({"op": "route.future.v2", "extra": 1})).unwrap(),
8799 )
8800 .unwrap();
8801 let out = handler.handle_status_update(endpoint, unknown).unwrap();
8802 assert!(
8803 out.is_empty(),
8804 "unknown push op must be ignored, got {out:?}"
8805 );
8806
8807 let malformed = Frame::build(
8809 FrameType::Push,
8810 control_flags(),
8811 0,
8812 0,
8813 6,
8814 serde_json::to_vec(&json!({"op": "route.status"})).unwrap(),
8815 )
8816 .unwrap();
8817 let out = handler.handle_status_update(endpoint, malformed).unwrap();
8818 assert_eq!(out.len(), 1);
8819 assert_eq!(out[0].header.ty, FrameType::Error);
8820 assert_eq!(parse_error(&out[0])["code"], "invalid_control_body");
8821 }
8822
8823 #[test]
8824 fn hello_rejected_when_connection_already_owns_client_routes() {
8825 let registry = Arc::new(Registry::default());
8826 let forwarding = Arc::new(ForwardingTable::default());
8827 let handler =
8828 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
8829 let _ = bind_liveness_route(®istry, &forwarding, "aft-module");
8831 let client_connection = ConnectionId::new(202);
8832
8833 let responses = handler
8836 .handle_control(
8837 client_connection,
8838 hello_frame("aft-second", PROTOCOL_VERSION, 9),
8839 )
8840 .unwrap();
8841 assert_eq!(responses[0].header.ty, FrameType::Error);
8842 assert_eq!(parse_error(&responses[0])["code"], "invalid_hello");
8843 assert!(registry.get_module("aft-second").unwrap().is_none());
8844 }
8845
8846 #[test]
8847 fn reserved_module_hello_requires_matching_launch_nonce() {
8848 let registry = Arc::new(Registry::default());
8849 let supervisor = SupervisorHandle::new();
8850 supervisor.set_reserved_nonce("vault", "the-real-nonce".to_string());
8853 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor);
8854
8855 let no_nonce = handler
8857 .handle_control(
8858 ConnectionId::new(1),
8859 hello_frame("vault", PROTOCOL_VERSION, 1),
8860 )
8861 .unwrap();
8862 assert_eq!(no_nonce[0].header.ty, FrameType::Error);
8863 assert_eq!(parse_error(&no_nonce[0])["code"], "reserved_module");
8864 assert!(registry.get_module("vault").unwrap().is_none());
8865
8866 let wrong = handler
8868 .handle_control(
8869 ConnectionId::new(2),
8870 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 2, Some("forged")),
8871 )
8872 .unwrap();
8873 assert_eq!(wrong[0].header.ty, FrameType::Error);
8874 assert_eq!(parse_error(&wrong[0])["code"], "reserved_module");
8875 assert!(registry.get_module("vault").unwrap().is_none());
8876
8877 let ok = handler
8879 .handle_control(
8880 ConnectionId::new(3),
8881 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 3, Some("the-real-nonce")),
8882 )
8883 .unwrap();
8884 assert_eq!(ok[0].header.ty, FrameType::HelloAck);
8885 assert!(registry.get_module("vault").unwrap().is_some());
8886 }
8887
8888 #[test]
8889 fn reserved_prefix_hello_uses_delimiter_sensitive_owner_nonce() {
8890 let registry = Arc::new(Registry::default());
8891 let supervisor = SupervisorHandle::new();
8892 supervisor.set_spawn_nonce("federation", "owner-nonce".to_string());
8893 supervisor.set_reserved_prefixes("federation", &["fed:".to_string()]);
8894 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor);
8895
8896 let squat = handler
8897 .handle_control(
8898 ConnectionId::new(1),
8899 hello_frame("fed:peerA:tool", PROTOCOL_VERSION, 1),
8900 )
8901 .unwrap();
8902 assert_eq!(squat[0].header.ty, FrameType::Error);
8903 assert_eq!(parse_error(&squat[0])["code"], "reserved_module");
8904 assert!(parse_error(&squat[0])["message"]
8905 .as_str()
8906 .unwrap()
8907 .contains("fed:"));
8908
8909 let accepted_peer = handler
8910 .handle_control(
8911 ConnectionId::new(2),
8912 hello_frame_with_nonce("fed:peerA:tool", PROTOCOL_VERSION, 2, Some("owner-nonce")),
8913 )
8914 .unwrap();
8915 assert_eq!(accepted_peer[0].header.ty, FrameType::HelloAck);
8916
8917 let accepted_short = handler
8918 .handle_control(
8919 ConnectionId::new(3),
8920 hello_frame_with_nonce("fed:x", PROTOCOL_VERSION, 3, Some("owner-nonce")),
8921 )
8922 .unwrap();
8923 assert_eq!(accepted_short[0].header.ty, FrameType::HelloAck);
8924
8925 for (conn, module_id) in [(4, "fedx:tool"), (5, "fed"), (6, "FED:x")] {
8926 let response = handler
8927 .handle_control(
8928 ConnectionId::new(conn),
8929 hello_frame(module_id, PROTOCOL_VERSION, conn),
8930 )
8931 .unwrap();
8932 assert_eq!(response[0].header.ty, FrameType::HelloAck, "{module_id}");
8933 }
8934 }
8935
8936 #[test]
8937 fn exact_reserved_module_takes_precedence_over_reserved_prefix() {
8938 let registry = Arc::new(Registry::default());
8939 let supervisor = SupervisorHandle::new();
8940 supervisor.set_spawn_nonce("federation", "owner-nonce".to_string());
8941 supervisor.set_reserved_prefixes("federation", &["fed:".to_string()]);
8942 supervisor.set_reserved_nonce("fed:special", "exact-nonce".to_string());
8943 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor);
8944
8945 let owner_nonce = handler
8946 .handle_control(
8947 ConnectionId::new(1),
8948 hello_frame_with_nonce("fed:special", PROTOCOL_VERSION, 1, Some("owner-nonce")),
8949 )
8950 .unwrap();
8951 assert_eq!(owner_nonce[0].header.ty, FrameType::Error);
8952 assert_eq!(parse_error(&owner_nonce[0])["code"], "reserved_module");
8953 assert!(registry.get_module("fed:special").unwrap().is_none());
8954
8955 let exact_nonce = handler
8956 .handle_control(
8957 ConnectionId::new(2),
8958 hello_frame_with_nonce("fed:special", PROTOCOL_VERSION, 2, Some("exact-nonce")),
8959 )
8960 .unwrap();
8961 assert_eq!(exact_nonce[0].header.ty, FrameType::HelloAck);
8962 assert!(registry.get_module("fed:special").unwrap().is_some());
8963 }
8964
8965 #[test]
8966 fn non_reserved_module_ignores_launch_nonce() {
8967 let registry = Arc::new(Registry::default());
8968 let handler = ControlHandler::new(Arc::clone(®istry));
8971 let no_nonce = handler
8972 .handle_control(
8973 ConnectionId::new(1),
8974 hello_frame("aft-no-nonce", PROTOCOL_VERSION, 1),
8975 )
8976 .unwrap();
8977 assert_eq!(no_nonce[0].header.ty, FrameType::HelloAck);
8978 assert!(registry.get_module("aft-no-nonce").unwrap().is_some());
8979
8980 let echoed_nonce = handler
8981 .handle_control(
8982 ConnectionId::new(2),
8983 hello_frame_with_nonce("aft-with-nonce", PROTOCOL_VERSION, 2, Some("spawn-nonce")),
8984 )
8985 .unwrap();
8986 assert_eq!(echoed_nonce[0].header.ty, FrameType::HelloAck);
8987 assert!(registry.get_module("aft-with-nonce").unwrap().is_some());
8988 }
8989
8990 #[test]
8991 fn malformed_hello_returns_error_and_handler_still_answers_ping() {
8992 let handler = ControlHandler::default();
8993 let conn = ConnectionId::new(1);
8994 let malformed = Frame::build(
8995 FrameType::Hello,
8996 control_flags(),
8997 0,
8998 0,
8999 3,
9000 b"{not json".to_vec(),
9001 )
9002 .unwrap();
9003
9004 let error = handler.handle_control(conn, malformed).unwrap();
9005 assert_eq!(error[0].header.ty, FrameType::Error);
9006 assert_eq!(parse_error(&error[0])["code"], "invalid_hello");
9007
9008 let ping = Frame::build(FrameType::Ping, control_flags(), 0, 0, 4, Vec::new()).unwrap();
9009 let pong = handler.handle_control(conn, ping).unwrap();
9010 assert_eq!(pong[0].header.ty, FrameType::Pong);
9011 assert_eq!(pong[0].header.corr, 4);
9012 }
9013
9014 #[test]
9015 fn duplicate_module_id_is_rejected_without_replacing_active_registration() {
9016 let registry = Arc::new(Registry::default());
9017 let handler = ControlHandler::new(Arc::clone(®istry));
9018
9019 handler
9020 .handle_control(
9021 ConnectionId::new(1),
9022 hello_frame("aft", PROTOCOL_VERSION, 1),
9023 )
9024 .unwrap();
9025 let duplicate = handler
9026 .handle_control(
9027 ConnectionId::new(2),
9028 hello_frame("aft", PROTOCOL_VERSION, 2),
9029 )
9030 .unwrap();
9031
9032 assert_eq!(duplicate[0].header.ty, FrameType::Error);
9033 assert_eq!(parse_error(&duplicate[0])["code"], "duplicate_module_id");
9034 let registration = registry.get_module("aft").unwrap().unwrap();
9035 assert_eq!(registration.connection_id, ConnectionId::new(1));
9036 }
9037
9038 #[test]
9039 fn liveness_poll_reports_false_when_process_liveness_reports_dead() {
9040 let registry = Arc::new(Registry::default());
9041 let forwarding = Arc::new(ForwardingTable::default());
9042 let process_liveness = Arc::new(FakeProcessLiveness { live: Some(false) });
9043 let handler =
9044 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9045 .with_process_liveness(process_liveness);
9046 let (ctx, route_channel, route_epoch) =
9047 bind_liveness_route(®istry, &forwarding, "aft-dead");
9048 let responses = handler
9049 .handle_route_poll(
9050 &ctx,
9051 route_poll_frame(41, PollKind::Liveness, route_channel),
9052 route_channel,
9053 route_epoch,
9054 PollKind::Liveness,
9055 )
9056 .unwrap();
9057
9058 assert_eq!(responses.len(), 1);
9059 assert_eq!(responses[0].header.ty, FrameType::Response);
9060 assert_route_poll_liveness(&responses[0], false);
9061 }
9062
9063 #[test]
9064 fn liveness_poll_without_process_source_uses_bound_route() {
9065 let registry = Arc::new(Registry::default());
9066 let forwarding = Arc::new(ForwardingTable::default());
9067 let handler =
9068 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
9069 let (ctx, route_channel, route_epoch) =
9070 bind_liveness_route(®istry, &forwarding, "aft-bound-only");
9071 let responses = handler
9072 .handle_route_poll(
9073 &ctx,
9074 route_poll_frame(42, PollKind::Liveness, route_channel),
9075 route_channel,
9076 route_epoch,
9077 PollKind::Liveness,
9078 )
9079 .unwrap();
9080
9081 assert_route_poll_liveness(&responses[0], true);
9082 }
9083
9084 #[test]
9085 fn liveness_poll_untracked_process_source_uses_bound_route() {
9086 let registry = Arc::new(Registry::default());
9087 let forwarding = Arc::new(ForwardingTable::default());
9088 let process_liveness = Arc::new(FakeProcessLiveness { live: None });
9089 let handler =
9090 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9091 .with_process_liveness(process_liveness);
9092 let (ctx, route_channel, route_epoch) =
9093 bind_liveness_route(®istry, &forwarding, "aft-untracked");
9094 let responses = handler
9095 .handle_route_poll(
9096 &ctx,
9097 route_poll_frame(43, PollKind::Liveness, route_channel),
9098 route_channel,
9099 route_epoch,
9100 PollKind::Liveness,
9101 )
9102 .unwrap();
9103
9104 assert_route_poll_liveness(&responses[0], true);
9105 }
9106
9107 #[tokio::test]
9108 async fn unknown_op_returns_unknown_control_op() {
9109 let handler = ControlHandler::default();
9110 let (ctx, _rx) = route_ctx(ConnectionId::new(77));
9111 let request = Frame::build(
9112 FrameType::Request,
9113 control_flags(),
9114 0,
9115 0,
9116 55,
9117 br#"{"op":"route.nope","route_channel":1}"#.to_vec(),
9118 )
9119 .unwrap();
9120
9121 let response = handler.handle_control_frame(&ctx, request).await.unwrap();
9122
9123 assert_eq!(response.len(), 1);
9124 assert_eq!(response[0].header.ty, FrameType::Error);
9125 assert_eq!(response[0].header.corr, 55);
9126 assert_eq!(parse_error(&response[0])["code"], "unknown_control_op");
9127 }
9128
9129 #[tokio::test]
9130 async fn supervisor_provenance_rejects_unknown_exact_module() {
9131 let handler = ControlHandler::default();
9132 let (ctx, _rx) = route_ctx(ConnectionId::new(79));
9133 let request = Frame::build(
9134 FrameType::Request,
9135 control_flags(),
9136 0,
9137 0,
9138 57,
9139 br#"{"op":"supervisor.provenance","module_id":"missing"}"#.to_vec(),
9140 )
9141 .unwrap();
9142
9143 let response = handler.handle_control_frame(&ctx, request).await.unwrap();
9144
9145 assert_eq!(response.len(), 1);
9146 assert_eq!(response[0].header.ty, FrameType::Error);
9147 assert_eq!(response[0].header.corr, 57);
9148 let error = parse_error(&response[0]);
9149 assert_eq!(error["code"], "unknown_module");
9150 assert_eq!(error["message"], "module_id 'missing' is not supervised");
9151 }
9152
9153 #[test]
9154 fn provenance_probe_override_keeps_handler_tests_deterministic() {
9155 let expected = subc_control::RunningImageAgreement::Unavailable {
9156 reason: subc_control::RunningImageUnavailableReason::HashFailed,
9157 };
9158 let handler = ControlHandler::default().with_provenance_probe_result(expected.clone());
9159 assert_eq!(handler.provenance_probe_override, Some(expected));
9160 }
9161
9162 #[tokio::test]
9163 async fn malformed_control_bodies_return_invalid_control_body() {
9164 let handler = ControlHandler::default();
9165 let (ctx, _rx) = route_ctx(ConnectionId::new(78));
9166
9167 for (corr, body) in [
9168 (56, br#"{"route_channel":1}"#.as_slice()),
9169 (57, br#"{"op":17,"route_channel":1}"#.as_slice()),
9170 (
9171 58,
9172 br#"{"op":"route.poll","route_channel":"bad","kind":"status"}"#.as_slice(),
9173 ),
9174 ] {
9175 let request = Frame::build(
9176 FrameType::Request,
9177 control_flags(),
9178 0,
9179 0,
9180 corr,
9181 body.to_vec(),
9182 )
9183 .unwrap();
9184 let response = handler.handle_control_frame(&ctx, request).await.unwrap();
9185
9186 assert_eq!(response.len(), 1);
9187 assert_eq!(response[0].header.ty, FrameType::Error);
9188 assert_eq!(response[0].header.corr, corr);
9189 assert_eq!(parse_error(&response[0])["code"], "invalid_control_body");
9190 }
9191 }
9192
9193 #[tokio::test]
9194 async fn goodbye_tears_down_registration_and_later_channel_is_unknown() {
9195 let registry = Arc::new(Registry::default());
9196 let control = Arc::new(ControlHandler::new(Arc::clone(®istry)));
9197 let router = Router::with_control_handler(Arc::clone(&control));
9198 let connection = router.begin_connection();
9199 let (ctx, mut rx) = route_ctx(connection.id());
9200
9201 router
9202 .route_for_connection(&ctx, hello_frame("aft", PROTOCOL_VERSION, 11))
9203 .await
9204 .unwrap();
9205 let response = rx.recv().await.unwrap();
9206 let ack = parse_ack(&response);
9207 assert_eq!(ack.negotiated_ver, PROTOCOL_VERSION);
9208 let channel = 1;
9209
9210 let goodbye =
9211 Frame::build(FrameType::Goodbye, control_flags(), 0, 0, 12, Vec::new()).unwrap();
9212 router.route_for_connection(&ctx, goodbye).await.unwrap();
9213 assert!(rx.try_recv().is_err());
9214 assert!(registry.get_module("aft").unwrap().is_none());
9215
9216 router
9217 .route_for_connection(&ctx, channel_request(channel, 13))
9218 .await
9219 .unwrap();
9220 let error_frame = rx.recv().await.unwrap();
9221 assert_eq!(error_frame.header.ty, FrameType::Error);
9222 assert_eq!(error_frame.header.channel, channel);
9223 }
9224
9225 #[tokio::test]
9226 async fn dropping_router_connection_releases_registration() {
9227 let registry = Arc::new(Registry::default());
9228 let control = Arc::new(ControlHandler::new(Arc::clone(®istry)));
9229 let router = Router::with_control_handler(control);
9230 let connection = router.begin_connection();
9231 let (ctx, mut rx) = route_ctx(connection.id());
9232
9233 router
9234 .route_for_connection(&ctx, hello_frame("aft", PROTOCOL_VERSION, 31))
9235 .await
9236 .unwrap();
9237 let response = rx.recv().await.unwrap();
9238 let ack = parse_ack(&response);
9239 assert_eq!(ack.negotiated_ver, PROTOCOL_VERSION);
9240 assert!(registry.get_module("aft").unwrap().is_some());
9241
9242 drop(connection);
9243
9244 assert!(registry.get_module("aft").unwrap().is_none());
9245 assert_eq!(registry.active_registration_count().unwrap(), 0);
9246 }
9247
9248 fn capability_manifest(
9249 module_id: &str,
9250 provides: &[&str],
9251 must_never_reach: &[&str],
9252 ) -> ModuleManifest {
9253 let mut manifest = manifest(module_id, PROTOCOL_VERSION);
9254 manifest.capabilities = Some(CapabilityDeclarations {
9255 provides: provides
9256 .iter()
9257 .map(|capability| (*capability).to_string())
9258 .collect(),
9259 requires: Vec::new(),
9260 must_never_reach: must_never_reach
9261 .iter()
9262 .map(|capability| (*capability).to_string())
9263 .collect(),
9264 });
9265 manifest
9266 }
9267
9268 fn hello_frame_with_manifest(manifest: ModuleManifest, corr: u64) -> Frame {
9269 Frame::build(
9270 FrameType::Hello,
9271 control_flags(),
9272 0,
9273 0,
9274 corr,
9275 serde_json::to_vec(&ModuleHelloBody {
9276 protocol_ver: manifest.protocol_ver,
9277 manifest,
9278 control_ops: None,
9279 launch_nonce: None,
9280 })
9281 .expect("capability test HELLO serializes"),
9282 )
9283 .expect("capability test HELLO frame builds")
9284 }
9285
9286 fn catalog_update_with_capabilities_frame(
9287 corr: u64,
9288 capabilities: CapabilityDeclarations,
9289 ) -> Frame {
9290 Frame::build(
9291 FrameType::Request,
9292 control_flags(),
9293 0,
9294 0,
9295 corr,
9296 serde_json::to_vec(&ModuleControlRequestFromModule::CatalogUpdate {
9297 provides: manifest("catalog-update-placeholder", PROTOCOL_VERSION).provides,
9298 capabilities: Some(capabilities),
9299 ready: None,
9300 })
9301 .expect("capability catalog.update serializes"),
9302 )
9303 .expect("capability catalog.update frame builds")
9304 }
9305
9306 async fn register_capability_manifest(
9307 handler: &ControlHandler,
9308 ctx: &RouteCtx,
9309 manifest: ModuleManifest,
9310 corr: u64,
9311 ) {
9312 let replies = handler
9313 .handle_control_frame(ctx, hello_frame_with_manifest(manifest, corr))
9314 .await
9315 .expect("capability test HELLO succeeds");
9316 assert!(
9317 matches!(replies.as_slice(), [Frame { header, .. }] if header.ty == FrameType::HelloAck),
9318 "capability test HELLO must register"
9319 );
9320 }
9321
9322 async fn open_route_for_capability_test(
9323 handler: &ControlHandler,
9324 target_ctx: &RouteCtx,
9325 target_rx: &mut mpsc::Receiver<crate::router::OutboundFrame>,
9326 client_connection_id: u64,
9327 corr: u64,
9328 target_module_id: &str,
9329 consumer_identity: Option<ConsumerIdentity>,
9330 ) -> (
9331 mpsc::Receiver<crate::router::OutboundFrame>,
9332 ModuleControlRequest,
9333 ) {
9334 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(client_connection_id));
9335 let route_handler = handler.clone();
9336 let target_module_id = target_module_id.to_string();
9337 let route_task = tokio::spawn(async move {
9338 route_handler
9339 .handle_control_frame(
9340 &client_ctx,
9341 route_open_frame_with_admission_facts(
9342 corr,
9343 &target_module_id,
9344 unique_project_root("admission-facts"),
9345 consumer_identity,
9346 None,
9347 ),
9348 )
9349 .await
9350 .expect("capability test route.open succeeds")
9351 });
9352 let bind = tokio::time::timeout(Duration::from_secs(1), target_rx.recv())
9353 .await
9354 .expect("capability test route.open must reach route.bind")
9355 .expect("target control receiver stays open");
9356 let bind_request: ModuleControlRequest =
9357 serde_json::from_slice(&bind.body).expect("route.bind decodes");
9358 handler
9359 .handle_control_frame(target_ctx, route_bind_ack(bind.header.corr))
9360 .await
9361 .expect("capability test route.bind ACK succeeds");
9362 assert!(route_task.await.expect("route.open task joins").is_empty());
9363 let opened = client_rx
9364 .recv()
9365 .await
9366 .expect("successful route.open publishes a response");
9367 assert!(matches!(
9368 serde_json::from_slice::<ClientControlResponse>(&opened.body),
9369 Ok(ClientControlResponse::RouteOpen { .. })
9370 ));
9371 (client_rx, bind_request)
9372 }
9373
9374 fn assert_capability_denied_push(frame: Frame, target_module_id: &str) {
9375 assert_eq!(frame.header.ty, FrameType::Push);
9376 assert_eq!(frame.header.channel, 0);
9377 assert_eq!(
9378 serde_json::from_slice::<ClientControlPush>(&frame.body)
9379 .expect("route.closed control push decodes"),
9380 ClientControlPush::RouteClosed {
9381 module_id: target_module_id.to_string(),
9382 reason: RouteCloseReason::CapabilityDenied,
9383 drained: false,
9384 abandoned: 0,
9385 excluded_subscriptions: 0,
9386 terminal: Some(false),
9387 }
9388 );
9389 }
9390
9391 #[tokio::test]
9392 async fn route_open_capability_forbidden_mutation_proof_creates_no_route() {
9393 let registry = Arc::new(Registry::default());
9394 let forwarding = Arc::new(ForwardingTable::default());
9395 let supervisor = SupervisorHandle::new();
9396 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
9397 let handler =
9398 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9399 .with_supervisor(supervisor);
9400 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(700));
9401 let (opener_ctx, _opener_rx) = route_ctx(ConnectionId::new(701));
9402 register_capability_manifest(
9403 &handler,
9404 &target_ctx,
9405 capability_manifest("target", &["credentials-provider/v1"], &[]),
9406 1,
9407 )
9408 .await;
9409 register_capability_manifest(
9410 &handler,
9411 &opener_ctx,
9412 capability_manifest("opener", &[], &["credentials-provider/v1"]),
9413 2,
9414 )
9415 .await;
9416
9417 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(702));
9418 let replies = handler
9419 .handle_control_frame(
9420 &client_ctx,
9421 route_open_frame_with_admission_facts(
9422 3,
9423 "target",
9424 unique_project_root("admission-facts"),
9425 Some(ConsumerIdentity {
9426 module_id: "opener".to_string(),
9427 launch_nonce: "opener-nonce".to_string(),
9428 }),
9429 None,
9430 ),
9431 )
9432 .await
9433 .expect("denied route.open returns a typed frame");
9434 assert_eq!(parse_error(&replies[0])["code"], "capability_forbidden");
9435 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
9436 assert!(
9437 target_rx.try_recv().is_err(),
9438 "forbidden route.open must not relay route.bind"
9439 );
9440 }
9441
9442 #[tokio::test]
9443 async fn capability_deny_edge_hello_mutation_proof_force_closes_existing_route() {
9444 let registry = Arc::new(Registry::default());
9445 let forwarding = Arc::new(ForwardingTable::default());
9446 let supervisor = SupervisorHandle::new();
9447 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
9448 let handler =
9449 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9450 .with_supervisor(supervisor);
9451 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(710));
9452 let (old_opener_ctx, _old_opener_rx) = route_ctx(ConnectionId::new(711));
9453 register_capability_manifest(
9454 &handler,
9455 &target_ctx,
9456 capability_manifest("target", &["credentials-provider/v1"], &[]),
9457 1,
9458 )
9459 .await;
9460 register_capability_manifest(
9461 &handler,
9462 &old_opener_ctx,
9463 capability_manifest("opener", &[], &[]),
9464 2,
9465 )
9466 .await;
9467 let (mut client_rx, _) = open_route_for_capability_test(
9468 &handler,
9469 &target_ctx,
9470 &mut target_rx,
9471 712,
9472 3,
9473 "target",
9474 Some(ConsumerIdentity {
9475 module_id: "opener".to_string(),
9476 launch_nonce: "opener-nonce".to_string(),
9477 }),
9478 )
9479 .await;
9480 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
9481
9482 handler
9483 .cleanup_connection(old_opener_ctx.connection_id)
9484 .expect("old opener registration cleans up");
9485 let (new_opener_ctx, _new_opener_rx) = route_ctx(ConnectionId::new(713));
9486 register_capability_manifest(
9487 &handler,
9488 &new_opener_ctx,
9489 capability_manifest("opener", &[], &["credentials-provider/v1"]),
9490 4,
9491 )
9492 .await;
9493
9494 assert_capability_denied_push(
9495 client_rx
9496 .try_recv()
9497 .expect("HELLO deny addition must emit route.closed")
9498 .frame,
9499 "target",
9500 );
9501 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
9502 assert!(matches!(
9503 target_rx.try_recv(),
9504 Ok(outbound) if outbound.header.ty == FrameType::Goodbye
9505 ));
9506 }
9507
9508 #[tokio::test]
9509 async fn capability_claim_catalog_update_mutation_proof_force_closes_existing_route() {
9510 let registry = Arc::new(Registry::default());
9511 let forwarding = Arc::new(ForwardingTable::default());
9512 let supervisor = SupervisorHandle::new();
9513 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
9514 let handler =
9515 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9516 .with_supervisor(supervisor);
9517 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(720));
9518 let (opener_ctx, _opener_rx) = route_ctx(ConnectionId::new(721));
9519 register_capability_manifest(
9520 &handler,
9521 &target_ctx,
9522 capability_manifest("target", &[], &[]),
9523 1,
9524 )
9525 .await;
9526 register_capability_manifest(
9527 &handler,
9528 &opener_ctx,
9529 capability_manifest("opener", &[], &["credentials-provider/v1"]),
9530 2,
9531 )
9532 .await;
9533 let (mut client_rx, _) = open_route_for_capability_test(
9534 &handler,
9535 &target_ctx,
9536 &mut target_rx,
9537 722,
9538 3,
9539 "target",
9540 Some(ConsumerIdentity {
9541 module_id: "opener".to_string(),
9542 launch_nonce: "opener-nonce".to_string(),
9543 }),
9544 )
9545 .await;
9546 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
9547
9548 let replies = handler
9549 .handle_control_frame(
9550 &target_ctx,
9551 catalog_update_with_capabilities_frame(
9552 4,
9553 CapabilityDeclarations {
9554 provides: vec!["credentials-provider/v1".to_string()],
9555 requires: Vec::new(),
9556 must_never_reach: Vec::new(),
9557 },
9558 ),
9559 )
9560 .await
9561 .expect("claim catalog.update succeeds");
9562 assert!(matches!(
9563 serde_json::from_slice::<ModuleControlResponseToModule>(&replies[0].body),
9564 Ok(ModuleControlResponseToModule::CatalogUpdate {})
9565 ));
9566 assert_capability_denied_push(
9567 client_rx
9568 .try_recv()
9569 .expect("claim addition must emit route.closed")
9570 .frame,
9571 "target",
9572 );
9573 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
9574 assert!(matches!(
9575 target_rx.try_recv(),
9576 Ok(outbound) if outbound.header.ty == FrameType::Goodbye
9577 ));
9578 }
9579
9580 #[tokio::test]
9581 async fn capability_claim_removal_mutation_proof_keeps_route_open_without_close_frame() {
9582 let registry = Arc::new(Registry::default());
9583 let forwarding = Arc::new(ForwardingTable::default());
9584 let supervisor = SupervisorHandle::new();
9585 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
9586 let handler =
9587 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9588 .with_supervisor(supervisor);
9589 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(730));
9590 let (opener_ctx, _opener_rx) = route_ctx(ConnectionId::new(731));
9591 register_capability_manifest(
9592 &handler,
9593 &target_ctx,
9594 capability_manifest("target", &["credentials-provider/v1"], &[]),
9595 1,
9596 )
9597 .await;
9598 register_capability_manifest(
9599 &handler,
9600 &opener_ctx,
9601 capability_manifest("opener", &[], &[]),
9602 2,
9603 )
9604 .await;
9605 let (mut client_rx, _) = open_route_for_capability_test(
9606 &handler,
9607 &target_ctx,
9608 &mut target_rx,
9609 732,
9610 3,
9611 "target",
9612 Some(ConsumerIdentity {
9613 module_id: "opener".to_string(),
9614 launch_nonce: "opener-nonce".to_string(),
9615 }),
9616 )
9617 .await;
9618 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
9619
9620 handler
9621 .handle_control_frame(
9622 &target_ctx,
9623 catalog_update_with_capabilities_frame(
9624 4,
9625 CapabilityDeclarations {
9626 provides: Vec::new(),
9627 requires: Vec::new(),
9628 must_never_reach: Vec::new(),
9629 },
9630 ),
9631 )
9632 .await
9633 .expect("claim removal catalog.update succeeds");
9634 assert_eq!(
9635 forwarding.active_binding_count().unwrap(),
9636 1,
9637 "removing an attested target claim must leave the route census unchanged"
9638 );
9639 assert!(
9640 client_rx.try_recv().is_err(),
9641 "claim removal must not emit route.closed capability_denied"
9642 );
9643 assert!(
9644 target_rx.try_recv().is_err(),
9645 "claim removal must not send the target a route GOODBYE"
9646 );
9647 }
9648
9649 #[tokio::test]
9652 async fn direct_client_scope_honesty_mutation_proof_opens_denied_capability_provider() {
9653 let registry = Arc::new(Registry::default());
9654 let forwarding = Arc::new(ForwardingTable::default());
9655 let supervisor = SupervisorHandle::new();
9656 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
9657 let handler =
9658 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9659 .with_supervisor(supervisor);
9660 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(740));
9661 let (opener_ctx, _opener_rx) = route_ctx(ConnectionId::new(741));
9662 register_capability_manifest(
9663 &handler,
9664 &target_ctx,
9665 capability_manifest("target", &["credentials-provider/v1"], &[]),
9666 1,
9667 )
9668 .await;
9669 register_capability_manifest(
9670 &handler,
9671 &opener_ctx,
9672 capability_manifest("opener", &[], &["credentials-provider/v1"]),
9673 2,
9674 )
9675 .await;
9676
9677 let (_client_rx, bind) = open_route_for_capability_test(
9678 &handler,
9679 &target_ctx,
9680 &mut target_rx,
9681 742,
9682 3,
9683 "target",
9684 None,
9685 )
9686 .await;
9687 let ModuleControlRequest::RouteBind { principal, .. } = bind else {
9688 panic!("direct scope-honesty route must bind");
9689 };
9690 assert_eq!(principal, Some(Principal::Direct));
9691 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
9692 }
9693
9694 #[tokio::test]
9697 async fn must_never_reach_self_route_is_capability_forbidden() {
9698 let registry = Arc::new(Registry::default());
9699 let forwarding = Arc::new(ForwardingTable::default());
9700 let supervisor = SupervisorHandle::new();
9701 supervisor.set_spawn_nonce("self-provider", "self-nonce".to_string());
9702 let handler =
9703 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9704 .with_supervisor(supervisor);
9705 let (self_ctx, mut self_rx) = route_ctx(ConnectionId::new(750));
9706 register_capability_manifest(
9707 &handler,
9708 &self_ctx,
9709 capability_manifest(
9710 "self-provider",
9711 &["credentials-provider/v1"],
9712 &["credentials-provider/v1"],
9713 ),
9714 1,
9715 )
9716 .await;
9717
9718 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(751));
9719 let replies = handler
9720 .handle_control_frame(
9721 &client_ctx,
9722 route_open_frame_with_admission_facts(
9723 2,
9724 "self-provider",
9725 unique_project_root("admission-facts"),
9726 Some(ConsumerIdentity {
9727 module_id: "self-provider".to_string(),
9728 launch_nonce: "self-nonce".to_string(),
9729 }),
9730 None,
9731 ),
9732 )
9733 .await
9734 .expect("self-route refusal returns a typed frame");
9735 assert_eq!(parse_error(&replies[0])["code"], "capability_forbidden");
9736 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
9737 assert!(
9738 self_rx.try_recv().is_err(),
9739 "self denial must not relay route.bind"
9740 );
9741 }
9742
9743 #[test]
9744 fn unsupported_channel_zero_frame_returns_error() {
9745 let handler = ControlHandler::default();
9746 let request = Frame::build(
9747 FrameType::Request,
9748 control_flags(),
9749 0,
9750 0,
9751 21,
9752 b"opaque".to_vec(),
9753 )
9754 .unwrap();
9755
9756 let response = handler
9757 .handle_control(ConnectionId::new(1), request)
9758 .unwrap();
9759
9760 assert_eq!(response[0].header.ty, FrameType::Error);
9761 assert_eq!(
9762 parse_error(&response[0])["code"],
9763 "unsupported_control_frame"
9764 );
9765 }
9766
9767 mod swap {
9772 use super::*;
9773
9774 const INCUMBENT: ConnectionId = ConnectionId::new(30);
9775 const CANDIDATE: ConnectionId = ConnectionId::new(40);
9776
9777 struct Swap {
9778 registry: Arc<Registry>,
9779 forwarding: Arc<ForwardingTable>,
9780 handler: ControlHandler,
9781 incumbent_ctx: RouteCtx,
9782 incumbent_rx: mpsc::Receiver<crate::router::OutboundFrame>,
9783 candidate_ctx: RouteCtx,
9784 candidate_rx: mpsc::Receiver<crate::router::OutboundFrame>,
9785 }
9786
9787 async fn swap_with_incumbent() -> Swap {
9788 let registry = Arc::new(Registry::default());
9789 let forwarding = Arc::new(ForwardingTable::default());
9790 let handler =
9791 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
9792 let (incumbent_ctx, incumbent_rx) = route_ctx(INCUMBENT);
9793 handler
9794 .handle_control_frame(&incumbent_ctx, hello_frame("aft", PROTOCOL_VERSION, 7))
9795 .await
9796 .unwrap();
9797 let (candidate_ctx, candidate_rx) = route_ctx(CANDIDATE);
9798 Swap {
9799 registry,
9800 forwarding,
9801 handler,
9802 incumbent_ctx,
9803 incumbent_rx,
9804 candidate_ctx,
9805 candidate_rx,
9806 }
9807 }
9808
9809 fn register_candidate(swap: &Swap, ready: Option<bool>) {
9810 let mut candidate_manifest = manifest("aft", PROTOCOL_VERSION);
9811 candidate_manifest.ready = ready;
9812 let registration = swap
9813 .registry
9814 .register_candidate_with_control_ops(
9815 candidate_manifest,
9816 PROTOCOL_VERSION,
9817 CANDIDATE,
9818 module_baseline_control_ops(),
9819 )
9820 .unwrap();
9821 swap.forwarding
9822 .register_candidate_module_connection(
9823 CANDIDATE,
9824 "aft".to_string(),
9825 PROTOCOL_VERSION,
9826 manifest_concurrency(®istration.manifest),
9827 swap.candidate_ctx.egress.clone(),
9828 )
9829 .unwrap();
9830 }
9831
9832 fn cutover(swap: &Swap) -> crate::forwarding::ModuleEndpointId {
9833 let cutover = swap.forwarding.cutover_candidate("aft").unwrap().unwrap();
9834 swap.registry.promote_candidate("aft").unwrap().unwrap();
9835 cutover.incumbent.unwrap()
9836 }
9837
9838 fn keyed_total(counters: &Value, key: &str) -> u64 {
9839 counters[key]
9840 .as_object()
9841 .map(|counts| counts.values().filter_map(Value::as_u64).sum())
9842 .unwrap_or(0)
9843 }
9844
9845 #[tokio::test]
9851 async fn incumbent_ack_between_promotion_and_drain_keeps_the_incumbent_serving() {
9852 let mut swap = swap_with_incumbent().await;
9853 let handler = swap.handler.clone();
9854
9855 let cotenant = ConnectionId::new(31);
9857 let (cotenant_ctx, mut cotenant_rx) = route_ctx(cotenant);
9858 let (cotenant_task, cotenant_bind) = relay_route_open(
9859 &handler,
9860 cotenant,
9861 &cotenant_ctx.egress,
9862 &mut swap.incumbent_rx,
9863 100,
9864 "aft",
9865 "swap-cotenant",
9866 )
9867 .await;
9868 handler
9869 .handle_control_frame(
9870 &swap.incumbent_ctx,
9871 route_bind_ack(cotenant_bind.header.corr),
9872 )
9873 .await
9874 .unwrap();
9875 assert!(cotenant_task.await.unwrap().is_empty());
9876 let (cotenant_channel, cotenant_epoch) =
9877 published_route(&cotenant_rx.recv().await.unwrap());
9878
9879 let caller = ConnectionId::new(32);
9881 let (caller_ctx, mut caller_rx) = route_ctx(caller);
9882 let (caller_task, caller_bind) = relay_route_open(
9883 &handler,
9884 caller,
9885 &caller_ctx.egress,
9886 &mut swap.incumbent_rx,
9887 101,
9888 "aft",
9889 "swap-caller",
9890 )
9891 .await;
9892 let (abandoned_channel, abandoned_epoch) = route_bind_channel(&caller_bind);
9893
9894 register_candidate(&swap, None);
9895 cutover(&swap);
9896
9897 let ack = handler
9899 .handle_control_frame(&swap.incumbent_ctx, route_bind_ack(caller_bind.header.corr))
9900 .await;
9901 let module_loop_error = ack.as_ref().err().map(ToString::to_string);
9902 if module_loop_error.is_some() {
9903 handler.cleanup_connection(INCUMBENT).unwrap();
9906 }
9907
9908 assert!(
9910 cotenant_rx.try_recv().is_err(),
9911 "the co-tenant route on the incumbent was torn down by one late ack: \
9912 {module_loop_error:?}"
9913 );
9914 assert!(matches!(
9915 swap.forwarding
9916 .lookup_data_route(cotenant, cotenant_channel, cotenant_epoch)
9917 .unwrap(),
9918 DataRoute::Client(DataRouteState::Bound(_))
9919 ));
9920 assert_eq!(module_loop_error, None);
9921 assert!(swap
9922 .registry
9923 .get_module_by_connection(INCUMBENT)
9924 .unwrap()
9925 .is_some());
9926
9927 let goodbye = tokio::time::timeout(Duration::from_secs(1), swap.incumbent_rx.recv())
9929 .await
9930 .expect("the incumbent is told to drop the abandoned binding")
9931 .unwrap()
9932 .frame;
9933 assert_eq!(goodbye.header.ty, FrameType::Goodbye);
9934 assert_eq!(goodbye.header.channel, abandoned_channel);
9935 assert_eq!(goodbye.header.epoch, abandoned_epoch);
9936 assert!(swap.incumbent_rx.try_recv().is_err());
9937
9938 let response = caller_task.await.unwrap();
9940 assert_eq!(response.len(), 1);
9941 assert_eq!(parse_error(&response[0])["code"], "module_reloading");
9942 assert!(caller_rx.try_recv().is_err());
9943
9944 assert_eq!(swap.forwarding.reserved_route_count().unwrap(), (0, 0));
9948 let counters = handler.counters().snapshot();
9949 assert_eq!(
9950 keyed_total(&counters, "route_open_accepted_by_principal"),
9951 1
9952 );
9953 assert_eq!(keyed_total(&counters, "route_open_refused_by_code"), 1);
9954 assert_eq!(counters["route_open_refused_by_code"]["module_rejected"], 1);
9955 }
9956
9957 #[tokio::test]
9961 async fn route_open_after_cutover_and_incumbent_drain_is_relayed_to_the_candidate() {
9962 let mut swap = swap_with_incumbent().await;
9963 register_candidate(&swap, None);
9964 let incumbent = cutover(&swap);
9965 swap.forwarding
9966 .begin_endpoint_drain(incumbent, RouteCloseReason::Restart)
9967 .unwrap()
9968 .expect("the incumbent is still registered");
9969
9970 let client = ConnectionId::new(33);
9971 let (client_ctx, mut client_rx) = route_ctx(client);
9972 let route_handler = swap.handler.clone();
9973 let open_ctx = RouteCtx {
9974 connection_id: client,
9975 egress: client_ctx.egress.clone(),
9976 };
9977 let mut route_task = tokio::spawn(async move {
9978 route_handler
9979 .handle_control_frame(
9980 &open_ctx,
9981 route_open_frame(90, "aft", unique_project_root("swap-after-drain")),
9982 )
9983 .await
9984 .unwrap()
9985 });
9986 let bind = tokio::select! {
9987 bind = swap.candidate_rx.recv() => bind.expect("candidate egress is open").frame,
9988 response = &mut route_task => {
9989 let response = response.unwrap();
9990 panic!(
9991 "post-cutover route.open was refused instead of relayed to the candidate: {}",
9992 parse_error(&response[0])["code"]
9993 );
9994 }
9995 };
9996 swap.handler
9997 .handle_control_frame(&swap.candidate_ctx, route_bind_ack(bind.header.corr))
9998 .await
9999 .unwrap();
10000 assert!(route_task.await.unwrap().is_empty());
10001 let (channel, epoch) = published_route(&client_rx.recv().await.unwrap());
10002 match swap
10003 .forwarding
10004 .lookup_data_route(client, channel, epoch)
10005 .unwrap()
10006 {
10007 DataRoute::Client(DataRouteState::Bound(route)) => {
10008 assert_eq!(route.module_endpoint.connection_id, CANDIDATE)
10009 }
10010 other => panic!("expected a bound route on the candidate, got {other:?}"),
10011 }
10012 assert!(swap.incumbent_rx.try_recv().is_err());
10013 }
10014
10015 #[tokio::test]
10020 async fn candidate_catalog_update_ready_reaches_the_candidate_registration() {
10021 let swap = swap_with_incumbent().await;
10022 register_candidate(&swap, Some(false));
10023 let update = Frame::build(
10024 FrameType::Request,
10025 control_flags(),
10026 0,
10027 0,
10028 55,
10029 serde_json::to_vec(&ModuleControlRequestFromModule::CatalogUpdate {
10030 provides: manifest("aft", PROTOCOL_VERSION).provides,
10031 capabilities: None,
10032 ready: Some(true),
10033 })
10034 .unwrap(),
10035 )
10036 .unwrap();
10037
10038 let replies = swap
10039 .handler
10040 .handle_control_frame(&swap.candidate_ctx, update)
10041 .await
10042 .unwrap();
10043
10044 assert_eq!(replies.len(), 1);
10045 assert_eq!(
10046 replies[0].header.ty,
10047 FrameType::Response,
10048 "candidate catalog.update was refused: {:?}",
10049 serde_json::from_slice::<Value>(&replies[0].body).ok()
10050 );
10051 assert!(swap.registry.get_candidate("aft").unwrap().unwrap().ready);
10052 assert_eq!(
10053 swap.registry
10054 .get_module("aft")
10055 .unwrap()
10056 .unwrap()
10057 .connection_id,
10058 INCUMBENT
10059 );
10060 }
10061 }
10062
10063 mod swap_admission {
10067 use super::*;
10068
10069 const INCUMBENT_NONCE: &str = "incumbent-nonce";
10070 const CANDIDATE_NONCE: &str = "candidate-nonce";
10071
10072 fn handler_with_incumbent(
10073 module_id: &str,
10074 reserved: bool,
10075 ) -> (Arc<Registry>, SupervisorHandle, ControlHandler) {
10076 let registry = Arc::new(Registry::default());
10077 let supervisor = SupervisorHandle::new();
10078 supervisor.set_spawn_nonce(module_id, INCUMBENT_NONCE.to_string());
10079 if reserved {
10080 supervisor.set_reserved_nonce(module_id, INCUMBENT_NONCE.to_string());
10081 }
10082 let handler =
10083 ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor.clone());
10084 let incumbent = handler
10085 .handle_control(
10086 ConnectionId::new(1),
10087 hello_frame_with_nonce(module_id, PROTOCOL_VERSION, 1, Some(INCUMBENT_NONCE)),
10088 )
10089 .unwrap();
10090 assert_eq!(incumbent[0].header.ty, FrameType::HelloAck);
10091 supervisor.open_swap(module_id, CANDIDATE_NONCE.to_string());
10092 (registry, supervisor, handler)
10093 }
10094
10095 #[test]
10101 fn unminted_nonce_on_an_unreserved_id_with_an_open_swap_is_refused() {
10102 let (registry, _supervisor, handler) = handler_with_incumbent("aft", false);
10103
10104 for (connection, nonce) in [(2, Some("forged")), (3, None)] {
10105 let replies = handler
10106 .handle_control(
10107 ConnectionId::new(connection),
10108 hello_frame_with_nonce("aft", PROTOCOL_VERSION, connection, nonce),
10109 )
10110 .unwrap();
10111 assert_eq!(replies[0].header.ty, FrameType::Error);
10112 assert_eq!(
10113 parse_error(&replies[0])["code"],
10114 "swap_token_invalid",
10115 "nonce {nonce:?}"
10116 );
10117 }
10118 assert!(registry.get_candidate("aft").unwrap().is_none());
10119 assert_eq!(
10120 registry.get_module("aft").unwrap().unwrap().connection_id,
10121 ConnectionId::new(1)
10122 );
10123
10124 let admitted = handler
10127 .handle_control(
10128 ConnectionId::new(4),
10129 hello_frame_with_nonce("aft", PROTOCOL_VERSION, 4, Some(CANDIDATE_NONCE)),
10130 )
10131 .unwrap();
10132 assert_eq!(admitted[0].header.ty, FrameType::HelloAck);
10133 assert_eq!(
10134 registry
10135 .get_candidate("aft")
10136 .unwrap()
10137 .unwrap()
10138 .connection_id,
10139 ConnectionId::new(4)
10140 );
10141 assert_eq!(
10142 registry.get_module("aft").unwrap().unwrap().connection_id,
10143 ConnectionId::new(1),
10144 "the candidate must not take the active slot"
10145 );
10146 let replayed = handler
10147 .handle_control(
10148 ConnectionId::new(5),
10149 hello_frame_with_nonce("aft", PROTOCOL_VERSION, 5, Some(CANDIDATE_NONCE)),
10150 )
10151 .unwrap();
10152 assert_eq!(parse_error(&replayed[0])["code"], "swap_token_invalid");
10153
10154 handler.cleanup_connection(ConnectionId::new(1)).unwrap();
10158 let squatter = handler
10159 .handle_control(
10160 ConnectionId::new(6),
10161 hello_frame_with_nonce("aft", PROTOCOL_VERSION, 6, Some("forged")),
10162 )
10163 .unwrap();
10164 assert_eq!(parse_error(&squatter[0])["code"], "swap_token_invalid");
10165 assert!(
10166 registry.get_module("aft").unwrap().is_none(),
10167 "a squatter took the active slot of an id being swapped"
10168 );
10169 }
10170
10171 #[test]
10176 fn reserved_module_candidate_is_admitted_ahead_of_the_reserved_gate() {
10177 let (registry, _supervisor, handler) = handler_with_incumbent("vault", true);
10178
10179 let replies = handler
10180 .handle_control(
10181 ConnectionId::new(2),
10182 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 2, Some(CANDIDATE_NONCE)),
10183 )
10184 .unwrap();
10185
10186 assert_eq!(
10187 replies[0].header.ty,
10188 FrameType::HelloAck,
10189 "reserved candidate refused: {:?}",
10190 serde_json::from_slice::<Value>(&replies[0].body).ok()
10191 );
10192 assert_eq!(
10193 registry
10194 .get_candidate("vault")
10195 .unwrap()
10196 .unwrap()
10197 .connection_id,
10198 ConnectionId::new(2)
10199 );
10200 }
10201
10202 #[test]
10205 fn without_an_open_swap_the_ordinary_gates_decide() {
10206 let (registry, supervisor, handler) = handler_with_incumbent("vault", true);
10207 supervisor.close_swap("vault");
10208
10209 let candidate = handler
10210 .handle_control(
10211 ConnectionId::new(2),
10212 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 2, Some(CANDIDATE_NONCE)),
10213 )
10214 .unwrap();
10215 assert_eq!(parse_error(&candidate[0])["code"], "reserved_module");
10216 let duplicate = handler
10217 .handle_control(
10218 ConnectionId::new(3),
10219 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 3, Some(INCUMBENT_NONCE)),
10220 )
10221 .unwrap();
10222 assert_eq!(parse_error(&duplicate[0])["code"], "duplicate_module_id");
10223 assert!(registry.get_candidate("vault").unwrap().is_none());
10224 }
10225 }
10226}
10227
10228#[cfg(test)]
10229mod concurrency_default_exposure_tests {
10230 use super::*;
10231
10232 fn hello_body(role_json: &str) -> Vec<u8> {
10233 format!(
10234 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":[]}}}}}}}}"#
10235 )
10236 .into_bytes()
10237 }
10238
10239 fn manifest_from(body: &[u8]) -> ModuleManifest {
10240 let value: serde_json::Value = serde_json::from_slice(body).expect("hello parses");
10241 serde_json::from_value(value.get("manifest").expect("manifest key").clone())
10242 .expect("manifest parses")
10243 }
10244
10245 const SURFACE_TAIL: &str = r#""operations":[],"config_schema":{"type":"object"},"observability":[],"identity_scope":[]"#;
10246
10247 #[test]
10248 fn absent_concurrency_on_management_surface_is_reported_as_defaulted() {
10249 let body = hello_body(&format!(
10250 r#"{{"role":"management_surface",{SURFACE_TAIL}}}"#
10251 ));
10252 let manifest = manifest_from(&body);
10253 assert_eq!(manifest_concurrency(&manifest), Concurrency::ModuleManaged);
10256 assert!(manifest_concurrency_was_defaulted(&body, &manifest));
10257 }
10258
10259 #[test]
10260 fn declared_concurrency_is_not_reported_even_when_it_equals_the_default() {
10261 let body = hello_body(&format!(
10262 r#"{{"role":"management_surface",{SURFACE_TAIL},"concurrency":"module_managed"}}"#
10263 ));
10264 let manifest = manifest_from(&body);
10265 assert_eq!(manifest_concurrency(&manifest), Concurrency::ModuleManaged);
10266 assert!(!manifest_concurrency_was_defaulted(&body, &manifest));
10267 }
10268
10269 #[test]
10270 fn non_management_roles_are_never_reported() {
10271 let body = hello_body(
10272 r#"{"role":"internal_service","service_id":"s","transport":"bulk","agent_facing":false,"operations":[]}"#,
10273 );
10274 let manifest = manifest_from(&body);
10275 assert!(!manifest_concurrency_was_defaulted(&body, &manifest));
10276 }
10277}