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 let hello_ack = self.build_hello_ack(&frame, negotiated_ver, &hello.manifest.module_id)?;
1739 if swap_candidate {
1740 return self.register_swap_candidate(
1741 connection_id,
1742 sink,
1743 &frame,
1744 hello.manifest,
1745 negotiated_ver,
1746 control_ops,
1747 hello_ack,
1748 );
1749 }
1750 let registration = match self.registry.register_with_control_ops(
1751 hello.manifest,
1752 negotiated_ver,
1753 connection_id,
1754 control_ops,
1755 ) {
1756 Ok(registration) => registration,
1757 Err(RegistryError::DuplicateModuleId { module_id }) => {
1758 return Ok(vec![control_error_frame(
1759 &frame,
1760 "duplicate_module_id",
1761 format!(
1762 "module_id '{module_id}' is already registered; duplicate HELLO rejected"
1763 ),
1764 )?])
1765 }
1766 Err(err @ RegistryError::PathHazardModuleId { .. }) => {
1767 return Ok(vec![control_error_frame(
1768 &frame,
1769 "invalid_module_id",
1770 err.to_string(),
1771 )?])
1772 }
1773 Err(err) => {
1774 return Ok(vec![control_error_frame(
1775 &frame,
1776 "registry_error",
1777 err.to_string(),
1778 )?])
1779 }
1780 };
1781
1782 let reply = if let Some(sink) = sink {
1783 let concurrency = manifest_concurrency(®istration.manifest);
1799 if let Err(err) = self.forwarding.register_module_connection_acked(
1800 connection_id,
1801 registration.manifest.module_id.clone(),
1802 negotiated_ver,
1803 concurrency,
1804 sink,
1805 hello_ack,
1806 ) {
1807 if matches!(self.deregister_connection(connection_id), Ok(r) if !r.is_empty()) {
1811 crate::supervise::notify_registration_release();
1812 }
1813 return Ok(vec![control_error_frame(
1814 &frame,
1815 forwarding_error_code(&err),
1816 err.to_string(),
1817 )?]);
1818 }
1819 Vec::new()
1820 } else {
1821 vec![hello_ack]
1824 };
1825
1826 if manifest_concurrency_was_defaulted(&frame.body, ®istration.manifest) {
1835 info!(
1836 module_id = %registration.manifest.module_id,
1837 "management surface registered with DEFAULTED concurrency=module_managed (manifest predates the field; declare the real lane)"
1838 );
1839 }
1840
1841 self.apply_registration_capabilities(®istration);
1842
1843 info!(
1844 module_id = %registration.manifest.module_id,
1845 module_version = %registration.manifest.module_version,
1846 negotiated_ver,
1847 routable_provider = manifest_provides_routable_role(®istration.manifest),
1848 connection_id = connection_id.get(),
1849 "module registered"
1850 );
1851
1852 Ok(reply)
1853 }
1854
1855 #[allow(clippy::too_many_arguments)]
1865 fn register_swap_candidate(
1866 &self,
1867 connection_id: ConnectionId,
1868 sink: Option<crate::FrameSink>,
1869 frame: &Frame,
1870 manifest: ModuleManifest,
1871 negotiated_ver: u8,
1872 control_ops: Vec<String>,
1873 hello_ack: Frame,
1874 ) -> Result<Vec<Frame>, RouterError> {
1875 let module_id = manifest.module_id.clone();
1876 let registration = match self.registry.register_candidate_with_control_ops(
1877 manifest,
1878 negotiated_ver,
1879 connection_id,
1880 control_ops,
1881 ) {
1882 Ok(registration) => registration,
1883 Err(RegistryError::DuplicateModuleId { module_id }) => {
1884 return Ok(vec![control_error_frame(
1885 frame,
1886 "duplicate_module_id",
1887 format!(
1888 "module_id '{module_id}' already has a swap candidate registered; duplicate HELLO rejected"
1889 ),
1890 )?])
1891 }
1892 Err(err @ RegistryError::PathHazardModuleId { .. }) => {
1893 return Ok(vec![control_error_frame(
1894 frame,
1895 "invalid_module_id",
1896 err.to_string(),
1897 )?])
1898 }
1899 Err(err) => {
1900 return Ok(vec![control_error_frame(
1901 frame,
1902 "registry_error",
1903 err.to_string(),
1904 )?])
1905 }
1906 };
1907 let reply = if let Some(sink) = sink {
1908 let concurrency = manifest_concurrency(®istration.manifest);
1912 if let Err(err) = self.forwarding.register_candidate_module_connection_acked(
1913 connection_id,
1914 module_id.clone(),
1915 negotiated_ver,
1916 concurrency,
1917 sink,
1918 hello_ack,
1919 ) {
1920 if matches!(self.deregister_connection(connection_id), Ok(r) if !r.is_empty()) {
1921 crate::supervise::notify_registration_release();
1922 }
1923 return Ok(vec![control_error_frame(
1924 frame,
1925 forwarding_error_code(&err),
1926 err.to_string(),
1927 )?]);
1928 }
1929 Vec::new()
1930 } else {
1931 vec![hello_ack]
1932 };
1933 self.supervisor.mark_swap_candidate_admitted(&module_id);
1934 info!(
1935 module_id = %module_id,
1936 module_version = %registration.manifest.module_version,
1937 negotiated_ver,
1938 ready = registration.ready,
1939 connection_id = connection_id.get(),
1940 "swap candidate registered; not routable until cutover"
1941 );
1942 Ok(reply)
1943 }
1944
1945 fn build_hello_ack(
1946 &self,
1947 frame: &Frame,
1948 negotiated_ver: u8,
1949 module_id: &str,
1950 ) -> Result<Frame, RouterError> {
1951 let ack = ModuleHelloAckBody {
1952 negotiated_ver,
1953 subc_ops: module_subc_ops(),
1954 subc_capabilities: self.subc_capabilities.as_ref().to_vec(),
1955 storage: self
1956 .storage_config
1957 .as_ref()
1958 .map(|cfg| cfg.descriptor_for(module_id)),
1959 machine_id: self.machine_id.as_ref().map(|id| id.as_str().to_owned()),
1960 };
1961 let body = serde_json::to_vec(&ack).map_err(|err| {
1962 RouterError::backend(
1963 0,
1964 frame.header.corr,
1965 format!("failed to encode HELLO_ACK: {err}"),
1966 )
1967 })?;
1968
1969 Frame::build_with_version(
1970 negotiated_ver,
1971 FrameType::HelloAck,
1972 control_flags(),
1973 0,
1974 0,
1975 frame.header.corr,
1976 body,
1977 )
1978 .map_err(RouterError::FrameBuild)
1979 }
1980
1981 async fn handle_client_control_request(
1982 &self,
1983 ctx: &RouteCtx,
1984 frame: Frame,
1985 request: ClientControlRequest,
1986 ) -> Result<Vec<Frame>, RouterError> {
1987 match request {
1988 ClientControlRequest::ServerDescribe {} => self.handle_server_describe(frame),
1989 ClientControlRequest::CatalogList { module_id } => {
1990 self.handle_catalog_list(frame, module_id)
1991 }
1992 ClientControlRequest::RouteOpen {
1993 target,
1994 identity,
1995 consumer_identity,
1996 consumer_capabilities,
1997 admission_facts,
1998 } => {
1999 self.handle_route_open(
2000 ctx,
2001 frame,
2002 RouteOpenRequest {
2003 target,
2004 identity,
2005 consumer_identity,
2006 consumer_capabilities,
2007 admission_facts,
2008 },
2009 )
2010 .await
2011 }
2012 ClientControlRequest::RoutePoll {
2013 route_channel,
2014 route_epoch,
2015 kind,
2016 } => self.handle_route_poll(ctx, frame, route_channel, route_epoch, kind),
2017 ClientControlRequest::SupervisorList {} => self.handle_supervisor_list(frame),
2018 ClientControlRequest::SupervisorSpawnSnapshot {} => {
2019 self.handle_supervisor_spawn_snapshot(frame)
2020 }
2021 ClientControlRequest::SupervisorSpawnSubscribe { since } => {
2022 self.handle_supervisor_spawn_subscribe(ctx, frame, since)
2023 }
2024 ClientControlRequest::SupervisorRestart {
2025 module_id,
2026 drain_timeout_ms,
2027 } => {
2028 self.handle_supervisor_restart(frame, module_id, drain_timeout_ms)
2029 .await
2030 }
2031 ClientControlRequest::SupervisorSwap {
2032 module_id,
2033 ready_timeout_ms,
2034 } => {
2035 self.handle_supervisor_swap(frame, module_id, ready_timeout_ms)
2036 .await
2037 }
2038 ClientControlRequest::SupervisorReload { module_id } => {
2039 self.handle_supervisor_reload(frame, module_id).await
2040 }
2041 ClientControlRequest::SupervisorRescan { preview } => {
2042 self.handle_supervisor_rescan(frame, preview).await
2043 }
2044 ClientControlRequest::SupervisorReleaseReserved { module_id } => {
2045 self.handle_supervisor_release_reserved(frame, module_id)
2046 .await
2047 }
2048 ClientControlRequest::SupervisorSetEnabled { module_id, enabled } => {
2049 self.handle_supervisor_set_enabled(frame, module_id, enabled)
2050 .await
2051 }
2052 ClientControlRequest::SupervisorHealthProbe { module_id } => {
2053 self.handle_supervisor_health_probe(frame, module_id).await
2054 }
2055 ClientControlRequest::SupervisorHealth {} => self.handle_supervisor_health(frame),
2056 ClientControlRequest::SupervisorRoutes { module_id } => {
2057 self.handle_supervisor_routes(frame, module_id)
2058 }
2059 ClientControlRequest::SupervisorProvenance { module_id } => {
2060 self.handle_supervisor_provenance(frame, module_id).await
2061 }
2062 ClientControlRequest::SupervisorStderrTail {
2063 module_id,
2064 max_lines,
2065 max_bytes,
2066 } => self.handle_supervisor_stderr_tail(frame, module_id, max_lines, max_bytes),
2067 ClientControlRequest::SupervisorTerminals { module_id } => {
2068 self.handle_supervisor_terminals(frame, module_id).await
2069 }
2070 }
2071 }
2072
2073 fn handle_module_control_request(
2074 &self,
2075 connection_id: ConnectionId,
2076 frame: Frame,
2077 request: ModuleControlRequestFromModule,
2078 ) -> Result<Vec<Frame>, RouterError> {
2079 match request {
2080 ModuleControlRequestFromModule::CatalogUpdate {
2081 provides,
2082 capabilities,
2083 ready,
2084 } => self.handle_catalog_update(connection_id, frame, provides, capabilities, ready),
2085 ModuleControlRequestFromModule::LiveRoots {} => {
2086 let registered = self
2087 .registry
2088 .get_module_by_connection(connection_id)
2089 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
2090 let Some(registration) = registered else {
2091 return Ok(vec![control_error_frame(&frame, "not_registered", "supervisor.live_roots requires an active module registration owned by this connection")?]);
2092 };
2093 let response = self
2094 .forwarding
2095 .live_roots(®istration.manifest.module_id)
2096 .map_err(RouterError::Forwarding)?;
2097 Ok(vec![control_response_body_frame(
2098 &frame,
2099 &response,
2100 "ModuleControlResponseToModule::LiveRoots",
2101 )?])
2102 }
2103 }
2104 }
2105
2106 fn handle_catalog_update(
2107 &self,
2108 connection_id: ConnectionId,
2109 frame: Frame,
2110 provides: Vec<ProviderRole>,
2111 capabilities: Option<CapabilityDeclarations>,
2112 ready: Option<bool>,
2113 ) -> Result<Vec<Frame>, RouterError> {
2114 self.refresh_capability_requirements();
2115 let Some(registration) = self
2116 .registry
2117 .get_module_by_connection(connection_id)
2118 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
2119 else {
2120 return Ok(vec![control_error_frame(
2121 &frame,
2122 "not_registered",
2123 "catalog.update requires an active module registration owned by this connection",
2124 )?]);
2125 };
2126
2127 if let Some(message) =
2128 catalog_update_frozen_field_message(®istration.manifest, &provides)
2129 {
2130 return Ok(vec![control_error_frame(
2131 &frame,
2132 "catalog_update_frozen_field",
2133 message,
2134 )?]);
2135 }
2136
2137 let mut candidate = registration.manifest.clone();
2138 candidate.provides = provides.clone();
2139 candidate.capabilities = capabilities
2140 .clone()
2141 .or_else(|| registration.manifest.capabilities.clone());
2142 if let Err(err) = candidate.validate_capability_grammar() {
2143 return Ok(vec![control_error_frame(
2144 &frame,
2145 "invalid_capability_grammar",
2146 err.to_string(),
2147 )?]);
2148 }
2149
2150 let updated = self
2151 .registry
2152 .replace_catalog_for_connection(connection_id, provides, capabilities, ready)
2153 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
2154 if updated.is_none() {
2155 return Ok(vec![control_error_frame(
2156 &frame,
2157 "not_registered",
2158 "catalog.update requires an active module registration owned by this connection",
2159 )?]);
2160 }
2161 if let Ok((_, registrations)) = self.runtime_capability_snapshot() {
2162 log_duplicate_claim_events(
2163 self.capability_evaluator
2164 .duplicate_claims(DuplicateClaimSource::CatalogUpdate, ®istrations),
2165 );
2166 }
2167 if capability_census_trigger(
2168 registration.manifest.capabilities.as_ref(),
2169 updated
2170 .as_ref()
2171 .and_then(|entry| entry.manifest.capabilities.as_ref()),
2172 ) {
2173 self.enforce_capability_denies();
2174 }
2175 self.refresh_capability_requirements();
2176
2177 let response = ModuleControlResponseToModule::CatalogUpdate {};
2178 control_response_body_frame(
2179 &frame,
2180 &response,
2181 "ModuleControlResponseToModule::CatalogUpdate",
2182 )
2183 .map(|frame| vec![frame])
2184 }
2185
2186 fn handle_server_describe(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
2187 self.refresh_capability_requirements();
2188 let mut counters = self.counters.snapshot();
2194 if let (Ok((connections_with_routes, max)), Some(obj)) = (
2195 self.forwarding.client_route_concentration(),
2196 counters.as_object_mut(),
2197 ) {
2198 obj.insert(
2199 "client_connections_with_routes".into(),
2200 connections_with_routes.into(),
2201 );
2202 obj.insert("max_routes_on_one_connection".into(), max.into());
2203 }
2204 if let (Some(open_breakers), Some(obj)) = (
2210 self.route_bind_breakers.open_snapshot(),
2211 counters.as_object_mut(),
2212 ) {
2213 obj.insert("route_bind_breakers_open".into(), open_breakers);
2214 }
2215 let response = ClientControlResponse::ServerDescribe {
2216 protocol_ver: PROTOCOL_VERSION,
2217 subc_ops: subc_ops(),
2218 capabilities: self.subc_capabilities.as_ref().to_vec(),
2219 connected_clients: self.connected_clients.count(),
2220 counters: Some(counters),
2221 build_git_sha: Some(env!("SUBC_BUILD_GIT_SHA").to_string()),
2222 build_lock_digest: Some(env!("SUBC_BUILD_LOCK_DIGEST").to_string()),
2223 capability_requirements: self.capability_requirement_statuses(),
2224 machine_id: self.machine_id.as_ref().map(|id| id.as_str().to_owned()),
2225 };
2226 Ok(vec![control_response_body_frame(
2227 &frame,
2228 &response,
2229 "ClientControlResponse::ServerDescribe",
2230 )?])
2231 }
2232
2233 fn handle_catalog_list(
2234 &self,
2235 frame: Frame,
2236 module_id: Option<String>,
2237 ) -> Result<Vec<Frame>, RouterError> {
2238 let (generation, modules) = self.registry.list_modules().map_err(|err| {
2239 RouterError::backend(0, frame.header.corr, format!("registry error: {err}"))
2240 })?;
2241 let entries = modules
2242 .into_iter()
2243 .filter(|registration| {
2244 module_id
2245 .as_deref()
2246 .map(|wanted| registration.manifest.module_id == wanted)
2247 .unwrap_or(true)
2248 })
2249 .map(|registration| {
2250 let not_ready = self.not_ready_reason(®istration);
2251 let roles = registration.manifest.provides;
2252 CatalogEntry {
2253 module_id: registration.manifest.module_id,
2254 ready: not_ready.is_none(),
2255 not_ready,
2256 module_version: Some(registration.manifest.module_version),
2257 roles,
2258 control_ops: registration.control_ops,
2259 capabilities: registration.manifest.capabilities,
2260 self_signals: registration.manifest.self_signals,
2261 }
2262 })
2263 .collect();
2264 let response = ClientControlResponse::CatalogList {
2265 generation,
2266 modules: entries,
2267 subc_ops: subc_ops(),
2268 };
2269 Ok(vec![control_response_body_frame(
2270 &frame,
2271 &response,
2272 "ClientControlResponse::CatalogList",
2273 )?])
2274 }
2275
2276 fn route_open_principal(
2277 &self,
2278 frame: &Frame,
2279 consumer_identity: Option<ConsumerIdentity>,
2280 ) -> Result<Result<Principal, Frame>, RouterError> {
2281 let Some(consumer_identity) = consumer_identity else {
2282 return Ok(Ok(Principal::Direct));
2283 };
2284
2285 if self.supervisor.spawned_consumer_authorized(
2286 &consumer_identity.module_id,
2287 &consumer_identity.launch_nonce,
2288 ) {
2289 return Ok(Ok(Principal::Reserved {
2290 module_id: consumer_identity.module_id,
2291 }));
2292 }
2293
2294 Ok(Err(control_error_frame(
2295 frame,
2296 "bad_consumer_identity",
2297 format!(
2298 "consumer_identity for module_id '{}' did not match a supervised launch nonce",
2299 consumer_identity.module_id
2300 ),
2301 )?))
2302 }
2303
2304 fn route_open_refusal_frame(
2310 &self,
2311 ctx: &RouteCtx,
2312 frame: &Frame,
2313 module_id: &str,
2314 reason: &'static str,
2315 code: &'static str,
2316 message: impl Into<String>,
2317 ) -> Result<Frame, RouterError> {
2318 self.observe_route_open_refusal(ctx, module_id, reason, code);
2319 control_error_frame(frame, code, message.into())
2320 }
2321
2322 fn route_open_breaker_refusal_frame(
2340 &self,
2341 ctx: &RouteCtx,
2342 frame: &Frame,
2343 module_id: &str,
2344 consecutive_timeouts: u32,
2345 retry_in: Duration,
2346 probe_in_flight: bool,
2347 ) -> Result<Frame, RouterError> {
2348 self.counters
2349 .increment_route_open_refused(crate::observability::ROUTE_OPEN_REFUSED_BREAKER_OPEN);
2350 debug!(
2351 target: "control",
2352 code = "module_timeout",
2353 module_id = ?module_id,
2354 connection_id = ctx.connection_id.get(),
2355 consecutive_timeouts,
2356 retry_in_ms = retry_in.as_millis() as u64,
2357 probe_in_flight,
2358 "route.open refused by open bind-relay breaker"
2359 );
2360 let detail = if probe_in_flight {
2361 "one probe bind is already in flight; retry once it settles".to_string()
2362 } else {
2363 format!("not relaying for another {retry_in:?}")
2364 };
2365 control_error_frame(
2366 frame,
2367 "module_timeout",
2368 format!(
2369 "module_id '{module_id}' failed {consecutive_timeouts} consecutive route.bind \
2370 relays; {detail}"
2371 ),
2372 )
2373 }
2374
2375 fn observe_route_open_refusal(
2388 &self,
2389 ctx: &RouteCtx,
2390 module_id: &str,
2391 reason: &'static str,
2392 code: &'static str,
2393 ) {
2394 self.counters.increment_route_open_refused(code);
2395 info!(
2396 target: "control",
2397 code,
2398 reason,
2399 module_id = ?module_id,
2400 connection_id = ctx.connection_id.get(),
2401 "route.open refused"
2402 );
2403 }
2404
2405 fn observe_route_open_accept(&self, ctx: &RouteCtx, module_id: &str, principal: &str) {
2450 self.counters.increment_route_open_accepted(principal);
2451 info!(
2452 target: "control",
2453 principal,
2454 module_id,
2455 connection_id = ctx.connection_id.get(),
2456 "route.open accepted"
2457 );
2458 }
2459
2460 fn supervised_absent_route_open_refusal_frame(
2461 &self,
2462 ctx: &RouteCtx,
2463 frame: &Frame,
2464 module_id: &str,
2465 code: &'static str,
2466 status: &crate::supervise::ModuleStatus,
2467 ) -> Result<Frame, RouterError> {
2468 self.counters.increment_route_open_refused(code);
2469 info!(
2470 target: "control",
2471 code,
2472 reason = "supervised_not_registered",
2473 module_id = ?module_id,
2474 connection_id = ctx.connection_id.get(),
2475 state = %status.state,
2476 enabled = status.enabled,
2477 live = status.live,
2478 "route.open refused"
2479 );
2480 control_error_frame(
2481 frame,
2482 code,
2483 format!(
2484 "module_id '{module_id}' is supervised but not available (state={}, enabled={}, live={})",
2485 status.state, status.enabled, status.live
2486 ),
2487 )
2488 }
2489
2490 async fn handle_route_open(
2491 &self,
2492 ctx: &RouteCtx,
2493 frame: Frame,
2494 request: RouteOpenRequest,
2495 ) -> Result<Vec<Frame>, RouterError> {
2496 let RouteOpenRequest {
2497 target,
2498 mut identity,
2499 consumer_identity,
2500 consumer_capabilities,
2501 admission_facts,
2502 } = request;
2503 let target_module_id = target_module_id(&target).to_string();
2504 debug!(
2505 connection_id = ctx.connection_id.get(),
2506 corr = frame.header.corr,
2507 module_id = %target_module_id,
2508 "handling route.open"
2509 );
2510
2511 let Some(registration) = self
2530 .registry
2531 .get_module(&target_module_id)
2532 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
2533 else {
2534 if let Some((status, warming)) =
2535 self.supervisor_status(&target_module_id, frame.header.corr)?
2536 {
2537 if status.protocol == ModuleProtocol::None {
2544 return Ok(vec![self.route_open_refusal_frame(
2545 ctx,
2546 &frame,
2547 &target_module_id,
2548 "protocol_none",
2549 error_codes::MODULE_NO_PROTOCOL,
2550 format!(
2551 "module_id '{target_module_id}' is declared protocol: none; \
2552 it speaks no subc wire and serves no routes"
2553 ),
2554 )?]);
2555 }
2556 let code = if warming {
2557 "module_warming"
2558 } else {
2559 "target_unavailable"
2560 };
2561 return Ok(vec![self.supervised_absent_route_open_refusal_frame(
2562 ctx,
2563 &frame,
2564 &target_module_id,
2565 code,
2566 &status,
2567 )?]);
2568 }
2569 if let Some(removed_ago_ms) =
2570 self.supervisor.removal_tombstone_age_ms(&target_module_id)
2571 {
2572 return Ok(vec![self.route_open_refusal_frame(
2573 ctx,
2574 &frame,
2575 &target_module_id,
2576 "removed",
2577 error_codes::MODULE_REMOVED,
2578 format!("module_id '{target_module_id}' was removed {removed_ago_ms} ms ago"),
2579 )?]);
2580 }
2581 return Ok(vec![self.route_open_refusal_frame(
2582 ctx,
2583 &frame,
2584 &target_module_id,
2585 "not_registered",
2586 error_codes::UNKNOWN_MODULE,
2587 format!("module_id '{target_module_id}' is not registered"),
2588 )?]);
2589 };
2590
2591 if !registration.ready {
2595 self.counters
2596 .increment_route_open_refused(ROUTE_OPEN_REFUSED_DECLARED_NOT_READY);
2597 info!(
2598 target: "control",
2599 code = error_codes::MODULE_WARMING,
2600 module_id = ?target_module_id,
2601 connection_id = ctx.connection_id.get(),
2602 reason = "declared_not_ready",
2603 "route.open refused"
2604 );
2605 return Ok(vec![control_error_body_frame(
2606 &frame,
2607 ErrorBody {
2608 code: error_codes::MODULE_WARMING.to_string(),
2609 message: format!(
2610 "module_id '{target_module_id}' is registered and has declared itself not ready; retry"
2611 ),
2612 detail: Some(serde_json::json!({
2613 "reason": "declared_not_ready"
2614 })),
2615 },
2616 )?]);
2617 }
2618
2619 if let Some(capability) = self.first_unprovided_required_capability(®istration) {
2638 self.counters
2639 .increment_route_open_refused(ROUTE_OPEN_REFUSED_REQUIRED_CAPABILITY_UNPROVIDED);
2640 info!(
2641 target: "control",
2642 code = error_codes::MODULE_WARMING,
2643 module_id = ?target_module_id,
2644 connection_id = ctx.connection_id.get(),
2645 reason = NotReadyReason::REQUIRED_CAPABILITY_UNPROVIDED,
2646 capability = %capability,
2647 "route.open refused"
2648 );
2649 return Ok(vec![control_error_body_frame(
2650 &frame,
2651 ErrorBody {
2652 code: error_codes::MODULE_WARMING.to_string(),
2653 message: format!(
2654 "module_id '{target_module_id}' requires capability '{capability}', \
2655 which no registered module provides; retry"
2656 ),
2657 detail: Some(serde_json::json!({
2658 "reason": NotReadyReason::REQUIRED_CAPABILITY_UNPROVIDED,
2659 "capability": capability,
2660 })),
2661 },
2662 )?]);
2663 }
2664
2665 if !target_has_required_role(&target, ®istration.manifest.provides) {
2666 return Ok(vec![self.route_open_refusal_frame(
2667 ctx,
2668 &frame,
2669 &target_module_id,
2670 "role_not_provided",
2671 "target_unavailable",
2672 format!("module_id '{target_module_id}' does not provide the requested target"),
2673 )?]);
2674 }
2675
2676 if registration.state != ChannelState::Active {
2677 return Ok(vec![self.route_open_refusal_frame(
2678 ctx,
2679 &frame,
2680 &target_module_id,
2681 "registration_not_active",
2682 "target_unavailable",
2683 format!("module_id '{target_module_id}' is not active"),
2684 )?]);
2685 }
2686
2687 if self
2688 .forwarding
2689 .module_is_draining(&target_module_id)
2690 .map_err(RouterError::Forwarding)?
2691 {
2692 return Ok(vec![self.route_open_refusal_frame(
2693 ctx,
2694 &frame,
2695 &target_module_id,
2696 "reloading",
2697 "module_reloading",
2698 format!("module_id '{target_module_id}' is reloading"),
2699 )?]);
2700 }
2701
2702 if self
2703 .process_liveness
2704 .as_ref()
2705 .and_then(|process_liveness| process_liveness.process_live(&target_module_id))
2706 == Some(false)
2707 {
2708 return Ok(vec![self.route_open_refusal_frame(
2709 ctx,
2710 &frame,
2711 &target_module_id,
2712 "supervisor_not_live",
2713 "target_unavailable",
2714 format!("module_id '{target_module_id}' is not live"),
2715 )?]);
2716 }
2717
2718 if !self
2719 .forwarding
2720 .has_live_module_connection(&target_module_id)
2721 .map_err(RouterError::Forwarding)?
2722 {
2723 return Ok(vec![self.route_open_refusal_frame(
2724 ctx,
2725 &frame,
2726 &target_module_id,
2727 "no_forwarding_connection",
2728 "target_unavailable",
2729 format!("module_id '{target_module_id}' has no live forwarding connection"),
2730 )?]);
2731 }
2732
2733 if let Some(error) =
2734 self.guard_module_control_op(&frame, &target_module_id, "route.bind")?
2735 {
2736 self.observe_route_open_refusal(
2737 ctx,
2738 &target_module_id,
2739 "op_not_allowed",
2740 "op_not_allowed",
2741 );
2742 return Ok(vec![error]);
2743 }
2744
2745 let principal = match self.route_open_principal(&frame, consumer_identity)? {
2746 Ok(principal) => principal,
2747 Err(error) => {
2748 self.observe_route_open_refusal(
2749 ctx,
2750 &target_module_id,
2751 "bad_consumer_identity",
2752 "bad_consumer_identity",
2753 );
2754 return Ok(vec![error]);
2755 }
2756 };
2757
2758 if let Principal::Reserved {
2762 module_id: opening_module_id,
2763 } = &principal
2764 {
2765 if let Some(opening_registration) = self
2766 .registry
2767 .get_module(opening_module_id)
2768 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
2769 {
2770 if let Some(capability) =
2771 denied_capability(&opening_registration.manifest, ®istration.manifest)
2772 {
2773 warn!(
2774 opening_module_id,
2775 target_module_id,
2776 capability,
2777 "refusing route.open because an attested capability deny edge matches"
2778 );
2779 return Ok(vec![self.route_open_refusal_frame(
2780 ctx,
2781 &frame,
2782 &target_module_id,
2783 "capability_deny_edge",
2784 "capability_forbidden",
2785 format!(
2786 "module_id '{opening_module_id}' must never reach capability '{capability}' provided by '{target_module_id}'"
2787 ),
2788 )?]);
2789 }
2790 }
2791 }
2792
2793 if admission_facts.is_some() {
2794 let carrier_matches = matches!(
2795 &principal,
2796 Principal::Reserved { module_id }
2797 if self.admission_facts_carrier_module_id.as_deref() == Some(module_id)
2798 );
2799 if !carrier_matches {
2800 return Ok(vec![self.route_open_refusal_frame(
2801 ctx,
2802 &frame,
2803 &target_module_id,
2804 "admission_facts_carrier_not_permitted",
2805 "admission_facts_not_permitted",
2806 "admission facts may only be carried by the configured reserved module",
2807 )?]);
2808 }
2809
2810 let target_allowed = self
2811 .admission_facts_targets
2812 .as_ref()
2813 .is_some_and(|targets| targets.iter().any(|id| id == &target_module_id));
2814 if !target_allowed {
2815 return Ok(vec![self.route_open_refusal_frame(
2816 ctx,
2817 &frame,
2818 &target_module_id,
2819 "admission_facts_target_not_listed",
2820 "admission_facts_target_not_allowed",
2821 format!(
2822 "admission facts are not permitted for target module_id '{target_module_id}'"
2823 ),
2824 )?]);
2825 }
2826
2827 }
2831
2832 let project_root = match ProjectRootId::from_path_allowing_missing(&identity.project_root) {
2854 Ok(project_root) => project_root,
2855 Err(err) => {
2856 return Ok(vec![control_error_frame(
2857 &frame,
2858 "invalid_project_root",
2859 err.to_string(),
2860 )?])
2861 }
2862 };
2863 identity.project_root = project_root.as_path().to_path_buf();
2864
2865 let _concurrency_guard = match self
2876 .route_bind_concurrency
2877 .try_admit(&target_module_id, MAX_PENDING_ROUTE_BINDS_PER_TARGET)
2878 {
2879 Ok(guard) => guard,
2880 Err(in_flight) => {
2881 return Ok(vec![self.route_open_admission_refusal_frame(
2882 ctx,
2883 &frame,
2884 &target_module_id,
2885 format!(
2886 "module_id '{target_module_id}' already has {in_flight} route.bind relays in flight; retry after one settles"
2887 ),
2888 )?]);
2889 }
2890 };
2891
2892 let mut breaker = match self.route_bind_breakers.admit(&target_module_id) {
2895 RouteBindAdmission::Admitted { guard, probe } => {
2896 if probe {
2897 info!(
2898 module_id = %target_module_id,
2899 connection_id = ctx.connection_id.get(),
2900 "route.bind breaker half-open: admitting one probe"
2901 );
2902 }
2903 guard
2904 }
2905 RouteBindAdmission::Refused {
2906 consecutive_timeouts,
2907 retry_in,
2908 probe_in_flight,
2909 } => {
2910 return Ok(vec![self.route_open_breaker_refusal_frame(
2911 ctx,
2912 &frame,
2913 &target_module_id,
2914 consecutive_timeouts,
2915 retry_in,
2916 probe_in_flight,
2917 )?]);
2918 }
2919 };
2920
2921 let route_bind_relay_timeout = self.route_bind_relay_timeout_for(&target_module_id);
2926 let relay_deadline = Instant::now() + route_bind_relay_timeout;
2927 let pending = match self
2928 .forwarding
2929 .begin_route_bind_relay_for(
2930 ctx.connection_id,
2931 ctx.egress.clone(),
2932 response_version(&frame),
2933 frame.header.corr,
2934 &target_module_id,
2935 principal.clone(),
2936 Some(project_root),
2937 relay_deadline,
2938 )
2939 .await
2940 {
2941 Ok(pending) => pending,
2942 Err(err) => {
2943 return Ok(vec![self.route_open_refusal_frame(
2944 ctx,
2945 &frame,
2946 &target_module_id,
2947 "relay_reservation_failed",
2948 forwarding_error_code(&err),
2949 err.to_string(),
2950 )?])
2951 }
2952 };
2953 let crate::forwarding::PendingRouteBindRelay {
2954 endpoint,
2955 module_sink,
2956 negotiated_ver,
2957 client_channel,
2958 client_epoch,
2959 module_channel,
2960 module_epoch,
2961 corr: relay_corr,
2962 receiver,
2963 } = pending;
2964 let mut reservation =
2965 RouteBindReservationGuard::new(Arc::clone(&self.forwarding), endpoint, relay_corr);
2966
2967 debug!(
2968 connection_id = ctx.connection_id.get(),
2969 client_channel,
2970 client_epoch,
2971 module_channel,
2972 module_epoch,
2973 "reserved route handle pair"
2974 );
2975 let principal_label = match &principal {
2978 Principal::Reserved { module_id } => format!("reserved:{module_id}"),
2979 Principal::Direct => "direct".to_string(),
2980 other => format!("{other:?}"),
2981 };
2982 let relay = ModuleControlRequest::RouteBind {
2983 route_channel: module_channel,
2984 epoch: module_epoch,
2985 target,
2986 identity,
2987 principal: Some(principal),
2988 consumer_capabilities,
2989 admission_facts,
2990 };
2991 let relay_body = serde_json::to_vec(&relay).map_err(|err| {
2992 RouterError::backend(
2993 0,
2994 frame.header.corr,
2995 format!("failed to encode route.bind request: {err}"),
2996 )
2997 })?;
2998 let relay_frame = Frame::build_with_version(
2999 negotiated_ver,
3000 FrameType::Request,
3001 control_flags(),
3002 0,
3003 0,
3004 relay_corr,
3005 relay_body,
3006 )
3007 .map_err(RouterError::FrameBuild)?;
3008
3009 if let Err(err) = module_sink.send(relay_frame).await {
3010 reservation.release_and_disarm();
3011 return Ok(vec![self.route_open_refusal_frame(
3012 ctx,
3013 &frame,
3014 &target_module_id,
3015 "relay_send_failed",
3016 "target_unavailable",
3017 err.to_string(),
3018 )?]);
3019 }
3020
3021 if !self
3022 .forwarding
3023 .mark_route_bind_relay_enqueued(endpoint, relay_corr)
3024 .map_err(RouterError::Forwarding)?
3025 {
3026 self.send_abandoned_route_bind_goodbye(
3027 &module_sink,
3028 negotiated_ver,
3029 module_channel,
3030 module_epoch,
3031 );
3032 }
3033
3034 match timeout_at(relay_deadline, receiver).await {
3035 Ok(Ok(RouteBindRelayOutcome::Accepted)) => {
3036 reservation.disarm();
3037 if breaker.record_accepted() {
3038 info!(
3039 module_id = %target_module_id,
3040 "route.bind breaker closed: the probe was accepted"
3041 );
3042 }
3043 self.observe_route_open_accept(ctx, &target_module_id, &principal_label);
3044 Ok(Vec::new())
3045 }
3046 Ok(Ok(RouteBindRelayOutcome::Rejected(body))) => {
3047 reservation.release_and_disarm();
3048 breaker.record_inconclusive();
3052 self.counters
3053 .increment_route_open_refused("module_rejected");
3054 info!(
3055 target: "control",
3056 code = "module_rejected",
3057 module_code = ?body.code,
3058 module_id = ?target_module_id,
3059 connection_id = ctx.connection_id.get(),
3060 "route.open refused"
3061 );
3062 Ok(vec![control_error_body_frame(&frame, body)?])
3063 }
3064 Ok(Ok(RouteBindRelayOutcome::ModuleGone(message))) => {
3065 reservation.release_and_disarm();
3066 breaker.record_inconclusive();
3067 tracing::warn!(
3074 module_id = %target_module_id,
3075 "route.bind relay abandoned: {message}"
3076 );
3077 Ok(vec![self.route_open_refusal_frame(
3078 ctx,
3079 &frame,
3080 &target_module_id,
3081 "relay_abandoned",
3082 "target_unavailable",
3083 message,
3084 )?])
3085 }
3086 Ok(Err(_)) => {
3087 reservation.release_and_disarm();
3088 breaker.record_inconclusive();
3089 Ok(vec![self.route_open_refusal_frame(
3090 ctx,
3091 &frame,
3092 &target_module_id,
3093 "relay_waiter_canceled",
3094 "target_unavailable",
3095 "route.bind relay waiter was canceled before the module responded",
3096 )?])
3097 }
3098 Err(_) => {
3099 reservation.release_and_disarm();
3100 if let Some(opened) = breaker.record_timeout(
3104 self.route_bind_breaker_threshold,
3105 self.route_bind_breaker_cooldown,
3106 ) {
3107 warn!(
3108 module_id = %target_module_id,
3109 consecutive_timeouts = opened.consecutive_timeouts,
3110 cooldown_ms = self.route_bind_breaker_cooldown.as_millis() as u64,
3111 reopened_after_probe = opened.reopened_after_probe,
3112 "route.bind breaker open: refusing route.open for this module without relaying until one probe says it recovered"
3113 );
3114 }
3115 tracing::warn!(
3123 module_id = %target_module_id,
3124 timeout_ms = route_bind_relay_timeout.as_millis() as u64,
3125 "route.bind relay timed out: module did not ack within budget"
3126 );
3127 Ok(vec![self.route_open_refusal_frame(
3128 ctx,
3129 &frame,
3130 &target_module_id,
3131 "relay_timed_out",
3132 "module_timeout",
3133 format!(
3134 "module_id '{target_module_id}' did not answer route.bind within {:?}",
3135 route_bind_relay_timeout
3136 ),
3137 )?])
3138 }
3139 }
3140 }
3141
3142 fn handle_supervisor_spawn_snapshot(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
3143 let response = ClientControlResponse::SupervisorSpawnSnapshot {
3144 snapshot: self.supervisor.spawn_snapshot(),
3145 };
3146 Ok(vec![control_response_body_frame(
3147 &frame,
3148 &response,
3149 "ClientControlResponse::SupervisorSpawnSnapshot",
3150 )?])
3151 }
3152
3153 fn handle_supervisor_spawn_subscribe(
3154 &self,
3155 ctx: &RouteCtx,
3156 frame: Frame,
3157 since: Option<SpawnCursor>,
3158 ) -> Result<Vec<Frame>, RouterError> {
3159 match self.supervisor.subscribe_spawns(
3160 ctx.connection_id,
3161 frame.header.corr,
3162 response_version(&frame),
3163 since,
3164 ctx.egress.clone(),
3165 ) {
3166 Ok(()) => Ok(Vec::new()),
3167 Err(SpawnSubscribeRefusal::ForeignIncarnation { current }) => {
3168 Ok(vec![control_error_body_frame(
3169 &frame,
3170 ErrorBody {
3171 code: "spawn_cursor_incarnation_mismatch".to_string(),
3172 message: "spawn cursor belongs to a different daemon incarnation"
3173 .to_string(),
3174 detail: Some(serde_json::json!({
3175 "current_daemon_incarnation": current
3176 })),
3177 },
3178 )?])
3179 }
3180 Err(SpawnSubscribeRefusal::TooOld { oldest }) => Ok(vec![control_error_body_frame(
3181 &frame,
3182 ErrorBody {
3183 code: "spawn_cursor_too_old".to_string(),
3184 message: "spawn cursor predates the retained event ring".to_string(),
3185 detail: Some(serde_json::json!({
3186 "oldest_retained_cursor": oldest
3187 })),
3188 },
3189 )?]),
3190 Err(SpawnSubscribeRefusal::Frame(error)) => Err(RouterError::backend(
3191 0,
3192 frame.header.corr,
3193 format!("failed to open supervisor spawn subscription: {error}"),
3194 )),
3195 }
3196 }
3197
3198 fn handle_supervisor_list(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
3199 let generation = self
3200 .registry
3201 .generation()
3202 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
3203 let modules = self
3204 .supervisor
3205 .list()
3206 .into_iter()
3207 .map(|module| {
3208 let status = module.status_for_control("list").map_err(|err| {
3209 RouterError::backend(
3210 0,
3211 frame.header.corr,
3212 format!("failed to read supervisor status: {err}"),
3213 )
3214 })?;
3215 Ok(SupervisorEntry {
3216 module_id: status.module_id,
3217 state: status.state.to_string(),
3218 enabled: status.enabled,
3219 live: status.live,
3220 protocol: status.protocol,
3221 health: status.health.status,
3222 last_probe_ms: status.health.last_probe_ms,
3223 last_exit_code: status.last_exit.as_ref().and_then(|e| e.code),
3224 last_exit_signal: status.last_exit.as_ref().and_then(|e| e.signal),
3225 last_exit_ms: status.last_exit.as_ref().map(|e| e.at_ms),
3226 last_exit_kind: status.last_exit.as_ref().map(|e| e.kind.into()),
3227 restart_count: Some(status.restart_count),
3228 max_restarts: Some(status.max_restarts),
3229 lifetime_restarts: Some(status.lifetime_restarts),
3230 spawn_generation: Some(status.spawn_generation),
3231 restart_window_secs: Some(status.restart_window.as_secs()),
3232 drain_timeout_ms: Some(status.drain_timeout.as_millis() as u64),
3233 restart_backoff_ms: Some(status.restart_backoff.as_millis() as u64),
3234 restart_max_backoff_ms: Some(status.restart_max_backoff.as_millis() as u64),
3235 })
3236 })
3237 .collect::<Result<Vec<_>, RouterError>>()?;
3238 let response = ClientControlResponse::SupervisorList {
3239 generation,
3240 modules,
3241 };
3242 Ok(vec![control_response_body_frame(
3243 &frame,
3244 &response,
3245 "ClientControlResponse::SupervisorList",
3246 )?])
3247 }
3248
3249 fn handle_supervisor_stderr_tail(
3250 &self,
3251 frame: Frame,
3252 module_id: String,
3253 max_lines: Option<u32>,
3254 max_bytes: Option<u32>,
3255 ) -> Result<Vec<Frame>, RouterError> {
3256 let Some(module) = self.supervisor.get(&module_id) else {
3257 return Ok(vec![control_error_frame(
3258 &frame,
3259 "unknown_module",
3260 format!("module_id '{module_id}' is not supervised"),
3261 )?]);
3262 };
3263
3264 let snapshot = module.stderr_tail(
3265 max_lines.map(|value| value as usize),
3266 max_bytes.map(|value| value as usize),
3267 );
3268
3269 let response = ClientControlResponse::SupervisorStderrTail {
3270 module_id,
3271 tail: StderrTail {
3272 capture: match snapshot.capture {
3273 CaptureState::Captured => StderrCaptureState::Captured,
3274 CaptureState::Incomplete { reason } => {
3275 StderrCaptureState::Incomplete { reason }
3276 }
3277 CaptureState::NotCaptured { reason } => {
3278 StderrCaptureState::NotCaptured { reason }
3279 }
3280 },
3281 entries: snapshot
3282 .entries
3283 .into_iter()
3284 .map(|entry| match entry {
3285 TailEntry::Line { text, truncated } => {
3286 StderrTailEntry::Line { text, truncated }
3287 }
3288 TailEntry::ProcessStart => StderrTailEntry::ProcessStart,
3289 })
3290 .collect(),
3291 dropped_lines: snapshot.dropped_lines,
3292 },
3293 };
3294 Ok(vec![control_response_body_frame(
3295 &frame,
3296 &response,
3297 "ClientControlResponse::SupervisorStderrTail",
3298 )?])
3299 }
3300
3301 async fn handle_supervisor_terminals(
3302 &self,
3303 frame: Frame,
3304 module_id: String,
3305 ) -> Result<Vec<Frame>, RouterError> {
3306 let Some(module) = self.supervisor.get(&module_id) else {
3307 return Ok(vec![control_error_frame(
3308 &frame,
3309 "unknown_module",
3310 format!("module_id '{module_id}' is not supervised"),
3311 )?]);
3312 };
3313
3314 let terminals = module
3317 .read_durable_terminal_history()
3318 .await
3319 .map_err(|error| {
3320 RouterError::backend(
3321 0,
3322 frame.header.corr,
3323 format!("failed to read terminal history: {error}"),
3324 )
3325 })?;
3326 let response = ClientControlResponse::SupervisorTerminals {
3327 module_id,
3328 terminals,
3329 };
3330 Ok(vec![control_response_body_frame(
3331 &frame,
3332 &response,
3333 "ClientControlResponse::SupervisorTerminals",
3334 )?])
3335 }
3336
3337 fn handle_supervisor_routes(
3338 &self,
3339 frame: Frame,
3340 module_id: Option<String>,
3341 ) -> Result<Vec<Frame>, RouterError> {
3342 let modules = self
3343 .forwarding
3344 .route_census(module_id.as_deref())
3345 .map_err(RouterError::Forwarding)?
3346 .into_iter()
3347 .map(|(module_id, routes)| SupervisorRouteModule {
3348 module_id,
3349 routes: routes
3350 .into_iter()
3351 .map(|route| SupervisorRoute {
3352 consumer: match route.principal {
3353 Principal::Reserved { module_id } => {
3354 SupervisorRouteConsumer::Reserved { module_id }
3355 }
3356 Principal::Direct | Principal::Unverified => {
3357 SupervisorRouteConsumer::Direct {
3358 connection_id: route.goodbye_target.connection_id.get(),
3359 }
3360 }
3361 },
3362 age_ms: Instant::now()
3363 .saturating_duration_since(route.bound_at)
3364 .as_millis()
3365 .try_into()
3366 .unwrap_or(u64::MAX),
3367 draining: route.draining,
3368 drain_reason: route.drain_reason,
3369 })
3370 .collect(),
3371 })
3372 .collect();
3373 let response = ClientControlResponse::SupervisorRoutes { modules };
3374 Ok(vec![control_response_body_frame(
3375 &frame,
3376 &response,
3377 "ClientControlResponse::SupervisorRoutes",
3378 )?])
3379 }
3380
3381 async fn handle_supervisor_provenance(
3382 &self,
3383 frame: Frame,
3384 module_id: Option<String>,
3385 ) -> Result<Vec<Frame>, RouterError> {
3386 let mut selected = if let Some(module_id) = module_id {
3387 let Some(module) = self.supervisor.get(&module_id) else {
3388 return Ok(vec![control_error_frame(
3389 &frame,
3390 "unknown_module",
3391 format!("module_id '{module_id}' is not supervised"),
3392 )?]);
3393 };
3394 vec![module]
3395 } else {
3396 self.supervisor.list()
3397 };
3398
3399 let mut modules = Vec::with_capacity(selected.len());
3400 for module in selected.drain(..) {
3401 let status = module.status().map_err(|err| {
3402 RouterError::backend(
3403 0,
3404 frame.header.corr,
3405 format!("failed to read supervisor status: {err}"),
3406 )
3407 })?;
3408 let module_declared = self
3409 .registry
3410 .get_module(&status.module_id)
3411 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
3412 .and_then(|registration| registration.manifest.provenance)
3413 .map(|build| ModuleDeclaredProvenance::Reported { build })
3414 .unwrap_or(ModuleDeclaredProvenance::Unverifiable);
3415 #[cfg(test)]
3416 let running_image = match &self.provenance_probe_override {
3417 Some(result) => result.clone(),
3418 None => module.running_image_agreement().await,
3419 };
3420 #[cfg(not(test))]
3421 let running_image = module.running_image_agreement().await;
3422 modules.push(SupervisorModuleProvenance {
3423 module_id: status.module_id,
3424 module_declared,
3425 daemon_observed: SupervisorObservedProcess {
3426 pid: status.pid,
3427 spawned_at_ms: status.spawned_at_ms,
3428 spawned_from: status.spawned_from,
3429 running_image,
3430 },
3431 });
3432 }
3433 let daemon = SupervisorDaemonProvenance {
3434 daemon_build: self.daemon_provenance.build.clone(),
3435 daemon_observed: DaemonObservedProcess {
3436 pid: self.daemon_provenance.pid,
3437 started_at_ms: self
3438 .daemon_provenance
3439 .start_clock
3440 .map(|clock| clock.started_at_ms())
3441 .or(self.daemon_provenance.started_at_ms),
3442 running_image: self
3443 .daemon_provenance
3444 .probe
3445 .observe(
3446 self.daemon_provenance.pid,
3447 self.daemon_provenance.executable_path.as_deref(),
3448 self.daemon_provenance.executable_identity,
3449 self.daemon_provenance.process_start_time,
3450 )
3451 .await,
3452 },
3453 };
3454 let response = ClientControlResponse::SupervisorProvenance { daemon, modules };
3455 Ok(vec![control_response_body_frame(
3456 &frame,
3457 &response,
3458 "ClientControlResponse::SupervisorProvenance",
3459 )?])
3460 }
3461
3462 fn handle_supervisor_health(&self, frame: Frame) -> Result<Vec<Frame>, RouterError> {
3463 self.refresh_capability_requirements();
3464 let generation = self
3465 .registry
3466 .generation()
3467 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?;
3468 let modules = self
3469 .supervisor
3470 .list()
3471 .into_iter()
3472 .map(|module| {
3473 let status = module.status_for_control("health").map_err(|err| {
3474 RouterError::backend(
3475 0,
3476 frame.header.corr,
3477 format!("failed to read supervisor health: {err}"),
3478 )
3479 })?;
3480 let module_id = status.module_id;
3481 let capability_detail = self
3482 .capability_evaluator
3483 .required_problem_detail(&module_id);
3484 Ok(SupervisorHealthEntry {
3485 module_id,
3486 status: status.health.status,
3487 detail: append_capability_problem_detail(
3488 status.health.detail,
3489 capability_detail,
3490 ),
3491 metrics: status.health.metrics,
3492 consecutive_failures: status.health.consecutive_failures,
3493 late_answer_count: status.health.late_answer_count,
3494 last_late_answer_latency_ms: status.health.last_late_answer_latency_ms,
3495 last_action: status.health.last_action,
3496 last_action_ms: status.health.last_action_ms,
3497 last_probe_ms: status.health.last_probe_ms,
3498 })
3499 })
3500 .collect::<Result<Vec<_>, RouterError>>()?;
3501 let response = ClientControlResponse::SupervisorHealth {
3502 generation,
3503 modules,
3504 };
3505 Ok(vec![control_response_body_frame(
3506 &frame,
3507 &response,
3508 "ClientControlResponse::SupervisorHealth",
3509 )?])
3510 }
3511
3512 async fn handle_supervisor_restart(
3513 &self,
3514 frame: Frame,
3515 module_id: String,
3516 drain_timeout_ms: Option<u64>,
3517 ) -> Result<Vec<Frame>, RouterError> {
3518 let operation_lock = self.supervisor.operation_lock();
3519 let _operation_guard = operation_lock.lock().await;
3520 let Some(module) = self.supervisor.get(&module_id) else {
3521 return Ok(vec![control_error_frame(
3522 &frame,
3523 "unknown_module",
3524 format!("module_id '{module_id}' is not supervised"),
3525 )?]);
3526 };
3527
3528 if let Err(err) = module.restart(drain_timeout_ms).await {
3529 let (code, message) = match err {
3530 crate::supervise::SuperviseError::Disabled { .. } => {
3531 ("module_disabled", err.to_string())
3532 }
3533 crate::supervise::SuperviseError::SwapInProgress { .. } => {
3534 ("swap_in_progress", err.to_string())
3535 }
3536 _ => (
3537 "target_unavailable",
3538 format!("failed to restart module_id '{module_id}': {err}"),
3539 ),
3540 };
3541 return Ok(vec![control_error_frame(&frame, code, message)?]);
3542 }
3543
3544 let response = ClientControlResponse::SupervisorAck {
3545 module_id,
3546 applied: true,
3547 };
3548 Ok(vec![control_response_body_frame(
3549 &frame,
3550 &response,
3551 "ClientControlResponse::SupervisorAck",
3552 )?])
3553 }
3554
3555 async fn handle_supervisor_swap(
3559 &self,
3560 frame: Frame,
3561 module_id: String,
3562 ready_timeout_ms: Option<u64>,
3563 ) -> Result<Vec<Frame>, RouterError> {
3564 let module = {
3571 let operation_lock = self.supervisor.operation_lock();
3572 let _operation_guard = operation_lock.lock().await;
3573 self.supervisor.get(&module_id)
3574 };
3575 let Some(module) = module else {
3576 return Ok(vec![control_error_frame(
3577 &frame,
3578 "unknown_module",
3579 format!("module_id '{module_id}' is not supervised"),
3580 )?]);
3581 };
3582
3583 if let Err(err) = module
3584 .swap(ready_timeout_ms.map(Duration::from_millis))
3585 .await
3586 {
3587 use crate::supervise::SuperviseError;
3588 let message = err.to_string();
3589 let error = match err {
3590 SuperviseError::Disabled { .. } => ErrorBody::new("module_disabled", message),
3591 SuperviseError::SwapRefused { reason, .. } => ErrorBody {
3592 code: "swap_refused".to_string(),
3593 message,
3594 detail: Some(serde_json::json!({ "reason": reason.as_str() })),
3595 },
3596 SuperviseError::SwapFailed {
3597 arm,
3598 candidate_exit,
3599 ..
3600 } => ErrorBody {
3601 code: "swap_failed".to_string(),
3602 message,
3603 detail: Some(serde_json::json!({
3604 "arm": arm.as_str(),
3605 "candidate_exit_code": candidate_exit.as_ref().and_then(|exit| exit.code),
3606 "candidate_exit_signal": candidate_exit.as_ref().and_then(|exit| exit.signal),
3607 })),
3608 },
3609 _ => ErrorBody::new(
3610 "target_unavailable",
3611 format!("failed to swap module_id '{module_id}': {message}"),
3612 ),
3613 };
3614 return Ok(vec![control_error_body_frame(&frame, error)?]);
3615 }
3616
3617 let response = ClientControlResponse::SupervisorAck {
3618 module_id,
3619 applied: true,
3620 };
3621 Ok(vec![control_response_body_frame(
3622 &frame,
3623 &response,
3624 "ClientControlResponse::SupervisorAck",
3625 )?])
3626 }
3627
3628 async fn handle_supervisor_reload(
3629 &self,
3630 frame: Frame,
3631 module_id: String,
3632 ) -> Result<Vec<Frame>, RouterError> {
3633 let operation_lock = self.supervisor.operation_lock();
3634 let _operation_guard = operation_lock.lock().await;
3635 let Some(module) = self.supervisor.get(&module_id) else {
3636 return Ok(vec![control_error_frame(
3637 &frame,
3638 "unknown_module",
3639 format!("module_id '{module_id}' is not supervised"),
3640 )?]);
3641 };
3642
3643 if let Err(err) = module.reload().await {
3644 let (code, message) = match err {
3645 crate::supervise::SuperviseError::Disabled { .. } => {
3646 ("module_disabled", err.to_string())
3647 }
3648 crate::supervise::SuperviseError::SwapInProgress { .. } => {
3649 ("swap_in_progress", err.to_string())
3650 }
3651 _ => (
3652 "reload_failed",
3653 format!("failed to reload module_id '{module_id}': {err}"),
3654 ),
3655 };
3656 return Ok(vec![control_error_frame(&frame, code, message)?]);
3657 }
3658
3659 let response = ClientControlResponse::SupervisorAck {
3660 module_id,
3661 applied: true,
3662 };
3663 Ok(vec![control_response_body_frame(
3664 &frame,
3665 &response,
3666 "ClientControlResponse::SupervisorAck",
3667 )?])
3668 }
3669
3670 async fn handle_supervisor_rescan(
3671 &self,
3672 frame: Frame,
3673 preview: bool,
3674 ) -> Result<Vec<Frame>, RouterError> {
3675 let Some(context) = self.rescan.clone() else {
3676 return Ok(vec![control_error_frame(
3677 &frame,
3678 "rescan_unavailable",
3679 "the daemon was not started with a reloadable config path".to_string(),
3680 )?]);
3681 };
3682
3683 let operation_lock = self.supervisor.operation_lock();
3684 let _operation_guard = operation_lock.lock().await;
3685 let loaded = match crate::daemon_config::load(&context.config_path) {
3686 Ok(config) => config,
3687 Err(err) => {
3688 return Ok(vec![control_error_frame(
3689 &frame,
3690 "invalid_daemon_config",
3691 format!("supervisor rescan rejected daemon config: {err}"),
3692 )?])
3693 }
3694 };
3695 let Some(config) = loaded else {
3703 return Ok(vec![control_error_frame(
3704 &frame,
3705 "invalid_daemon_config",
3706 format!(
3707 "daemon config not found at {}; refusing to rescan (an absent config would \
3708 retire every supervised module)",
3709 context.config_path.display()
3710 ),
3711 )?]);
3712 };
3713 let (
3714 configured_port,
3715 storage_config,
3716 admission_facts_carrier_module_id,
3717 admission_facts_targets,
3718 modules,
3719 reserved_capabilities,
3720 ) = (
3721 config.port,
3722 config.storage,
3723 config.admission_facts_carrier_module_id,
3724 config.admission_facts_targets,
3725 config.modules,
3726 config.reserved_capabilities,
3727 );
3728
3729 let mut restart_required = Vec::new();
3738 for section in RestartRequiredSection::ALL {
3739 let changed = match section {
3740 RestartRequiredSection::Port => configured_port != context.configured_port,
3741 RestartRequiredSection::Storage => storage_config != context.storage_config,
3742 RestartRequiredSection::AdmissionFactsCarrierModuleId => {
3743 admission_facts_carrier_module_id != context.admission_facts_carrier_module_id
3744 }
3745 RestartRequiredSection::AdmissionFactsTargets => {
3746 admission_facts_targets != context.admission_facts_targets
3747 }
3748 };
3749 if changed {
3750 restart_required.push(section.label().to_string());
3751 }
3752 }
3753 if !restart_required.is_empty() {
3754 warn!(
3755 config_path = %context.config_path.display(),
3756 sections = %restart_required.join(", "),
3757 "daemon config changed outside the modules section; restart the daemon to apply those changes"
3758 );
3759 }
3760
3761 for configured in &modules {
3762 if let Err(err) = validate_spec(&configured.module_spec()) {
3763 return Ok(vec![control_error_frame(
3764 &frame,
3765 "invalid_daemon_config",
3766 format!("supervisor rescan rejected daemon config: {err}"),
3767 )?]);
3768 }
3769 }
3770
3771 let configured_capabilities = modules
3772 .iter()
3773 .map(|module| (module.module_id.clone(), module.enabled))
3774 .collect::<Vec<_>>();
3775 let preview_capability_warnings = if preview {
3776 let (_, registrations) = self.runtime_capability_snapshot()?;
3777 let current_modules = self
3778 .supervisor
3779 .list()
3780 .into_iter()
3781 .map(|module| module.module_id().to_string())
3782 .collect::<BTreeSet<_>>();
3783 let resulting_modules = configured_capabilities.clone();
3784 let removed = current_modules
3785 .into_iter()
3786 .filter(|module_id| {
3787 !resulting_modules
3788 .iter()
3789 .any(|(configured_id, _)| configured_id == module_id)
3790 })
3791 .collect::<Vec<_>>();
3792 self.capability_evaluator.preview_removal_warnings(
3793 resulting_modules,
3794 &removed,
3795 ®istrations,
3796 )
3797 } else {
3798 Vec::new()
3799 };
3800 let result = match self
3801 .reconcile_supervised_modules(&context.supervisor, modules, preview)
3802 .await
3803 {
3804 Ok(result) => result,
3805 Err(message) => {
3806 return Ok(vec![control_error_frame(&frame, "rescan_failed", message)?])
3807 }
3808 };
3809 if !preview {
3810 self.capability_evaluator
3811 .configure(configured_capabilities, reserved_capabilities);
3812 self.capability_evaluator.wake_deadline_loop();
3813 self.refresh_capability_requirements();
3814 }
3815 let mut result = result;
3816 result.restart_required = restart_required;
3817 result.capability_warnings = preview_capability_warnings;
3818 let response = ClientControlResponse::SupervisorRescan { result };
3819 Ok(vec![control_response_body_frame(
3820 &frame,
3821 &response,
3822 "ClientControlResponse::SupervisorRescan",
3823 )?])
3824 }
3825
3826 async fn handle_supervisor_release_reserved(
3827 &self,
3828 frame: Frame,
3829 module_id: String,
3830 ) -> Result<Vec<Frame>, RouterError> {
3831 let Some(context) = self.rescan.clone() else {
3832 return Ok(vec![control_error_frame(
3833 &frame,
3834 "release_unavailable",
3835 "reserved-id release requires a daemon started with a reloadable config path",
3836 )?]);
3837 };
3838 let operation_lock = self.supervisor.operation_lock();
3839 let _operation_guard = operation_lock.lock().await;
3840 let loaded = match crate::daemon_config::load(&context.config_path) {
3841 Ok(Some(config)) => config,
3842 Ok(None) => {
3843 return Ok(vec![control_error_frame(
3844 &frame,
3845 "invalid_daemon_config",
3846 format!(
3847 "daemon config not found at {}; refusing to release reserved module_id '{module_id}'",
3848 context.config_path.display()
3849 ),
3850 )?])
3851 }
3852 Err(err) => {
3853 return Ok(vec![control_error_frame(
3854 &frame,
3855 "invalid_daemon_config",
3856 format!("unable to verify reserved-id release against daemon config: {err}"),
3857 )?])
3858 }
3859 };
3860 if loaded
3861 .modules
3862 .iter()
3863 .any(|configured| configured.module_id == module_id)
3864 {
3865 return Ok(vec![control_error_frame(
3866 &frame,
3867 "reserved_module_configured",
3868 format!(
3869 "module_id '{module_id}' remains configured; remove its config entry and rescan before releasing its reserved id"
3870 ),
3871 )?]);
3872 }
3873 if !self.supervisor.release_retained_reserved_gate(&module_id) {
3874 return Ok(vec![control_error_frame(
3875 &frame,
3876 "reserved_gate_not_retained",
3877 format!(
3878 "module_id '{module_id}' has no retired reserved-id gate to release; rescan its removed reserved configuration first"
3879 ),
3880 )?]);
3881 }
3882
3883 let response = ClientControlResponse::SupervisorAck {
3884 module_id,
3885 applied: true,
3886 };
3887 Ok(vec![control_response_body_frame(
3888 &frame,
3889 &response,
3890 "ClientControlResponse::SupervisorAck",
3891 )?])
3892 }
3893
3894 async fn reconcile_supervised_modules(
3903 &self,
3904 supervisor: &Supervisor,
3905 configured_modules: Vec<crate::daemon_config::ConfiguredModule>,
3906 preview: bool,
3907 ) -> Result<SupervisorRescanResult, String> {
3908 let mut current = BTreeMap::new();
3909 for module in self.supervisor.list() {
3910 let (spec, health) = module.configuration().map_err(|err| {
3911 format!(
3912 "failed to read configuration for module_id '{}': {err}",
3913 module.module_id()
3914 )
3915 })?;
3916 let enabled = module
3917 .status()
3918 .map_err(|err| {
3919 format!(
3920 "failed to read status for module_id '{}': {err}",
3921 module.module_id()
3922 )
3923 })?
3924 .enabled;
3925 current.insert(
3926 module.module_id().to_string(),
3927 (module, spec, health, enabled),
3928 );
3929 }
3930 let configured = configured_modules
3931 .into_iter()
3932 .map(|module| (module.module_id.clone(), module))
3933 .collect::<BTreeMap<_, _>>();
3934
3935 let added = configured
3936 .keys()
3937 .filter(|module_id| !current.contains_key(*module_id))
3938 .cloned()
3939 .collect::<Vec<_>>();
3940 let removed = current
3941 .keys()
3942 .filter(|module_id| !configured.contains_key(*module_id))
3943 .cloned()
3944 .collect::<Vec<_>>();
3945 let mut changed_pending_reload = Vec::new();
3946 let mut configuration_changes = BTreeSet::new();
3947 let mut enabled_changes = BTreeSet::new();
3948 let mut unchanged = 0_u32;
3949
3950 for (module_id, configured_module) in &configured {
3951 let Some((_, current_spec, current_health, current_enabled)) = current.get(module_id)
3952 else {
3953 continue;
3954 };
3955 let configuration_changed = *current_spec != configured_module.module_spec()
3956 || *current_health != configured_module.health;
3957 let enabled_changed = *current_enabled != configured_module.enabled;
3958 if configuration_changed {
3959 configuration_changes.insert(module_id.clone());
3960 changed_pending_reload.push(module_id.clone());
3961 }
3962 if enabled_changed {
3963 enabled_changes.insert(module_id.clone());
3964 }
3965 if !configuration_changed && !enabled_changed {
3966 unchanged = unchanged.saturating_add(1);
3967 }
3968 }
3969
3970 if preview {
3976 return Ok(SupervisorRescanResult {
3977 added,
3978 removed,
3979 changed_pending_reload,
3980 enabled_changes: enabled_changes.iter().cloned().collect(),
3981 unchanged,
3982 preview: true,
3983 restart_required: Vec::new(),
3987 capability_warnings: Vec::new(),
3988 });
3989 }
3990
3991 for module_id in &removed {
3992 let module = ¤t
3993 .get(module_id)
3994 .expect("removed module came from current supervisor state")
3995 .0;
3996 module.retire().await.map_err(|err| {
3997 format!("failed to retire module_id '{module_id}' during rescan: {err}")
3998 })?;
3999 self.supervisor.record_rescan_removal(module_id);
4033 self.supervisor.retire(module_id);
4034 }
4035
4036 for module_id in configured.keys() {
4037 let Some((module, _, _, _)) = current.get(module_id) else {
4038 continue;
4039 };
4040 let configured_module = configured
4041 .get(module_id)
4042 .expect("configured module id came from configured map");
4043 if configuration_changes.contains(module_id) {
4044 module
4045 .update_configuration(
4046 configured_module.module_spec(),
4047 configured_module.health,
4048 configured_module.drain_timeout_ms,
4049 )
4050 .await
4051 .map_err(|err| {
4052 format!(
4053 "failed to update module_id '{module_id}' configuration during rescan: {err}"
4054 )
4055 })?;
4056 }
4057 if enabled_changes.contains(module_id) {
4058 module
4059 .set_enabled(configured_module.enabled)
4060 .await
4061 .map_err(|err| {
4062 format!(
4063 "failed to apply module_id '{module_id}' enabled={} during rescan: {err}",
4064 configured_module.enabled
4065 )
4066 })?;
4067 }
4068 }
4069
4070 for module_id in &added {
4071 let configured_module = configured
4072 .get(module_id)
4073 .expect("added module id came from configured map");
4074 supervisor
4075 .supervise_configured_with_health(
4076 configured_module.module_spec(),
4077 configured_module.enabled,
4078 configured_module.health,
4079 configured_module.drain_timeout_ms,
4080 configured_module.restart,
4081 )
4082 .map_err(|err| {
4083 format!("failed to add module_id '{module_id}' during rescan: {err}")
4084 })?;
4085 }
4086
4087 Ok(SupervisorRescanResult {
4088 added,
4089 removed,
4090 changed_pending_reload,
4091 enabled_changes: enabled_changes.iter().cloned().collect(),
4092 unchanged,
4093 preview: false,
4094 restart_required: Vec::new(),
4097 capability_warnings: Vec::new(),
4098 })
4099 }
4100
4101 async fn handle_supervisor_set_enabled(
4102 &self,
4103 frame: Frame,
4104 module_id: String,
4105 enabled: bool,
4106 ) -> Result<Vec<Frame>, RouterError> {
4107 let operation_lock = self.supervisor.operation_lock();
4108 let _operation_guard = operation_lock.lock().await;
4109 let Some(module) = self.supervisor.get(&module_id) else {
4110 return Ok(vec![control_error_frame(
4111 &frame,
4112 "unknown_module",
4113 format!("module_id '{module_id}' is not supervised"),
4114 )?]);
4115 };
4116
4117 let applied = match module.set_enabled(enabled).await {
4118 Ok(applied) => applied,
4119 Err(err) => {
4120 return Ok(vec![control_error_frame(
4121 &frame,
4122 "target_unavailable",
4123 format!("failed to set module_id '{module_id}' enabled={enabled}: {err}"),
4124 )?])
4125 }
4126 };
4127
4128 self.capability_evaluator.wake_deadline_loop();
4129 self.refresh_capability_requirements();
4130 let response = ClientControlResponse::SupervisorAck { module_id, applied };
4131 Ok(vec![control_response_body_frame(
4132 &frame,
4133 &response,
4134 "ClientControlResponse::SupervisorAck",
4135 )?])
4136 }
4137
4138 async fn handle_supervisor_health_probe(
4139 &self,
4140 frame: Frame,
4141 module_id: String,
4142 ) -> Result<Vec<Frame>, RouterError> {
4143 self.refresh_capability_requirements();
4144 let Some(registration) = self
4145 .registry
4146 .get_module(&module_id)
4147 .map_err(|err| RouterError::backend(0, frame.header.corr, err.to_string()))?
4148 else {
4149 return Ok(vec![control_error_frame(
4150 &frame,
4151 "unknown_module",
4152 format!("module_id '{module_id}' is not registered"),
4153 )?]);
4154 };
4155
4156 if !module_registration_grants_op(®istration.control_ops, MODULE_CONTROL_OP_HEALTH_CHECK)
4169 {
4170 return Ok(vec![control_error_frame(
4171 &frame,
4172 "health_not_advertised",
4173 format!("module_id '{module_id}' did not advertise health.check"),
4174 )?]);
4175 }
4176
4177 let deadline = Instant::now() + self.health_probe_timeout;
4178 let pending = match self.forwarding.begin_module_control_rpc_for(
4179 &module_id,
4180 MODULE_CONTROL_OP_HEALTH_CHECK,
4181 deadline,
4182 ) {
4183 Ok(pending) => pending,
4184 Err(err) => {
4185 return Ok(vec![control_error_frame(
4186 &frame,
4187 forwarding_error_code(&err),
4188 err.to_string(),
4189 )?])
4190 }
4191 };
4192
4193 let PendingModuleControlRpc {
4194 endpoint,
4195 module_sink,
4196 negotiated_ver,
4197 corr: probe_corr,
4198 receiver,
4199 } = pending;
4200 let mut guard =
4201 ModuleControlRpcGuard::new(Arc::clone(&self.forwarding), endpoint, probe_corr);
4202 let probe_body =
4203 serde_json::to_vec(&ModuleControlRequest::HealthCheck {}).map_err(|err| {
4204 RouterError::backend(
4205 0,
4206 frame.header.corr,
4207 format!("failed to encode health.check request: {err}"),
4208 )
4209 })?;
4210 let probe_frame = Frame::build_with_version(
4211 negotiated_ver,
4212 FrameType::Request,
4213 control_flags(),
4214 0,
4215 0,
4216 probe_corr,
4217 probe_body,
4218 )
4219 .map_err(RouterError::FrameBuild)?;
4220
4221 if let Err(err) = module_sink.send(probe_frame).await {
4222 return Ok(vec![control_error_frame(
4223 &frame,
4224 "target_unavailable",
4225 err.to_string(),
4226 )?]);
4227 }
4228
4229 match timeout_at(deadline, receiver).await {
4230 Ok(Ok(ModuleControlRpcOutcome::Response(response))) => {
4231 guard.disarm();
4232 let Some(report) = response.health_report() else {
4233 return Ok(vec![control_error_frame(
4234 &frame,
4235 "invalid_control_body",
4236 "health.check RPC returned a non-health response",
4237 )?]);
4238 };
4239 let HealthReport {
4244 status,
4245 detail,
4246 metrics,
4247 } = report;
4248 let capability_detail = self
4249 .capability_evaluator
4250 .required_problem_detail(&module_id);
4251 let response = ClientControlResponse::SupervisorHealthProbe {
4252 module_id,
4253 status,
4254 detail: append_capability_problem_detail(detail, capability_detail),
4255 metrics,
4256 };
4257 Ok(vec![control_response_body_frame(
4258 &frame,
4259 &response,
4260 "ClientControlResponse::SupervisorHealthProbe",
4261 )?])
4262 }
4263 Ok(Ok(ModuleControlRpcOutcome::Rejected(body))) => {
4264 guard.disarm();
4265 Ok(vec![control_error_body_frame(&frame, body)?])
4266 }
4267 Ok(Ok(ModuleControlRpcOutcome::ModuleGone(message))) => {
4268 guard.disarm();
4269 Ok(vec![control_error_frame(
4270 &frame,
4271 "target_unavailable",
4272 message,
4273 )?])
4274 }
4275 Ok(Ok(ModuleControlRpcOutcome::MalformedResponse(message))) => {
4276 guard.disarm();
4277 Ok(vec![control_error_frame(
4278 &frame,
4279 "invalid_control_body",
4280 message,
4281 )?])
4282 }
4283 Ok(Ok(ModuleControlRpcOutcome::UnexpectedOp { expected, actual })) => {
4284 guard.disarm();
4285 Ok(vec![control_error_frame(
4286 &frame,
4287 "invalid_control_body",
4288 format!("expected module-control op '{expected}', got '{actual}'"),
4289 )?])
4290 }
4291 Ok(Ok(ModuleControlRpcOutcome::DeadlineElapsed)) => {
4292 guard.disarm();
4293 Ok(vec![control_error_frame(
4294 &frame,
4295 "module_timeout",
4296 format!(
4297 "module_id '{module_id}' answered health.check after {:?}",
4298 self.health_probe_timeout
4299 ),
4300 )?])
4301 }
4302 Ok(Err(_)) => Ok(vec![control_error_frame(
4303 &frame,
4304 "target_unavailable",
4305 "health.check waiter was canceled before the module responded",
4306 )?]),
4307 Err(_) => Ok(vec![control_error_frame(
4308 &frame,
4309 "module_timeout",
4310 format!(
4311 "module_id '{module_id}' did not answer health.check within {:?}",
4312 self.health_probe_timeout
4313 ),
4314 )?]),
4315 }
4316 }
4317
4318 fn supervisor_status(
4319 &self,
4320 module_id: &str,
4321 corr: u64,
4322 ) -> Result<Option<(crate::supervise::ModuleStatus, bool)>, RouterError> {
4323 self.supervisor
4324 .get(module_id)
4325 .map(|module| {
4326 let warming = module.is_warming_for_control("status").map_err(|err| {
4327 RouterError::backend(
4328 0,
4329 corr,
4330 format!(
4331 "failed to read supervisor warming state for module_id '{module_id}': {err}"
4332 ),
4333 )
4334 })?;
4335 module.status_for_control("status").map_err(|err| {
4336 RouterError::backend(
4337 0,
4338 corr,
4339 format!(
4340 "failed to read supervisor status for module_id '{module_id}': {err}"
4341 ),
4342 )
4343 }).map(|status| (status, warming))
4344 })
4345 .transpose()
4346 }
4347
4348 fn guard_module_control_op(
4349 &self,
4350 frame: &Frame,
4351 module_id: &str,
4352 op: &str,
4353 ) -> Result<Option<Frame>, RouterError> {
4354 if self.module_grants_op(module_id, op, frame.header.corr)? {
4355 return Ok(None);
4356 }
4357
4358 Ok(Some(control_error_frame(
4359 frame,
4360 "op_not_allowed",
4361 format!("module_id '{module_id}' did not grant control op '{op}'"),
4362 )?))
4363 }
4364
4365 fn module_grants_op(&self, module_id: &str, op: &str, corr: u64) -> Result<bool, RouterError> {
4366 let Some(registration) = self
4367 .registry
4368 .get_module(module_id)
4369 .map_err(|err| RouterError::backend(0, corr, err.to_string()))?
4370 else {
4371 return Ok(false);
4372 };
4373 Ok(module_registration_grants_op(®istration.control_ops, op))
4374 }
4375
4376 fn handle_status_update(
4377 &self,
4378 endpoint: ModuleEndpointId,
4379 frame: Frame,
4380 ) -> Result<Vec<Frame>, RouterError> {
4381 let update = match serde_json::from_slice::<ModuleControlPush>(&frame.body) {
4382 Ok(update) => update,
4383 Err(err) => {
4384 if is_known_module_push_op(&frame.body) {
4389 return Ok(vec![control_error_frame(
4390 &frame,
4391 "invalid_control_body",
4392 format!("malformed module control push body: {err}"),
4393 )?]);
4394 }
4395 return Ok(Vec::new());
4396 }
4397 };
4398
4399 match update {
4400 ModuleControlPush::RouteStatus {
4401 route_channel,
4402 route_epoch,
4403 status,
4404 } => {
4405 self.forwarding
4406 .cache_status(endpoint, route_channel, route_epoch, status)
4407 .map_err(RouterError::Forwarding)?;
4408 }
4409 }
4410 Ok(Vec::new())
4411 }
4412
4413 fn handle_route_poll(
4414 &self,
4415 ctx: &RouteCtx,
4416 frame: Frame,
4417 route_channel: u16,
4418 route_epoch: u32,
4419 kind: PollKind,
4420 ) -> Result<Vec<Frame>, RouterError> {
4421 let snapshot = self
4422 .forwarding
4423 .route_poll_snapshot(ctx.connection_id, route_channel, route_epoch)
4424 .map_err(RouterError::Forwarding)?;
4425 let response = match (kind, snapshot) {
4426 (PollKind::Status, RoutePollSnapshot::Bound { status, .. }) => {
4427 ClientControlResponse::RoutePoll {
4428 route_channel,
4429 route_epoch,
4430 status,
4431 live: None,
4432 }
4433 }
4434 (PollKind::Status, RoutePollSnapshot::Absent) => ClientControlResponse::RoutePoll {
4435 route_channel,
4436 route_epoch,
4437 status: None,
4438 live: None,
4439 },
4440 (PollKind::Liveness, RoutePollSnapshot::Bound { module_id, .. }) => {
4441 let live = self
4467 .process_liveness
4468 .as_ref()
4469 .and_then(|source| source.process_live(&module_id))
4470 .unwrap_or(true);
4471 ClientControlResponse::RoutePoll {
4472 route_channel,
4473 route_epoch,
4474 status: None,
4475 live: Some(live),
4476 }
4477 }
4478 (PollKind::Liveness, RoutePollSnapshot::Absent) => ClientControlResponse::RoutePoll {
4479 route_channel,
4480 route_epoch,
4481 status: None,
4482 live: Some(false),
4483 },
4484 };
4485
4486 Ok(vec![control_response_body_frame(
4487 &frame,
4488 &response,
4489 "ClientControlResponse::RoutePoll",
4490 )?])
4491 }
4492
4493 pub(crate) fn observe_module_control_completion(
4494 &self,
4495 completion: ModuleControlRpcCompletion,
4496 ) -> bool {
4497 match completion {
4498 ModuleControlRpcCompletion::Unknown => false,
4499 ModuleControlRpcCompletion::Settled => true,
4500 ModuleControlRpcCompletion::LateHealthAnswer { module_id, latency } => {
4501 let latency_ms = latency.as_millis().min(u128::from(u64::MAX)) as u64;
4502 info!(
4503 module_id = %module_id,
4504 latency_ms,
4505 "late health.check answer proves the module is alive"
4506 );
4507 match self
4508 .supervisor
4509 .record_late_health_answer(&module_id, latency_ms)
4510 {
4511 Ok(true) => {}
4512 Ok(false) => debug!(
4513 module_id = %module_id,
4514 latency_ms,
4515 "late health.check answer has no active supervisor snapshot"
4516 ),
4517 Err(err) => warn!(
4518 module_id = %module_id,
4519 latency_ms,
4520 error = %err,
4521 "failed to record late health.check answer"
4522 ),
4523 }
4524 true
4525 }
4526 }
4527 }
4528
4529 fn refuse_to_end_module_connection_for_a_client(
4550 &self,
4551 module_connection_id: ConnectionId,
4552 corr: u64,
4553 err: ForwardingError,
4554 ) -> Result<(), RouterError> {
4555 if let ForwardingError::ConnectionClosing { connection_id } = err {
4556 if connection_id != module_connection_id {
4557 warn!(
4558 module_connection_id = module_connection_id.get(),
4559 client_connection_id = connection_id.get(),
4560 corr,
4561 "dropping a route.bind response for a closing client; the module connection keeps serving"
4562 );
4563 return Ok(());
4564 }
4565 }
4566 Err(RouterError::Forwarding(err))
4567 }
4568
4569 fn handle_module_relay_response(
4570 &self,
4571 connection_id: ConnectionId,
4572 frame: Frame,
4573 ) -> Result<Vec<Frame>, RouterError> {
4574 let mut secondary_error = None;
4575 let outcome = match frame.header.ty {
4576 FrameType::Response => match serde_json::from_slice::<ControlOpProbe>(&frame.body) {
4577 Ok(probe) if probe.op == "route.bind" => {
4578 match serde_json::from_slice::<ModuleControlResponse>(&frame.body) {
4579 Ok(ModuleControlResponse::RouteBindAck {}) => {
4580 RouteBindRelayOutcome::Accepted
4581 }
4582 Ok(other) => {
4583 let message =
4584 format!("route.bind response carried unexpected body: {other:?}");
4585 secondary_error = Some(control_error_frame(
4586 &frame,
4587 "invalid_control_body",
4588 message.clone(),
4589 )?);
4590 RouteBindRelayOutcome::ModuleGone(message)
4591 }
4592 Err(err) => {
4593 let message = format!("malformed route.bind response body: {err}");
4594 secondary_error = Some(control_error_frame(
4595 &frame,
4596 "invalid_control_body",
4597 message.clone(),
4598 )?);
4599 RouteBindRelayOutcome::ModuleGone(message)
4600 }
4601 }
4602 }
4603 Ok(probe) => {
4604 let outcome = match serde_json::from_slice::<ModuleControlResponse>(&frame.body)
4605 {
4606 Ok(response) => ModuleControlRpcOutcome::Response(response),
4607 Err(err) => ModuleControlRpcOutcome::MalformedResponse(format!(
4608 "malformed {} response body: {err}",
4609 probe.op
4610 )),
4611 };
4612 let completion = self
4613 .forwarding
4614 .complete_module_control_rpc(
4615 connection_id,
4616 frame.header.corr,
4617 Some(&probe.op),
4618 outcome,
4619 )
4620 .map_err(RouterError::Forwarding)?;
4621 if !self.observe_module_control_completion(completion) {
4622 debug!(
4623 connection_id = connection_id.get(),
4624 corr = frame.header.corr,
4625 op = %probe.op,
4626 "dropping late or unknown module-control RPC response"
4627 );
4628 }
4629 return Ok(Vec::new());
4630 }
4631 Err(err) => {
4632 if let Some(expected_op) = self
4633 .forwarding
4634 .pending_module_control_op(connection_id, frame.header.corr)
4635 .map_err(RouterError::Forwarding)?
4636 {
4637 let completion = self
4638 .forwarding
4639 .complete_module_control_rpc(
4640 connection_id,
4641 frame.header.corr,
4642 None,
4643 ModuleControlRpcOutcome::MalformedResponse(format!(
4644 "malformed {expected_op} response body: {err}"
4645 )),
4646 )
4647 .map_err(RouterError::Forwarding)?;
4648 if !self.observe_module_control_completion(completion) {
4649 debug!(
4650 connection_id = connection_id.get(),
4651 corr = frame.header.corr,
4652 "dropping late malformed module-control RPC response"
4653 );
4654 }
4655 return Ok(Vec::new());
4656 }
4657 let message = format!("malformed route.bind response body: {err}");
4658 secondary_error = Some(control_error_frame(
4659 &frame,
4660 "invalid_control_body",
4661 message.clone(),
4662 )?);
4663 RouteBindRelayOutcome::ModuleGone(message)
4664 }
4665 },
4666 FrameType::Error => {
4667 if self
4668 .forwarding
4669 .pending_module_control_op(connection_id, frame.header.corr)
4670 .map_err(RouterError::Forwarding)?
4671 .is_some()
4672 {
4673 let outcome = match serde_json::from_slice::<ErrorBody>(&frame.body) {
4674 Ok(body) => ModuleControlRpcOutcome::Rejected(body),
4675 Err(err) => ModuleControlRpcOutcome::MalformedResponse(format!(
4676 "malformed module-control ERROR body: {err}"
4677 )),
4678 };
4679 let completion = self
4680 .forwarding
4681 .complete_module_control_rpc(
4682 connection_id,
4683 frame.header.corr,
4684 None,
4685 outcome,
4686 )
4687 .map_err(RouterError::Forwarding)?;
4688 if !self.observe_module_control_completion(completion) {
4689 debug!(
4690 connection_id = connection_id.get(),
4691 corr = frame.header.corr,
4692 "dropping late or unknown module-control RPC error"
4693 );
4694 }
4695 return Ok(Vec::new());
4696 }
4697 match serde_json::from_slice::<ErrorBody>(&frame.body) {
4698 Ok(body) => RouteBindRelayOutcome::Rejected(body),
4699 Err(err) => {
4700 let message = format!("malformed route.bind ERROR body: {err}");
4701 secondary_error = Some(control_error_frame(
4702 &frame,
4703 "invalid_control_body",
4704 message.clone(),
4705 )?);
4706 RouteBindRelayOutcome::ModuleGone(message)
4707 }
4708 }
4709 }
4710 ty => {
4711 return Ok(vec![control_error_frame(
4712 &frame,
4713 "unsupported_control_frame",
4714 format!("unsupported module channel-0 frame {ty:?}"),
4715 )?])
4716 }
4717 };
4718
4719 let settled =
4720 self.forwarding
4721 .complete_pending_relay(connection_id, frame.header.corr, outcome);
4722 let completion = match settled {
4723 Ok(completion) => completion,
4724 Err(err) => {
4725 self.refuse_to_end_module_connection_for_a_client(
4726 connection_id,
4727 frame.header.corr,
4728 err,
4729 )?;
4730 return Ok(secondary_error.into_iter().collect());
4731 }
4732 };
4733 if let Some(target) = completion.abandoned.as_ref() {
4734 send_goodbye_target_best_effort(target, "late accepted route.bind");
4735 }
4736 if !completion.settled {
4737 debug!(
4738 connection_id = connection_id.get(),
4739 corr = frame.header.corr,
4740 frame_type = ?frame.header.ty,
4741 "dropping late or unknown route.bind relay response"
4742 );
4743 }
4744 Ok(secondary_error.into_iter().collect())
4745 }
4746
4747 fn handle_goodbye(&self, connection_id: ConnectionId) -> Result<Vec<Frame>, RouterError> {
4748 debug!(connection_id = connection_id.get(), "handling GOODBYE");
4749 let registrations = self
4750 .deregister_connection(connection_id)
4751 .map_err(|err| RouterError::backend(0, 0, err.to_string()))?;
4752 let released_routes = self
4753 .forwarding
4754 .cleanup_connection(connection_id)
4755 .map_err(RouterError::Forwarding)?;
4756 self.emit_route_goodbyes(released_routes);
4757 if !registrations.is_empty() {
4759 crate::supervise::notify_registration_release();
4760 }
4761 Ok(Vec::new())
4762 }
4763}
4764
4765impl Default for ControlHandler {
4766 fn default() -> Self {
4767 Self::new(Arc::new(Registry::default()))
4768 }
4769}
4770
4771impl crate::supervise::SwapPromotionObserver for ControlHandler {
4772 fn swap_promoted(&self, registration: &crate::registry::ModuleRegistration) {
4773 self.apply_registration_capabilities(registration);
4774 }
4775}
4776
4777fn capability_requirement_status(status: RequirementStatus) -> CapabilityRequirementStatus {
4778 CapabilityRequirementStatus {
4779 consumer: status.consumer,
4780 capability: status.capability,
4781 need: match status.need {
4782 subc_protocol::manifest::CapabilityNeed::Required => "required".to_string(),
4783 subc_protocol::manifest::CapabilityNeed::Optional => "optional".to_string(),
4784 },
4785 verdict: status.verdict.as_str().to_string(),
4786 episode_seq: status.episode_seq,
4787 config_satisfiable: status.config_satisfiable,
4788 runtime_available: status.runtime_available,
4789 detail: status.detail,
4790 }
4791}
4792
4793fn append_capability_problem_detail(
4794 detail: Option<String>,
4795 capability_detail: Option<String>,
4796) -> Option<String> {
4797 match (detail, capability_detail) {
4798 (Some(detail), Some(capability_detail)) => Some(format!("{detail}; {capability_detail}")),
4799 (Some(detail), None) => Some(detail),
4800 (None, Some(capability_detail)) => Some(capability_detail),
4801 (None, None) => None,
4802 }
4803}
4804
4805fn subc_ops() -> Vec<String> {
4806 SUBC_CONTROL_OPS
4807 .iter()
4808 .map(|op| (*op).to_string())
4809 .collect()
4810}
4811
4812fn module_subc_ops() -> Vec<String> {
4813 SUBC_CONTROL_OPS
4814 .iter()
4815 .chain(MODULE_TO_SUBC_CONTROL_OPS.iter())
4816 .map(|op| (*op).to_string())
4817 .collect()
4818}
4819
4820#[cfg(test)]
4821fn module_baseline_control_ops() -> Vec<String> {
4822 MODULE_BASELINE_CONTROL_OPS
4823 .iter()
4824 .map(|op| (*op).to_string())
4825 .collect()
4826}
4827
4828fn effective_module_control_ops(declared: Option<Vec<String>>) -> Vec<String> {
4829 let mut seen = HashSet::new();
4830 let mut effective = Vec::new();
4831 for op in MODULE_BASELINE_CONTROL_OPS {
4832 if seen.insert((*op).to_string()) {
4833 effective.push((*op).to_string());
4834 }
4835 }
4836 for op in declared.unwrap_or_default() {
4837 if seen.insert(op.clone()) {
4838 effective.push(op);
4839 }
4840 }
4841 effective
4842}
4843
4844fn module_registration_grants_op(control_ops: &[String], op: &str) -> bool {
4845 MODULE_BASELINE_CONTROL_OPS.contains(&op) || control_ops.iter().any(|granted| granted == op)
4846}
4847
4848fn target_module_id(target: &RouteTarget) -> &str {
4849 match target {
4850 RouteTarget::ToolProvider { module_id }
4851 | RouteTarget::ManagementSurface { module_id }
4852 | RouteTarget::InternalService { module_id, .. } => module_id,
4853 }
4854}
4855
4856fn target_has_required_role(target: &RouteTarget, roles: &[ProviderRole]) -> bool {
4857 roles.iter().any(|role| match (target, role) {
4858 (RouteTarget::ToolProvider { .. }, ProviderRole::ToolProvider { .. }) => true,
4859 (RouteTarget::ManagementSurface { .. }, ProviderRole::ManagementSurface { .. }) => true,
4860 (
4861 RouteTarget::InternalService { service_id, .. },
4862 ProviderRole::InternalService {
4863 service_id: provided,
4864 ..
4865 },
4866 ) => service_id == provided,
4867 _ => false,
4868 })
4869}
4870
4871fn is_routable_role(role: &ProviderRole) -> bool {
4872 matches!(
4873 role,
4874 ProviderRole::ToolProvider { .. }
4875 | ProviderRole::ManagementSurface { .. }
4876 | ProviderRole::InternalService { .. }
4877 )
4878}
4879
4880#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4881enum ControlRequestBodyError {
4882 UnknownOp,
4883 InvalidBody,
4884}
4885
4886#[derive(Debug, Deserialize)]
4887struct ControlOpProbe {
4888 op: String,
4889}
4890
4891const MODULE_PUSH_OPS: &[&str] = &["route.status"];
4894
4895fn is_known_module_push_op(body: &[u8]) -> bool {
4896 serde_json::from_slice::<ControlOpProbe>(body)
4897 .map(|probe| MODULE_PUSH_OPS.contains(&probe.op.as_str()))
4898 .unwrap_or(false)
4899}
4900
4901fn is_known_module_request_op(body: &[u8]) -> bool {
4902 serde_json::from_slice::<ControlOpProbe>(body)
4903 .map(|probe| MODULE_TO_SUBC_CONTROL_OPS.contains(&probe.op.as_str()))
4904 .unwrap_or(false)
4905}
4906
4907fn log_control_dispatch_arrival(op: &'static str, connection_id: ConnectionId, corr: u64) {
4908 debug!(
4909 op = %op,
4910 connection_id = connection_id.get(),
4911 corr,
4912 "control dispatch"
4913 );
4914}
4915
4916fn log_slow_control_dispatch(
4917 dispatch_started_at: Option<StdInstant>,
4918 op: &'static str,
4919 connection_id: ConnectionId,
4920 corr: u64,
4921) {
4922 let Some(dispatch_started_at) = dispatch_started_at else {
4923 return;
4924 };
4925 let elapsed = dispatch_started_at.elapsed();
4926 if elapsed >= SLOW_CONTROL_DISPATCH_THRESHOLD {
4927 warn!(
4928 op = %op,
4929 connection_id = connection_id.get(),
4930 corr,
4931 elapsed_ms = elapsed.as_millis() as u64,
4932 "slow control dispatch"
4933 );
4934 }
4935}
4936
4937fn client_control_request_op(request: &ClientControlRequest) -> &'static str {
4938 match request {
4939 ClientControlRequest::ServerDescribe {} => ops::SERVER_DESCRIBE,
4940 ClientControlRequest::SupervisorProvenance { .. } => ops::SUPERVISOR_PROVENANCE,
4941 ClientControlRequest::CatalogList { .. } => ops::CATALOG_LIST,
4942 ClientControlRequest::RouteOpen { .. } => ops::ROUTE_OPEN,
4943 ClientControlRequest::RoutePoll { .. } => ops::ROUTE_POLL,
4944 ClientControlRequest::SupervisorList {} => ops::SUPERVISOR_LIST,
4945 ClientControlRequest::SupervisorSpawnSnapshot {} => ops::SUPERVISOR_SPAWN_SNAPSHOT,
4946 ClientControlRequest::SupervisorSpawnSubscribe { .. } => ops::SUPERVISOR_SPAWN_SUBSCRIBE,
4947 ClientControlRequest::SupervisorRestart { .. } => ops::SUPERVISOR_RESTART,
4948 ClientControlRequest::SupervisorSwap { .. } => ops::SUPERVISOR_SWAP,
4949 ClientControlRequest::SupervisorReload { .. } => ops::SUPERVISOR_RELOAD,
4950 ClientControlRequest::SupervisorRescan { .. } => ops::SUPERVISOR_RESCAN,
4951 ClientControlRequest::SupervisorReleaseReserved { .. } => ops::SUPERVISOR_RELEASE_RESERVED,
4952 ClientControlRequest::SupervisorSetEnabled { .. } => ops::SUPERVISOR_SET_ENABLED,
4953 ClientControlRequest::SupervisorHealthProbe { .. } => ops::SUPERVISOR_HEALTH_PROBE,
4954 ClientControlRequest::SupervisorHealth {} => ops::SUPERVISOR_HEALTH,
4955 ClientControlRequest::SupervisorRoutes { .. } => ops::SUPERVISOR_ROUTES,
4956 ClientControlRequest::SupervisorStderrTail { .. } => ops::SUPERVISOR_STDERR_TAIL,
4957 ClientControlRequest::SupervisorTerminals { .. } => ops::SUPERVISOR_TERMINALS,
4958 }
4959}
4960
4961fn module_control_request_op(request: &ModuleControlRequestFromModule) -> &'static str {
4962 match request {
4963 ModuleControlRequestFromModule::CatalogUpdate { .. } => MODULE_TO_SUBC_OP_CATALOG_UPDATE,
4964 ModuleControlRequestFromModule::LiveRoots {} => "supervisor.live_roots",
4965 }
4966}
4967
4968fn parse_client_control_request(
4969 body: &[u8],
4970) -> Result<ClientControlRequest, (serde_json::Error, ControlRequestBodyError)> {
4971 serde_json::from_slice::<ClientControlRequest>(body).map_err(|err| {
4972 let classification = match serde_json::from_slice::<ControlOpProbe>(body) {
4973 Ok(probe) if SUBC_CONTROL_OPS.contains(&probe.op.as_str()) => {
4974 ControlRequestBodyError::InvalidBody
4975 }
4976 Ok(_) => ControlRequestBodyError::UnknownOp,
4977 Err(_) => ControlRequestBodyError::InvalidBody,
4978 };
4979 (err, classification)
4980 })
4981}
4982
4983fn parse_module_control_request_from_module(
4984 body: &[u8],
4985) -> Result<ModuleControlRequestFromModule, (serde_json::Error, ControlRequestBodyError)> {
4986 serde_json::from_slice::<ModuleControlRequestFromModule>(body).map_err(|err| {
4987 let classification = match serde_json::from_slice::<ControlOpProbe>(body) {
4988 Ok(probe) if MODULE_TO_SUBC_CONTROL_OPS.contains(&probe.op.as_str()) => {
4989 ControlRequestBodyError::InvalidBody
4990 }
4991 Ok(_) => ControlRequestBodyError::UnknownOp,
4992 Err(_) => ControlRequestBodyError::InvalidBody,
4993 };
4994 (err, classification)
4995 })
4996}
4997
4998#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
4999enum ProviderRoleKind {
5000 ToolProvider,
5001 PipelineStage,
5002 ManagementSurface,
5003 InternalService,
5004}
5005
5006fn provider_role_kind(role: &ProviderRole) -> ProviderRoleKind {
5007 match role {
5008 ProviderRole::ToolProvider { .. } => ProviderRoleKind::ToolProvider,
5009 ProviderRole::PipelineStage { .. } => ProviderRoleKind::PipelineStage,
5010 ProviderRole::ManagementSurface { .. } => ProviderRoleKind::ManagementSurface,
5011 ProviderRole::InternalService { .. } => ProviderRoleKind::InternalService,
5012 }
5013}
5014
5015fn provider_role_kind_set(roles: &[ProviderRole]) -> BTreeSet<ProviderRoleKind> {
5016 roles.iter().map(provider_role_kind).collect()
5017}
5018
5019fn capability_census_trigger(
5023 old: Option<&CapabilityDeclarations>,
5024 new: Option<&CapabilityDeclarations>,
5025) -> bool {
5026 let old_provides = old
5027 .map(|capabilities| capabilities.provides.iter().collect::<HashSet<_>>())
5028 .unwrap_or_default();
5029 let old_denies = old
5030 .map(|capabilities| capabilities.must_never_reach.iter().collect::<HashSet<_>>())
5031 .unwrap_or_default();
5032 let new = new.cloned().unwrap_or(CapabilityDeclarations {
5033 provides: Vec::new(),
5034 requires: Vec::new(),
5035 must_never_reach: Vec::new(),
5036 });
5037
5038 new.provides
5039 .iter()
5040 .any(|capability| !old_provides.contains(capability))
5041 || new
5042 .must_never_reach
5043 .iter()
5044 .any(|capability| !old_denies.contains(capability))
5045}
5046
5047fn denied_capability<'a>(
5050 opening_manifest: &'a ModuleManifest,
5051 target_manifest: &ModuleManifest,
5052) -> Option<&'a str> {
5053 let opening_capabilities = opening_manifest.capabilities.as_ref()?;
5054 let target_capabilities = target_manifest.capabilities.as_ref()?;
5055 opening_capabilities
5056 .must_never_reach
5057 .iter()
5058 .find(|denied| {
5059 target_capabilities
5060 .provides
5061 .iter()
5062 .any(|provided| provided == *denied)
5063 })
5064 .map(String::as_str)
5065}
5066
5067fn catalog_update_frozen_field_message(
5068 registered: &ModuleManifest,
5069 provides: &[ProviderRole],
5070) -> Option<String> {
5071 let old_has_provides = !registered.provides.is_empty();
5072 let new_has_provides = !provides.is_empty();
5073 if old_has_provides != new_has_provides {
5074 return Some(format!(
5075 "catalog.update cannot change module '{}' between supervision-only and routable; routability is fixed at HELLO",
5076 registered.module_id
5077 ));
5078 }
5079
5080 if provider_role_kind_set(®istered.provides) != provider_role_kind_set(provides) {
5081 return Some(format!(
5082 "catalog.update cannot change provider role kinds for module '{}'; role kinds are fixed at HELLO",
5083 registered.module_id
5084 ));
5085 }
5086
5087 let registered_concurrency = manifest_concurrency(registered);
5088 let mut candidate = registered.clone();
5089 candidate.provides = provides.to_vec();
5090 let candidate_concurrency = manifest_concurrency(&candidate);
5091 if candidate_concurrency != registered_concurrency {
5092 return Some(format!(
5093 "catalog.update cannot change module '{}' concurrency from {:?} to {:?}; concurrency is fixed at HELLO",
5094 registered.module_id, registered_concurrency, candidate_concurrency
5095 ));
5096 }
5097
5098 None
5101}
5102
5103fn manifest_provides_routable_role(manifest: &ModuleManifest) -> bool {
5104 manifest.provides.iter().any(is_routable_role)
5105}
5106
5107fn manifest_concurrency(manifest: &ModuleManifest) -> Concurrency {
5113 manifest
5114 .provides
5115 .iter()
5116 .find_map(|provider| match provider {
5117 ProviderRole::ToolProvider { concurrency, .. }
5118 | ProviderRole::ManagementSurface { concurrency, .. } => Some(concurrency.clone()),
5119 ProviderRole::PipelineStage { .. } | ProviderRole::InternalService { .. } => None,
5120 })
5121 .unwrap_or(Concurrency::ModuleManaged)
5122}
5123
5124fn manifest_concurrency_was_defaulted(raw_hello: &[u8], manifest: &ModuleManifest) -> bool {
5131 let has_management_surface = manifest
5132 .provides
5133 .iter()
5134 .any(|provider| matches!(provider, ProviderRole::ManagementSurface { .. }));
5135 if !has_management_surface {
5136 return false;
5137 }
5138 let Ok(raw) = serde_json::from_slice::<serde_json::Value>(raw_hello) else {
5139 return false;
5140 };
5141 let Some(provides) = raw
5142 .get("manifest")
5143 .and_then(|manifest| manifest.get("provides"))
5144 .and_then(serde_json::Value::as_array)
5145 else {
5146 return false;
5147 };
5148 provides.iter().any(|role| {
5153 role.get("role").and_then(serde_json::Value::as_str) == Some("management_surface")
5154 && role.get("concurrency").is_none()
5155 })
5156}
5157
5158fn negotiate_version(peer_version: u8) -> Result<u8, String> {
5159 if peer_version != PROTOCOL_VERSION {
5160 return Err(format!(
5161 "protocol_ver {peer_version} is unsupported; this daemon requires exactly {PROTOCOL_VERSION}"
5162 ));
5163 }
5164 Ok(PROTOCOL_VERSION)
5165}
5166
5167fn pong(frame: &Frame) -> Result<Frame, RouterError> {
5168 Frame::build_with_version(
5169 response_version(frame),
5170 FrameType::Pong,
5171 frame.header.flags,
5172 0,
5173 0,
5174 frame.header.corr,
5175 Vec::new(),
5176 )
5177 .map_err(RouterError::FrameBuild)
5178}
5179
5180fn control_error_frame(
5181 frame: &Frame,
5182 code: &'static str,
5183 message: impl Into<String>,
5184) -> Result<Frame, RouterError> {
5185 control_error_body_frame(
5186 frame,
5187 ErrorBody {
5188 code: code.to_string(),
5189 message: message.into(),
5190 detail: None,
5191 },
5192 )
5193}
5194
5195fn control_error_body_frame(frame: &Frame, error: ErrorBody) -> Result<Frame, RouterError> {
5196 let body = serde_json::to_vec(&error).map_err(|err| {
5197 RouterError::backend(
5198 0,
5199 frame.header.corr,
5200 format!("failed to encode control ERROR: {err}"),
5201 )
5202 })?;
5203
5204 Frame::build_with_version(
5205 response_version(frame),
5206 FrameType::Error,
5207 control_flags(),
5208 0,
5209 0,
5210 frame.header.corr,
5211 body,
5212 )
5213 .map_err(RouterError::FrameBuild)
5214}
5215
5216fn control_response_body_frame<T: Serialize>(
5217 frame: &Frame,
5218 reply: &T,
5219 label: &'static str,
5220) -> Result<Frame, RouterError> {
5221 let body = serde_json::to_vec(reply).map_err(|err| {
5222 RouterError::backend(
5223 0,
5224 frame.header.corr,
5225 format!("failed to encode {label}: {err}"),
5226 )
5227 })?;
5228
5229 Frame::build_with_version(
5230 response_version(frame),
5231 FrameType::Response,
5232 control_flags(),
5233 0,
5234 0,
5235 frame.header.corr,
5236 body,
5237 )
5238 .map_err(RouterError::FrameBuild)
5239}
5240
5241fn forwarding_error_code(err: &ForwardingError) -> &'static str {
5271 match err {
5272 ForwardingError::NoModuleConnection => "target_unavailable",
5273 ForwardingError::ModuleReloading { .. } => "module_reloading",
5274 ForwardingError::ClientRouteChannelExhausted { .. }
5275 | ForwardingError::ModuleRouteChannelExhausted { .. } => "route_limit",
5276 ForwardingError::StaleModuleEndpoint
5277 | ForwardingError::UnknownReservation { .. }
5278 | ForwardingError::ConnectionClosing { .. }
5279 | ForwardingError::ClientEgressClosed { .. }
5280 | ForwardingError::ModuleEgressUnavailable { .. } => "target_unavailable",
5281 ForwardingError::CandidateSlotOccupied { .. } => "duplicate_module_id",
5284 ForwardingError::RelayCorrelationExhausted
5285 | ForwardingError::RouteOpenBuild(_)
5286 | ForwardingError::Poisoned => "forwarding_error",
5287 }
5288}
5289
5290fn response_version(frame: &Frame) -> u8 {
5291 if (MIN_SUPPORTED_VERSION..=PROTOCOL_VERSION).contains(&frame.header.ver) {
5292 frame.header.ver
5293 } else {
5294 PROTOCOL_VERSION
5295 }
5296}
5297
5298fn control_flags() -> Flags {
5299 Flags::new(false, Priority::Passive, false)
5300}
5301
5302fn send_goodbye_target_best_effort(target: &GoodbyeTarget, context: &str) {
5303 let Ok(frame) = Frame::build_with_version(
5304 target.negotiated_ver,
5305 FrameType::Goodbye,
5306 control_flags(),
5307 target.channel,
5308 target.epoch,
5309 0,
5310 Vec::new(),
5311 ) else {
5312 return;
5313 };
5314 if let Err(err) = target.sink.try_send(frame) {
5315 warn!(
5316 route_channel = target.channel,
5317 route_epoch = target.epoch,
5318 error = %err,
5319 %context,
5320 "route GOODBYE dropped under backpressure"
5321 );
5322 }
5323}
5324
5325pub(crate) fn send_route_control_pushes(
5326 forwarding: &ForwardingTable,
5327 routes: Vec<EndpointRoute>,
5328 push: ClientControlPush,
5329) {
5330 let body = match serde_json::to_vec(&push) {
5331 Ok(body) => body,
5332 Err(err) => {
5333 warn!(error = %err, "failed to serialize route lifecycle control PUSH");
5334 return;
5335 }
5336 };
5337 let mut targets = Vec::new();
5338 for route in routes {
5339 let target = route.goodbye_target;
5340 if let Some(existing) = targets
5341 .iter()
5342 .find(|existing: &&GoodbyeTarget| existing.connection_id == target.connection_id)
5343 {
5344 debug_assert_eq!(
5345 existing.negotiated_ver, target.negotiated_ver,
5346 "one connection cannot negotiate multiple frame versions"
5347 );
5348 continue;
5349 }
5350 targets.push(target);
5351 }
5352 for target in targets {
5353 let frame = match Frame::build_with_version(
5354 target.negotiated_ver,
5355 FrameType::Push,
5356 control_flags(),
5357 0,
5358 0,
5359 0,
5360 body.clone(),
5361 ) {
5362 Ok(frame) => frame,
5363 Err(err) => {
5364 warn!(
5365 route_channel = target.channel,
5366 error = %err,
5367 "failed to build route lifecycle control PUSH frame"
5368 );
5369 continue;
5370 }
5371 };
5372 if let Err(err) = target.sink.try_send(frame) {
5373 if target.close_on_delivery_failure() {
5374 warn!(
5375 target_connection_id = target.connection_id.get(),
5376 route_channel = target.channel,
5377 error = %err,
5378 "route lifecycle control PUSH was not delivered to client; closing target connection"
5379 );
5380 let _ = forwarding.escalate_client_delivery_failure(
5381 target.connection_id,
5382 target.channel,
5383 target.epoch,
5384 CloseReason::new(
5385 "route_lifecycle_push_delivery_failed",
5386 format!(
5387 "failed to enqueue route lifecycle control PUSH for channel {}: {err}",
5388 target.channel
5389 ),
5390 ),
5391 crate::forwarding::UndeliveredFrame {
5392 module_id: target.module_id.as_deref(),
5393 sink: &target.sink,
5394 },
5395 );
5396 }
5397 }
5398 }
5399}
5400
5401#[cfg(test)]
5402mod tests {
5403 use std::{
5404 collections::BTreeMap,
5405 fmt,
5406 path::PathBuf,
5407 sync::{Arc, Mutex},
5408 time::Duration,
5409 };
5410
5411 use serde_json::{json, Value};
5412 use subc_protocol::{
5413 manifest::{
5414 Concurrency, ExecutionMode, IdentityScope, ManagementOperation,
5415 ManagementOperationKind, ObservabilityKind, ObservabilitySurface, ProviderRole, Tool,
5416 },
5417 session::HealthStatus,
5418 FrameType,
5419 };
5420
5421 use super::*;
5422 use crate::{
5423 forwarding::{DataRoute, DataRouteState},
5424 registry::ChannelState,
5425 router::FrameSink,
5426 stderr_tail::DEFAULT_MAX_LINE_BYTES,
5427 supervise::{ModuleSpec, ModuleState, RestartPolicy, Supervisor, SupervisorHandle},
5428 test_support::TestTempDir,
5429 RouteCtx, Router,
5430 };
5431 use tokio::{
5432 sync::mpsc,
5433 time::{sleep, Instant},
5434 };
5435 use tracing::{
5436 field::{Field, Visit},
5437 Event, Subscriber,
5438 };
5439 use tracing_subscriber::{layer::Context, prelude::*, Layer};
5440
5441 fn fake_aft_stub_path() -> PathBuf {
5460 let mut path = std::env::current_exe().expect("current_exe available in tests");
5461 path.pop(); path.pop(); path.push(if cfg!(windows) {
5464 "fake-aft-stub.exe"
5465 } else {
5466 "fake-aft-stub"
5467 });
5468 assert!(
5469 path.exists(),
5470 "fake-aft-stub not built at {}: run `cargo test -p subc-core` (which builds \
5471 [[bin]] targets) rather than `cargo test -p subc-core --lib` (which does not)",
5472 path.display()
5473 );
5474 path
5475 }
5476
5477 fn client_retries(code: &str) -> bool {
5482 subc_protocol::error_codes::is_retryable_route_open(code)
5483 }
5484
5485 #[test]
5496 fn retryability_of_forwarding_codes_matches_the_failure() {
5497 let transient = [
5500 ForwardingError::NoModuleConnection,
5501 ForwardingError::ModuleReloading {
5502 module_id: "m".into(),
5503 },
5504 ForwardingError::StaleModuleEndpoint,
5505 ForwardingError::UnknownReservation {
5506 client_channel: 1,
5507 module_channel: 1,
5508 },
5509 ForwardingError::ConnectionClosing {
5510 connection_id: ConnectionId::new(1),
5511 },
5512 ForwardingError::ClientEgressClosed {
5513 connection_id: ConnectionId::new(1),
5514 },
5515 ForwardingError::ModuleEgressUnavailable {
5516 connection_id: ConnectionId::new(1),
5517 },
5518 ];
5519 for err in transient {
5520 let code = forwarding_error_code(&err);
5521 assert!(
5522 client_retries(code),
5523 "{err:?} is transient but publishes {code:?}, which clients treat as permanent"
5524 );
5525 }
5526
5527 let permanent = [
5532 ForwardingError::ClientRouteChannelExhausted {
5533 connection_id: ConnectionId::new(1),
5534 },
5535 ForwardingError::ModuleRouteChannelExhausted {
5536 endpoint: ModuleEndpointId {
5537 connection_id: ConnectionId::new(1),
5538 generation: 1,
5539 },
5540 },
5541 ForwardingError::RelayCorrelationExhausted,
5542 ForwardingError::RouteOpenBuild("x".into()),
5543 ForwardingError::Poisoned,
5544 ];
5545 for err in permanent {
5546 let code = forwarding_error_code(&err);
5547 assert!(
5548 !client_retries(code),
5549 "{err:?} cannot be fixed by retrying but publishes {code:?}, which clients retry"
5550 );
5551 }
5552 }
5553
5554 #[tokio::test]
5582 async fn an_unattested_caller_is_never_stamped_as_a_supervised_module() {
5583 let handler = ControlHandler::default();
5584 let frame =
5585 Frame::build(FrameType::Request, control_flags(), 0, 0, 900, Vec::new()).unwrap();
5586
5587 let stamped = handler.route_open_principal(&frame, None).unwrap().unwrap();
5591 assert_eq!(
5592 stamped,
5593 Principal::Direct,
5594 "a caller that proved nothing must not be stamped as a supervised module"
5595 );
5596
5597 let forged = handler
5602 .route_open_principal(
5603 &frame,
5604 Some(ConsumerIdentity {
5605 module_id: "aft".to_string(),
5606 launch_nonce: "not-a-real-nonce".to_string(),
5607 }),
5608 )
5609 .unwrap();
5610 let refusal = forged.expect_err("an unmatched launch nonce must not yield a principal");
5611 assert_eq!(parse_error(&refusal)["code"], "bad_consumer_identity");
5612 }
5613
5614 #[test]
5631 fn a_wire_body_actually_yields_the_consumer_identity_the_daemon_stamps_from() {
5632 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"}}"#;
5633 let parsed: ClientControlRequest = serde_json::from_slice(body).unwrap();
5634 let ClientControlRequest::RouteOpen {
5635 consumer_identity, ..
5636 } = parsed
5637 else {
5638 panic!("route.open body must parse as RouteOpen");
5639 };
5640 assert_eq!(
5641 consumer_identity,
5642 Some(ConsumerIdentity {
5643 module_id: "aft".to_string(),
5644 launch_nonce: "n".to_string(),
5645 }),
5646 "the wire field name must reach the value route_open_principal reads"
5647 );
5648 }
5649
5650 fn manifest(module_id: &str, protocol_ver: u8) -> ModuleManifest {
5651 ModuleManifest::builder(module_id, "0.1.0")
5652 .protocol_ver(protocol_ver)
5653 .provides(vec![ProviderRole::ToolProvider {
5654 tools: vec![Tool {
5655 name: "read".to_string(),
5656 description: None,
5657 execution_mode: ExecutionMode::Pure,
5658 schema: json!({"type": "object"}),
5659 }],
5660 identity_scope: vec![IdentityScope::Project, IdentityScope::Session],
5661 concurrency: Concurrency::ModuleManaged,
5662 emits_push: true,
5663 sub_supervises: true,
5664 }])
5665 .build()
5666 }
5667
5668 fn hello_frame(module_id: &str, protocol_ver: u8, corr: u64) -> Frame {
5669 hello_frame_with_control_ops(module_id, protocol_ver, corr, None)
5670 }
5671
5672 fn hello_frame_with_control_ops(
5673 module_id: &str,
5674 protocol_ver: u8,
5675 corr: u64,
5676 control_ops: Option<Vec<String>>,
5677 ) -> Frame {
5678 hello_frame_full(module_id, protocol_ver, corr, control_ops, None)
5679 }
5680
5681 fn hello_frame_with_nonce(
5682 module_id: &str,
5683 protocol_ver: u8,
5684 corr: u64,
5685 launch_nonce: Option<&str>,
5686 ) -> Frame {
5687 hello_frame_full(
5688 module_id,
5689 protocol_ver,
5690 corr,
5691 None,
5692 launch_nonce.map(ToOwned::to_owned),
5693 )
5694 }
5695
5696 fn hello_frame_full(
5697 module_id: &str,
5698 protocol_ver: u8,
5699 corr: u64,
5700 control_ops: Option<Vec<String>>,
5701 launch_nonce: Option<String>,
5702 ) -> Frame {
5703 let body = serde_json::to_vec(&ModuleHelloBody {
5704 manifest: manifest(module_id, protocol_ver),
5705 protocol_ver,
5706 control_ops,
5707 launch_nonce,
5708 })
5709 .unwrap();
5710 Frame::build(FrameType::Hello, control_flags(), 0, 0, corr, body).unwrap()
5711 }
5712
5713 fn non_routable_hello_frame_with_control_ops(
5714 module_id: &str,
5715 corr: u64,
5716 control_ops: Option<Vec<String>>,
5717 ) -> Frame {
5718 let mut manifest = manifest(module_id, PROTOCOL_VERSION);
5719 manifest.provides.clear();
5720 let body = serde_json::to_vec(&ModuleHelloBody {
5721 manifest,
5722 protocol_ver: PROTOCOL_VERSION,
5723 control_ops,
5724 launch_nonce: None,
5725 })
5726 .unwrap();
5727 Frame::build(FrameType::Hello, control_flags(), 0, 0, corr, body).unwrap()
5728 }
5729
5730 fn capability_grammar_hello_frame(
5731 capabilities: Value,
5732 runtime_computed: Option<Value>,
5733 corr: u64,
5734 ) -> Frame {
5735 let mut body = serde_json::to_value(ModuleHelloBody {
5736 manifest: manifest("capability-grammar-test", PROTOCOL_VERSION),
5737 protocol_ver: PROTOCOL_VERSION,
5738 control_ops: None,
5739 launch_nonce: None,
5740 })
5741 .expect("HELLO body serializes");
5742 body["manifest"]["capabilities"] = capabilities;
5743 if let Some(runtime_computed) = runtime_computed {
5744 body["runtime_computed"] = runtime_computed;
5745 }
5746 Frame::build(
5747 FrameType::Hello,
5748 control_flags(),
5749 0,
5750 0,
5751 corr,
5752 serde_json::to_vec(&body).expect("HELLO body reserializes"),
5753 )
5754 .expect("HELLO frame builds")
5755 }
5756
5757 fn channel_request(channel: u16, corr: u64) -> Frame {
5758 Frame::build(
5759 FrameType::Request,
5760 Flags::new(true, Priority::Interactive, false),
5761 channel,
5762 0,
5763 corr,
5764 b"opaque".to_vec(),
5765 )
5766 .unwrap()
5767 }
5768
5769 fn route_ctx(
5770 connection_id: ConnectionId,
5771 ) -> (RouteCtx, mpsc::Receiver<crate::router::OutboundFrame>) {
5772 let (tx, rx) = mpsc::channel(8);
5773 (
5774 RouteCtx {
5775 connection_id,
5776 egress: FrameSink::new(tx),
5777 },
5778 rx,
5779 )
5780 }
5781
5782 fn parse_ack(frame: &Frame) -> ModuleHelloAckBody {
5783 serde_json::from_slice(&frame.body).unwrap()
5784 }
5785
5786 async fn hello_via_sink(
5791 handler: &ControlHandler,
5792 ctx: &RouteCtx,
5793 rx: &mut mpsc::Receiver<crate::router::OutboundFrame>,
5794 hello: Frame,
5795 ) -> Frame {
5796 let replies = handler.handle_control_frame(ctx, hello).await.unwrap();
5797 assert!(
5798 replies.is_empty(),
5799 "a registered HELLO replies with nothing; its ack is already queued: {replies:?}"
5800 );
5801 let ack = rx
5802 .try_recv()
5803 .expect("HELLO_ACK is queued on the module sink")
5804 .frame;
5805 assert_eq!(ack.header.ty, FrameType::HelloAck);
5806 ack
5807 }
5808
5809 fn parse_error(frame: &Frame) -> Value {
5810 serde_json::from_slice(&frame.body).unwrap()
5811 }
5812
5813 fn parse_route_poll(frame: &Frame) -> ClientControlResponse {
5814 serde_json::from_slice(&frame.body).unwrap()
5815 }
5816
5817 fn route_poll_frame(corr: u64, kind: PollKind, route_channel: u16) -> Frame {
5818 let body = serde_json::to_vec(&ClientControlRequest::RoutePoll {
5819 route_channel,
5820 route_epoch: 0,
5821 kind,
5822 })
5823 .unwrap();
5824 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
5825 }
5826
5827 fn supervisor_health_probe_frame(corr: u64, module_id: &str) -> Frame {
5828 let body = serde_json::to_vec(&ClientControlRequest::SupervisorHealthProbe {
5829 module_id: module_id.to_string(),
5830 })
5831 .unwrap();
5832 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
5833 }
5834
5835 fn route_open_frame(corr: u64, module_id: &str, project_root: TestTempDir) -> Frame {
5836 route_open_frame_with_consumer_capabilities(corr, module_id, project_root, None)
5837 }
5838
5839 fn route_open_frame_with_consumer_capabilities(
5840 corr: u64,
5841 module_id: &str,
5842 project_root: TestTempDir,
5843 consumer_capabilities: Option<Vec<String>>,
5844 ) -> Frame {
5845 let body = serde_json::to_vec(&ClientControlRequest::RouteOpen {
5846 target: RouteTarget::ToolProvider {
5847 module_id: module_id.to_string(),
5848 },
5849 identity: BindIdentity::new(
5850 project_root.path().to_path_buf(),
5851 "unit".to_string(),
5852 "session".to_string(),
5853 ),
5854 consumer_identity: None,
5855 consumer_capabilities,
5856 admission_facts: None,
5857 })
5858 .unwrap();
5859 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
5860 }
5861
5862 fn route_open_frame_with_admission_facts(
5863 corr: u64,
5864 module_id: &str,
5865 project_root: TestTempDir,
5866 consumer_identity: Option<subc_control::ConsumerIdentity>,
5867 facts: Option<Value>,
5868 ) -> Frame {
5869 let body = serde_json::to_vec(&ClientControlRequest::RouteOpen {
5870 target: RouteTarget::ToolProvider {
5871 module_id: module_id.to_string(),
5872 },
5873 identity: BindIdentity::new(
5874 project_root.path().to_path_buf(),
5875 "unit".to_string(),
5876 format!("session-{corr}"),
5877 ),
5878 consumer_identity,
5879 consumer_capabilities: None,
5880 admission_facts: facts,
5881 })
5882 .unwrap();
5883 Frame::build(FrameType::Request, control_flags(), 0, 0, corr, body).unwrap()
5884 }
5885
5886 #[derive(Clone, Default)]
5887 struct EventCapture {
5888 events: Arc<Mutex<Vec<CapturedEvent>>>,
5889 }
5890
5891 #[derive(Clone, Debug)]
5892 struct CapturedEvent {
5893 target: String,
5894 fields: BTreeMap<String, String>,
5895 }
5896
5897 impl EventCapture {
5898 fn events(&self) -> Vec<CapturedEvent> {
5899 self.events.lock().unwrap().clone()
5900 }
5901 }
5902
5903 impl<S> Layer<S> for EventCapture
5904 where
5905 S: Subscriber,
5906 {
5907 fn on_event(&self, event: &Event<'_>, _context: Context<'_, S>) {
5908 let mut visitor = EventFieldVisitor::default();
5909 event.record(&mut visitor);
5910 self.events.lock().unwrap().push(CapturedEvent {
5911 target: event.metadata().target().to_string(),
5912 fields: visitor.fields,
5913 });
5914 }
5915 }
5916
5917 #[derive(Default)]
5918 struct EventFieldVisitor {
5919 fields: BTreeMap<String, String>,
5920 }
5921
5922 impl Visit for EventFieldVisitor {
5923 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
5924 self.fields
5925 .insert(field.name().to_string(), format!("{value:?}"));
5926 }
5927 }
5928
5929 fn health_response(corr: u64, status: HealthStatus) -> Frame {
5930 let body = serde_json::to_vec(&ModuleControlResponse::HealthCheck {
5931 status,
5932 detail: Some("warming".to_string()),
5933 metrics: Some(json!({"queue_depth": 3})),
5934 })
5935 .unwrap();
5936 Frame::build(FrameType::Response, control_flags(), 0, 0, corr, body).unwrap()
5937 }
5938
5939 fn route_bind_ack(corr: u64) -> Frame {
5940 let body = serde_json::to_vec(&ModuleControlResponse::RouteBindAck {}).unwrap();
5941 Frame::build(FrameType::Response, control_flags(), 0, 0, corr, body).unwrap()
5942 }
5943
5944 fn unique_project_root(label: &str) -> TestTempDir {
5945 TestTempDir::new(label)
5946 }
5947
5948 fn assert_route_poll_liveness(frame: &Frame, expected_live: bool) {
5949 match parse_route_poll(frame) {
5950 ClientControlResponse::RoutePoll {
5951 status: None,
5952 live: Some(live),
5953 ..
5954 } => assert_eq!(live, expected_live),
5955 other => panic!("unexpected route.poll response: {other:?}"),
5956 }
5957 }
5958
5959 fn bind_liveness_route(
5960 registry: &Registry,
5961 forwarding: &ForwardingTable,
5962 module_id: &str,
5963 ) -> (RouteCtx, u16, u32) {
5964 let module_connection = ConnectionId::new(101);
5965 let client_connection = ConnectionId::new(202);
5966 let registration = registry
5967 .register_with_control_ops(
5968 manifest(module_id, PROTOCOL_VERSION),
5969 PROTOCOL_VERSION,
5970 module_connection,
5971 module_baseline_control_ops(),
5972 )
5973 .unwrap();
5974 let (module_tx, _module_rx) = mpsc::channel(8);
5975 let endpoint = forwarding
5976 .register_module_connection(
5977 module_connection,
5978 module_id.to_string(),
5979 PROTOCOL_VERSION,
5980 manifest_concurrency(®istration.manifest),
5981 FrameSink::new(module_tx),
5982 )
5983 .unwrap();
5984 let (client_ctx, _client_rx) = route_ctx(client_connection);
5985 let pending = forwarding
5986 .begin_route_bind_relay_for_test(
5987 client_connection,
5988 client_ctx.egress.clone(),
5989 1,
5990 module_id,
5991 )
5992 .unwrap();
5993 assert_eq!(pending.endpoint, endpoint);
5994 let route_channel = pending.client_channel;
5995 let route_epoch = pending.client_epoch;
5996 forwarding
5997 .complete_pending_relay(
5998 module_connection,
5999 pending.corr,
6000 RouteBindRelayOutcome::Accepted,
6001 )
6002 .unwrap();
6003 (client_ctx, route_channel, route_epoch)
6004 }
6005
6006 struct FakeProcessLiveness {
6007 live: Option<bool>,
6008 }
6009
6010 impl ModuleProcessLiveness for FakeProcessLiveness {
6011 fn process_live(&self, _module_id: &str) -> Option<bool> {
6012 self.live
6013 }
6014 }
6015
6016 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6017 async fn supervisor_stderr_tail_converts_a_real_truncated_ring_entry_to_prefix_only_wire_data()
6018 {
6019 let registry = Arc::new(Registry::default());
6020 let supervisor_handle = SupervisorHandle::new();
6021 let supervisor = Supervisor::new(
6022 Arc::clone(®istry),
6023 RestartPolicy::new(1, Duration::from_millis(10)),
6024 )
6025 .with_handle(supervisor_handle.clone());
6026 let source_line = format!("config error: {}", "x".repeat(DEFAULT_MAX_LINE_BYTES));
6027 let module = supervisor
6028 .spawn(ModuleSpec {
6029 module_id: "stderr-tail-wire".to_string(),
6030 program: fake_aft_stub_path(),
6031 args: Vec::new(),
6032 env: vec![
6033 ("FAKE_AFT_STDERR_LINE".to_string(), source_line.clone()),
6034 ("FAKE_AFT_EXIT_CODE".to_string(), "1".to_string()),
6035 ],
6036 reserved: false,
6037 reserved_prefixes: Vec::new(),
6038 protocol: ModuleProtocol::Subc,
6039 overlap: Default::default(),
6040 })
6041 .unwrap();
6042
6043 let deadline = Instant::now() + Duration::from_secs(5);
6044 loop {
6045 let tail = module.stderr_tail(None, None);
6046 if tail
6047 .entries
6048 .iter()
6049 .any(|entry| matches!(entry, TailEntry::ProcessStart))
6050 && tail.entries.iter().any(|entry| {
6051 matches!(
6052 entry,
6053 TailEntry::Line {
6054 truncated: true,
6055 ..
6056 }
6057 )
6058 })
6059 {
6060 break;
6061 }
6062 assert!(
6063 Instant::now() < deadline,
6064 "module did not produce a truncated line and restart boundary: {tail:?}"
6065 );
6066 sleep(Duration::from_millis(10)).await;
6067 }
6068
6069 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
6070 let request = ClientControlRequest::SupervisorStderrTail {
6071 module_id: "stderr-tail-wire".to_string(),
6072 max_lines: None,
6073 max_bytes: None,
6074 };
6075 let frame = Frame::build(
6076 FrameType::Request,
6077 control_flags(),
6078 0,
6079 0,
6080 1,
6081 serde_json::to_vec(&request).unwrap(),
6082 )
6083 .unwrap();
6084 let (ctx, _egress) = route_ctx(ConnectionId::new(1));
6085 let responses = handler.handle_control_frame(&ctx, frame).await.unwrap();
6086 let ClientControlResponse::SupervisorStderrTail { tail, .. } =
6087 serde_json::from_slice(&responses[0].body).unwrap()
6088 else {
6089 panic!("expected supervisor.stderr_tail response");
6090 };
6091
6092 assert!(
6093 tail.entries
6094 .iter()
6095 .any(|entry| matches!(entry, StderrTailEntry::ProcessStart)),
6096 "the control response lost the restart boundary"
6097 );
6098 let Some(StderrTailEntry::Line { text, truncated }) = tail.entries.iter().find(|entry| {
6099 matches!(
6100 entry,
6101 StderrTailEntry::Line {
6102 truncated: true,
6103 ..
6104 }
6105 )
6106 }) else {
6107 panic!("the control response lost the truncated line");
6108 };
6109 assert_eq!(text, &source_line[..DEFAULT_MAX_LINE_BYTES]);
6110 assert!(*truncated);
6111 }
6112
6113 #[tokio::test(flavor = "current_thread")]
6118 async fn supervisor_terminals_reads_the_journal_off_the_runtime_worker() {
6119 let dir = TestTempDir::new("terminals-off-worker");
6120 let journal_path = dir.join("terminals.jsonl");
6121 let registry = Arc::new(Registry::default());
6122 let supervisor_handle = SupervisorHandle::new();
6123 let supervisor =
6124 Supervisor::new(Arc::clone(®istry), RestartPolicy::new(1, Duration::ZERO))
6125 .with_handle(supervisor_handle.clone())
6126 .with_terminal_journal(journal_path.clone(), "off-worker-daemon".to_string());
6127 let module = supervisor
6128 .spawn(ModuleSpec {
6129 module_id: "terminal-off-worker".to_string(),
6130 program: fake_aft_stub_path(),
6131 args: Vec::new(),
6132 env: vec![("FAKE_AFT_EXIT_CODE".to_string(), "23".to_string())],
6133 reserved: false,
6134 reserved_prefixes: Vec::new(),
6135 protocol: ModuleProtocol::Subc,
6136 overlap: Default::default(),
6137 })
6138 .unwrap();
6139 let deadline = Instant::now() + Duration::from_secs(5);
6140 while module.terminal_history().entries.len() != 2 {
6141 assert!(Instant::now() < deadline, "module did not record two exits");
6142 sleep(Duration::from_millis(10)).await;
6143 }
6144
6145 let (started, release) = crate::terminal_journal::read_pause::install(&journal_path);
6146 let handler =
6147 Arc::new(ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle));
6148 let frame = Frame::build(
6149 FrameType::Request,
6150 control_flags(),
6151 0,
6152 0,
6153 1,
6154 serde_json::to_vec(&ClientControlRequest::SupervisorTerminals {
6155 module_id: "terminal-off-worker".to_string(),
6156 })
6157 .unwrap(),
6158 )
6159 .unwrap();
6160 let (ctx, _egress) = route_ctx(ConnectionId::new(1));
6161 let spawned_at = std::time::Instant::now();
6162 let read = tokio::spawn({
6163 let handler = Arc::clone(&handler);
6164 async move { handler.handle_control_frame(&ctx, frame).await }
6165 });
6166 tokio::task::spawn_blocking(move || started.recv_timeout(Duration::from_secs(5)))
6169 .await
6170 .unwrap()
6171 .expect("the history read reached its pause");
6172 let elapsed = spawned_at.elapsed();
6173 assert!(
6174 elapsed < Duration::from_secs(2) && !read.is_finished(),
6175 "this task could not run while the history read was paused \
6176 (resumed after {elapsed:?}, read finished: {})",
6177 read.is_finished()
6178 );
6179
6180 drop(release);
6181 let responses = read.await.unwrap().unwrap();
6182 let response: ClientControlResponse = serde_json::from_slice(&responses[0].body).unwrap();
6183 let ClientControlResponse::SupervisorTerminals { terminals, .. } = response else {
6184 panic!("expected supervisor.terminals response");
6185 };
6186 assert_eq!(terminals.entries.len(), 2);
6187 assert_eq!(terminals.journal_skipped_lines, 0);
6188 assert_eq!(terminals.journal_read_errors, 0);
6189 }
6190
6191 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6192 async fn supervisor_terminals_golden_is_generated_through_the_real_handler() {
6193 let registry = Arc::new(Registry::default());
6194 let supervisor_handle = SupervisorHandle::new();
6195 let supervisor =
6196 Supervisor::new(Arc::clone(®istry), RestartPolicy::new(1, Duration::ZERO))
6197 .with_handle(supervisor_handle.clone());
6198 let module = supervisor
6199 .spawn(ModuleSpec {
6200 module_id: "terminal-golden".to_string(),
6201 program: fake_aft_stub_path(),
6202 args: Vec::new(),
6203 env: vec![("FAKE_AFT_EXIT_CODE".to_string(), "23".to_string())],
6204 reserved: false,
6205 reserved_prefixes: Vec::new(),
6206 protocol: ModuleProtocol::Subc,
6207 overlap: Default::default(),
6208 })
6209 .unwrap();
6210
6211 let deadline = Instant::now() + Duration::from_secs(5);
6212 while module.terminal_history().entries.len() != 2 {
6213 assert!(
6214 Instant::now() < deadline,
6215 "module did not retain two terminal exits: {:?}",
6216 module.terminal_history()
6217 );
6218 sleep(Duration::from_millis(10)).await;
6219 }
6220
6221 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
6222 let request = ClientControlRequest::SupervisorTerminals {
6223 module_id: "terminal-golden".to_string(),
6224 };
6225 let frame = Frame::build(
6226 FrameType::Request,
6227 control_flags(),
6228 0,
6229 0,
6230 1,
6231 serde_json::to_vec(&request).unwrap(),
6232 )
6233 .unwrap();
6234 let (ctx, _egress) = route_ctx(ConnectionId::new(1));
6235 let responses = handler.handle_control_frame(&ctx, frame).await.unwrap();
6236 let response: ClientControlResponse = serde_json::from_slice(&responses[0].body).unwrap();
6237 let ClientControlResponse::SupervisorTerminals { terminals, .. } = &response else {
6238 panic!("expected supervisor.terminals response");
6239 };
6240 assert_eq!(terminals.entries.len(), 2);
6241 assert_eq!(terminals.dropped, 0);
6242
6243 let mut rendered = serde_json::to_value(response).unwrap();
6244 rendered["daemon_started_at_ms"] = json!(1_700_000_000_000u64);
6247 for (index, entry) in rendered["entries"]
6248 .as_array_mut()
6249 .expect("terminal response entries array")
6250 .iter_mut()
6251 .enumerate()
6252 {
6253 entry["at_ms"] = json!(1_700_000_000_001u64 + index as u64);
6254 }
6255
6256 let golden_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6257 .join("../subc-control/tests/golden/client_control_response_supervisor_terminals.json");
6258 let serialized = serde_json::to_string_pretty(&rendered).unwrap() + "\n";
6259 if std::env::var_os("UPDATE_GOLDEN").is_some() {
6260 std::fs::write(&golden_path, &serialized).unwrap();
6261 }
6262 let expected: Value =
6263 serde_json::from_str(&std::fs::read_to_string(&golden_path).unwrap()).unwrap();
6264 assert_eq!(rendered, expected);
6265 }
6266
6267 #[test]
6268 fn hello_registers_manifest_and_returns_ack() {
6269 let registry = Arc::new(Registry::default());
6270 let handler = ControlHandler::new(Arc::clone(®istry));
6271 let conn = ConnectionId::new(1);
6272
6273 let responses = handler
6274 .handle_control(conn, hello_frame("aft", PROTOCOL_VERSION, 7))
6275 .unwrap();
6276
6277 assert_eq!(responses.len(), 1);
6278 assert_eq!(responses[0].header.ty, FrameType::HelloAck);
6279 assert_eq!(responses[0].header.channel, 0);
6280 assert_eq!(responses[0].header.corr, 7);
6281 let ack = parse_ack(&responses[0]);
6282 assert_eq!(ack.negotiated_ver, PROTOCOL_VERSION);
6283 assert!(ack
6284 .subc_capabilities
6285 .contains(&CAP_MANIFEST_REGISTRATION.to_string()));
6286 assert!(ack.subc_ops.contains(&ops::SUPERVISOR_LIST.to_string()));
6287 assert!(ack.subc_ops.contains(&ops::SUPERVISOR_RESTART.to_string()));
6288 assert!(ack
6289 .subc_ops
6290 .contains(&ops::SUPERVISOR_SET_ENABLED.to_string()));
6291 assert!(ack
6292 .subc_ops
6293 .contains(&MODULE_TO_SUBC_OP_CATALOG_UPDATE.to_string()));
6294
6295 let registration = registry.get_module("aft").unwrap().unwrap();
6296 assert_eq!(registration.negotiated_ver, PROTOCOL_VERSION);
6297 assert_eq!(registration.state, ChannelState::Active);
6298 assert_eq!(registration.connection_id, conn);
6299 assert_eq!(registration.control_ops, module_baseline_control_ops());
6300 }
6301
6302 #[test]
6303 fn capability_grammar_refusals_name_the_field_and_leave_no_catalog_entry() {
6304 let invalid_identifiers = [
6305 ("case_change", "credentials-Provider/v1"),
6306 ("leading_zero", "credentials-provider/v01"),
6307 ("trailing_hyphen", "credentials-provider-/v1"),
6308 ("consecutive_hyphens", "credentials--provider/v1"),
6309 ("uppercase", "Credentials-provider/v1"),
6310 ("missing_v", "credentials-provider/1"),
6311 ("whitespace", "credentials provider/v1"),
6312 ("zero_version", "credentials-provider/v0"),
6313 ("out_of_range_version", "credentials-provider/v4294967296"),
6314 (
6315 "overlength_name",
6316 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/v1",
6317 ),
6318 ];
6319 let mut cases = invalid_identifiers
6320 .into_iter()
6321 .map(|(name, identifier)| {
6322 (
6323 format!("identifier_{name}"),
6324 "capabilities.provides[0]".to_string(),
6325 identifier.to_string(),
6326 json!({ "provides": [identifier] }),
6327 None,
6328 )
6329 })
6330 .collect::<Vec<_>>();
6331 cases.extend([
6332 (
6333 "unknown_need".to_string(),
6334 "capabilities.requires[0].need".to_string(),
6335 "deferred".to_string(),
6336 json!({ "requires": [{ "capability": "credentials-provider/v1", "need": "deferred" }] }),
6337 None,
6338 ),
6339 (
6340 "duplicate_provides".to_string(),
6341 "capabilities.provides[1]".to_string(),
6342 "credentials-provider/v1".to_string(),
6343 json!({ "provides": ["credentials-provider/v1", "credentials-provider/v1"] }),
6344 None,
6345 ),
6346 (
6347 "duplicate_must_never_reach".to_string(),
6348 "capabilities.must_never_reach[1]".to_string(),
6349 "credentials-provider/v1".to_string(),
6350 json!({ "must_never_reach": ["credentials-provider/v1", "credentials-provider/v1"] }),
6351 None,
6352 ),
6353 (
6354 "duplicate_requires_same_need".to_string(),
6355 "capabilities.requires[1]".to_string(),
6356 "credentials-provider/v1".to_string(),
6357 json!({ "requires": [
6358 { "capability": "credentials-provider/v1", "need": "required" },
6359 { "capability": "credentials-provider/v1", "need": "required" }
6360 ] }),
6361 None,
6362 ),
6363 (
6364 "duplicate_requires_conflicting_need".to_string(),
6365 "capabilities.requires[1]".to_string(),
6366 "credentials-provider/v1".to_string(),
6367 json!({ "requires": [
6368 { "capability": "credentials-provider/v1", "need": "required" },
6369 { "capability": "credentials-provider/v1", "need": "optional" }
6370 ] }),
6371 None,
6372 ),
6373 (
6374 "capabilities_root_pointer".to_string(),
6375 "runtime_computed[0]".to_string(),
6376 "/capabilities".to_string(),
6377 json!({}),
6378 Some(json!(["/capabilities"])),
6379 ),
6380 (
6381 "capabilities_descendant_pointer".to_string(),
6382 "runtime_computed[0]".to_string(),
6383 "/capabilities/provides".to_string(),
6384 json!({}),
6385 Some(json!(["/capabilities/provides"])),
6386 ),
6387 (
6388 "malformed_pointer_without_leading_slash".to_string(),
6389 "runtime_computed[0]".to_string(),
6390 "capabilities".to_string(),
6391 json!({}),
6392 Some(json!(["capabilities"])),
6393 ),
6394 (
6395 "malformed_pointer_escape".to_string(),
6396 "runtime_computed[0]".to_string(),
6397 "/roles/~2/tools".to_string(),
6398 json!({}),
6399 Some(json!(["/roles/~2/tools"])),
6400 ),
6401 (
6402 "unknown_capabilities_field".to_string(),
6403 "capabilities.future".to_string(),
6404 "<array>".to_string(),
6405 json!({ "future": [] }),
6406 None,
6407 ),
6408 ]);
6409
6410 for (index, (name, field, value, capabilities, runtime_computed)) in
6411 cases.into_iter().enumerate()
6412 {
6413 let registry = Arc::new(Registry::default());
6414 let handler = ControlHandler::new(Arc::clone(®istry));
6415 let response = handler
6416 .handle_control(
6417 ConnectionId::new((index + 1) as u64),
6418 capability_grammar_hello_frame(
6419 capabilities,
6420 runtime_computed,
6421 index as u64 + 1,
6422 ),
6423 )
6424 .expect("invalid HELLO returns a refusal");
6425
6426 assert_eq!(response.len(), 1, "{name} must emit one refusal");
6427 let error = parse_error(&response[0]);
6428 assert_eq!(error["code"], "invalid_capability_grammar", "{name}");
6429 let message = error["message"]
6430 .as_str()
6431 .expect("error message is a string");
6432 assert!(
6433 message.contains(&field),
6434 "{name}: field missing from {message}"
6435 );
6436 assert!(
6437 message.contains(&value),
6438 "{name}: value missing from {message}"
6439 );
6440 assert_eq!(
6441 registry
6442 .active_registration_count()
6443 .expect("registry reads"),
6444 0,
6445 "{name}: refused HELLO must not create a catalog entry"
6446 );
6447 }
6448 }
6449
6450 #[test]
6451 fn legal_runtime_pointer_and_capabilities_are_mirrored_in_catalog_list() {
6452 let registry = Arc::new(Registry::default());
6453 let handler = ControlHandler::new(Arc::clone(®istry));
6454 let capabilities = json!({
6455 "provides": ["credentials-provider/v1"],
6456 "requires": [{ "capability": "context-transform/v1", "need": "optional" }],
6457 "must_never_reach": ["federation-transport/v1"]
6458 });
6459 let response = handler
6460 .handle_control(
6461 ConnectionId::new(99),
6462 capability_grammar_hello_frame(
6463 capabilities.clone(),
6464 Some(json!(["/roles/0/tools"])),
6465 99,
6466 ),
6467 )
6468 .expect("valid HELLO registers");
6469 assert_eq!(response[0].header.ty, FrameType::HelloAck);
6470
6471 let request = Frame::build(
6472 FrameType::Request,
6473 control_flags(),
6474 0,
6475 0,
6476 100,
6477 serde_json::to_vec(&ClientControlRequest::CatalogList { module_id: None })
6478 .expect("catalog request serializes"),
6479 )
6480 .expect("catalog request frame builds");
6481 let response = handler
6482 .handle_catalog_list(request, None)
6483 .expect("catalog list succeeds");
6484 let ClientControlResponse::CatalogList { modules, .. } =
6485 serde_json::from_slice(&response[0].body).expect("catalog response decodes")
6486 else {
6487 panic!("catalog request must return catalog.list");
6488 };
6489 assert_eq!(modules.len(), 1);
6490 assert_eq!(
6491 serde_json::to_value(&modules[0].capabilities).expect("catalog capabilities serialize"),
6492 capabilities
6493 );
6494 }
6495
6496 #[test]
6497 fn catalog_list_mirrors_management_operation_description() {
6498 let registry = Arc::new(Registry::default());
6499 let handler = ControlHandler::new(Arc::clone(®istry));
6500 let description = "List managed records and return their identifiers and metadata.";
6501 let mut manifest = manifest("described-management", PROTOCOL_VERSION);
6502 manifest.provides = vec![ProviderRole::ManagementSurface {
6503 operations: vec![ManagementOperation {
6504 name: "records.list".to_string(),
6505 kind: ManagementOperationKind::Query,
6506 description: Some(description.to_string()),
6507 }],
6508 config_schema: json!({"type": "object"}),
6509 observability: vec![ObservabilitySurface {
6510 name: "records.stats".to_string(),
6511 kind: ObservabilityKind::Snapshot,
6512 }],
6513 identity_scope: vec![IdentityScope::Project],
6514 concurrency: Concurrency::ModuleManaged,
6515 }];
6516 registry
6517 .register_with_control_ops(
6518 manifest,
6519 PROTOCOL_VERSION,
6520 ConnectionId::new(99),
6521 Vec::new(),
6522 )
6523 .expect("described management manifest registers");
6524
6525 let request = Frame::build(
6526 FrameType::Request,
6527 control_flags(),
6528 0,
6529 0,
6530 100,
6531 serde_json::to_vec(&ClientControlRequest::CatalogList { module_id: None })
6532 .expect("catalog request serializes"),
6533 )
6534 .expect("catalog request frame builds");
6535 let response = handler
6536 .handle_catalog_list(request, None)
6537 .expect("catalog list succeeds");
6538 let body: Value = serde_json::from_slice(&response[0].body).expect("catalog response JSON");
6539 assert_eq!(
6540 body["modules"][0]["roles"][0]["operations"][0]["description"], description,
6541 "catalog.list must preserve the declared operation description verbatim"
6542 );
6543 }
6544
6545 #[test]
6546 fn reserved_capability_refusal_mutation_proof_leaves_no_catalog_entry() {
6547 let registry = Arc::new(Registry::default());
6548 let handler = ControlHandler::new(Arc::clone(®istry)).with_capability_config(
6549 [("vault".to_string(), true), ("squatter".to_string(), true)],
6550 BTreeMap::from([("credentials-provider/v1".to_string(), "vault".to_string())]),
6551 );
6552 let mut squatter = manifest("squatter", PROTOCOL_VERSION);
6553 squatter.capabilities = Some(subc_protocol::manifest::CapabilityDeclarations {
6554 provides: vec!["credentials-provider/v1".to_string()],
6555 requires: Vec::new(),
6556 must_never_reach: Vec::new(),
6557 });
6558 let frame = Frame::build(
6559 FrameType::Hello,
6560 control_flags(),
6561 0,
6562 0,
6563 77,
6564 serde_json::to_vec(&ModuleHelloBody {
6565 manifest: squatter,
6566 protocol_ver: PROTOCOL_VERSION,
6567 control_ops: None,
6568 launch_nonce: None,
6569 })
6570 .expect("HELLO serializes"),
6571 )
6572 .expect("HELLO frame builds");
6573 let response = handler
6574 .handle_control(ConnectionId::new(77), frame)
6575 .expect("reserved claim receives a typed refusal");
6576 assert_eq!(parse_error(&response[0])["code"], "reserved_capability");
6577 assert_eq!(
6578 registry
6579 .active_registration_count()
6580 .expect("registry reads"),
6581 0,
6582 "a reserved capability refusal must not leave a catalog entry"
6583 );
6584 }
6585
6586 #[test]
6587 fn server_describe_surfaces_required_capability_verdict_fields() {
6588 let registry = Arc::new(Registry::default());
6589 let handler = ControlHandler::new(Arc::clone(®istry)).with_capability_config(
6590 [
6591 ("consumer".to_string(), true),
6592 ("provider".to_string(), false),
6593 ],
6594 BTreeMap::new(),
6595 );
6596 let mut consumer = manifest("consumer", PROTOCOL_VERSION);
6597 consumer.capabilities = Some(subc_protocol::manifest::CapabilityDeclarations {
6598 provides: Vec::new(),
6599 requires: vec![subc_protocol::manifest::CapabilityRequirement {
6600 capability: "credentials-provider/v1".to_string(),
6601 need: subc_protocol::manifest::CapabilityNeed::Required,
6602 }],
6603 must_never_reach: Vec::new(),
6604 });
6605 let hello = Frame::build(
6606 FrameType::Hello,
6607 control_flags(),
6608 0,
6609 0,
6610 78,
6611 serde_json::to_vec(&ModuleHelloBody {
6612 manifest: consumer,
6613 protocol_ver: PROTOCOL_VERSION,
6614 control_ops: None,
6615 launch_nonce: None,
6616 })
6617 .expect("HELLO serializes"),
6618 )
6619 .expect("HELLO frame builds");
6620 handler
6621 .handle_control(ConnectionId::new(78), hello)
6622 .expect("consumer registers");
6623 let describe = Frame::build(
6624 FrameType::Request,
6625 control_flags(),
6626 0,
6627 0,
6628 79,
6629 serde_json::to_vec(&ClientControlRequest::ServerDescribe {})
6630 .expect("request serializes"),
6631 )
6632 .expect("describe frame builds");
6633 let response = handler
6634 .handle_server_describe(describe)
6635 .expect("server.describe succeeds");
6636 let rendered: Value = serde_json::from_slice(&response[0].body).expect("response JSON");
6637 let requirement = &rendered["capability_requirements"][0];
6638 assert_eq!(requirement["consumer"], "consumer");
6639 assert_eq!(requirement["verdict"], "never_provided");
6640 assert_eq!(requirement["episode_seq"], 1);
6641 assert_eq!(requirement["config_satisfiable"], false);
6642 assert_eq!(requirement["runtime_available"], false);
6643 assert!(requirement["detail"]
6644 .as_str()
6645 .expect("detail string")
6646 .contains("credentials-provider/v1"));
6647 }
6648
6649 #[test]
6650 fn catalog_list_omits_capabilities_for_legacy_manifest() {
6651 let registry = Arc::new(Registry::default());
6652 let handler = ControlHandler::new(Arc::clone(®istry));
6653 let hello = handler
6654 .handle_control(
6655 ConnectionId::new(101),
6656 hello_frame("legacy-capability-manifest", PROTOCOL_VERSION, 101),
6657 )
6658 .expect("legacy HELLO registers");
6659 assert_eq!(hello[0].header.ty, FrameType::HelloAck);
6660
6661 let request = Frame::build(
6662 FrameType::Request,
6663 control_flags(),
6664 0,
6665 0,
6666 102,
6667 serde_json::to_vec(&ClientControlRequest::CatalogList { module_id: None })
6668 .expect("catalog request serializes"),
6669 )
6670 .expect("catalog request frame builds");
6671 let response = handler
6672 .handle_catalog_list(request, None)
6673 .expect("catalog list succeeds");
6674 let body: Value = serde_json::from_slice(&response[0].body).expect("catalog response JSON");
6675 assert!(
6676 body["modules"][0].get("capabilities").is_none(),
6677 "legacy manifest must retain an absent capabilities field on catalog.list"
6678 );
6679 }
6680
6681 #[test]
6682 fn hello_ack_omits_storage_when_no_storage_config() {
6683 let registry = Arc::new(Registry::default());
6684 let handler = ControlHandler::new(Arc::clone(®istry));
6685 let responses = handler
6686 .handle_control(
6687 ConnectionId::new(1),
6688 hello_frame("aft", PROTOCOL_VERSION, 7),
6689 )
6690 .unwrap();
6691 let ack = parse_ack(&responses[0]);
6692 assert_eq!(ack.storage, None, "no storage config -> no descriptor");
6693 assert_eq!(ack.machine_id, None, "no machine id configured -> no field");
6694 }
6695
6696 #[tokio::test]
6697 async fn hello_ack_and_server_describe_carry_the_configured_machine_id() {
6698 let id = crate::machine_id::MachineId::parse("0123456789abcdef0123456789abcdef").unwrap();
6699 let registry = Arc::new(Registry::default());
6700 let handler = ControlHandler::new(Arc::clone(®istry)).with_machine_id(Some(id.clone()));
6701 let responses = handler
6702 .handle_control(
6703 ConnectionId::new(1),
6704 hello_frame("aft", PROTOCOL_VERSION, 7),
6705 )
6706 .unwrap();
6707 let ack = parse_ack(&responses[0]);
6708 assert_eq!(ack.machine_id.as_deref(), Some(id.as_str()));
6709
6710 let described = handler
6711 .handle_control_frame(
6712 &route_ctx(ConnectionId::new(2)).0,
6713 Frame::build(
6714 FrameType::Request,
6715 control_flags(),
6716 0,
6717 0,
6718 9,
6719 serde_json::to_vec(&ClientControlRequest::ServerDescribe {}).unwrap(),
6720 )
6721 .unwrap(),
6722 )
6723 .await
6724 .unwrap();
6725 let ClientControlResponse::ServerDescribe { machine_id, .. } =
6726 serde_json::from_slice(&described[0].body).unwrap()
6727 else {
6728 panic!("server.describe answered with another shape");
6729 };
6730 assert_eq!(machine_id.as_deref(), Some(id.as_str()));
6731 }
6732
6733 #[test]
6734 fn hello_ack_delivers_resolved_storage_descriptor_per_module() {
6735 let registry = Arc::new(Registry::default());
6738 let handler = ControlHandler::new(Arc::clone(®istry)).with_storage_config(Some(
6739 crate::daemon_config::StorageConfig::Sqlite {
6740 data_home: std::path::PathBuf::from("/data"),
6741 },
6742 ));
6743
6744 let responses = handler
6745 .handle_control(
6746 ConnectionId::new(1),
6747 hello_frame("alfonso-routing", PROTOCOL_VERSION, 7),
6748 )
6749 .unwrap();
6750 let ack = parse_ack(&responses[0]);
6751 assert_eq!(
6752 ack.storage,
6753 Some(serde_json::json!({
6754 "module_id": "alfonso-routing",
6755 "storage_namespace": "default",
6756 "isolation": { "kind": "module" },
6757 "backend": {
6758 "backend": "sqlite",
6759 "path": "/data/cortexkit/alfonso-routing/store.db"
6760 }
6761 })),
6762 "the delivered descriptor is the module's own sqlite store path"
6763 );
6764 }
6765
6766 #[test]
6767 fn hello_control_ops_none_is_baseline_and_guard_rejects_synthetic_gated_op() {
6768 let registry = Arc::new(Registry::default());
6769 let handler = ControlHandler::new(Arc::clone(®istry));
6770 let conn = ConnectionId::new(1);
6771 let responses = handler
6772 .handle_control(
6773 conn,
6774 hello_frame_with_control_ops("aft", PROTOCOL_VERSION, 7, None),
6775 )
6776 .unwrap();
6777 assert_eq!(responses[0].header.ty, FrameType::HelloAck);
6778 let registration = registry.get_module("aft").unwrap().unwrap();
6779 assert_eq!(registration.control_ops, module_baseline_control_ops());
6780
6781 let frame =
6782 Frame::build(FrameType::Request, control_flags(), 0, 0, 77, Vec::new()).unwrap();
6783 assert!(handler
6784 .guard_module_control_op(&frame, "aft", "route.bind")
6785 .unwrap()
6786 .is_none());
6787 let error = handler
6788 .guard_module_control_op(&frame, "aft", "test.synthetic")
6789 .unwrap()
6790 .expect("synthetic ungranted op should be rejected");
6791 assert_eq!(error.header.ty, FrameType::Error);
6792 assert_eq!(parse_error(&error)["code"], "op_not_allowed");
6793 }
6794
6795 #[test]
6796 fn hello_control_ops_some_adds_optional_grants() {
6797 let registry = Arc::new(Registry::default());
6798 let handler = ControlHandler::new(Arc::clone(®istry));
6799 handler
6800 .handle_control(
6801 ConnectionId::new(1),
6802 hello_frame_with_control_ops(
6803 "aft",
6804 PROTOCOL_VERSION,
6805 7,
6806 Some(vec![
6807 "future.synthetic".to_string(),
6808 "route.bind".to_string(),
6809 ]),
6810 ),
6811 )
6812 .unwrap();
6813 let registration = registry.get_module("aft").unwrap().unwrap();
6814 assert_eq!(
6815 registration.control_ops,
6816 vec![
6817 "route.bind".to_string(),
6818 "route.status".to_string(),
6819 "future.synthetic".to_string(),
6820 ]
6821 );
6822 let frame =
6823 Frame::build(FrameType::Request, control_flags(), 0, 0, 78, Vec::new()).unwrap();
6824 assert!(handler
6825 .guard_module_control_op(&frame, "aft", "future.synthetic")
6826 .unwrap()
6827 .is_none());
6828 }
6829
6830 #[tokio::test]
6831 async fn health_probe_refuses_unadvertised_module_without_sending_frame() {
6832 let registry = Arc::new(Registry::default());
6833 let forwarding = Arc::new(ForwardingTable::default());
6834 let handler =
6835 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
6836 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(10));
6837 hello_via_sink(
6838 &handler,
6839 &module_ctx,
6840 &mut module_rx,
6841 hello_frame_with_control_ops("aft", PROTOCOL_VERSION, 7, None),
6842 )
6843 .await;
6844
6845 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(20));
6846 let responses = handler
6847 .handle_control_frame(&client_ctx, supervisor_health_probe_frame(77, "aft"))
6848 .await
6849 .unwrap();
6850 assert_eq!(responses.len(), 1);
6851 assert_eq!(responses[0].header.ty, FrameType::Error);
6852 assert_eq!(parse_error(&responses[0])["code"], "health_not_advertised");
6853 assert!(module_rx.try_recv().is_err());
6854 }
6855
6856 #[tokio::test]
6857 async fn health_probe_demuxes_while_route_bind_relay_is_in_flight() {
6858 let registry = Arc::new(Registry::default());
6859 let forwarding = Arc::new(ForwardingTable::default());
6860 let handler =
6861 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
6862 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(30));
6863 hello_via_sink(
6864 &handler,
6865 &module_ctx,
6866 &mut module_rx,
6867 hello_frame_with_control_ops(
6868 "aft",
6869 PROTOCOL_VERSION,
6870 7,
6871 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
6872 ),
6873 )
6874 .await;
6875
6876 let project_root = unique_project_root("demux");
6877 let (route_client_ctx, mut route_client_rx) = route_ctx(ConnectionId::new(31));
6878 let route_handler = handler.clone();
6879 let route_task = tokio::spawn(async move {
6880 route_handler
6881 .handle_control_frame(
6882 &route_client_ctx,
6883 route_open_frame(100, "aft", project_root),
6884 )
6885 .await
6886 .unwrap()
6887 });
6888 let bind_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
6889 .await
6890 .unwrap()
6891 .unwrap();
6892 assert!(matches!(
6893 serde_json::from_slice::<ModuleControlRequest>(&bind_frame.body).unwrap(),
6894 ModuleControlRequest::RouteBind { .. }
6895 ));
6896
6897 let (health_client_ctx, _health_client_rx) = route_ctx(ConnectionId::new(32));
6898 let health_handler = handler.clone();
6899 let health_task = tokio::spawn(async move {
6900 health_handler
6901 .handle_control_frame(
6902 &health_client_ctx,
6903 supervisor_health_probe_frame(101, "aft"),
6904 )
6905 .await
6906 .unwrap()
6907 });
6908 let health_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
6909 .await
6910 .unwrap()
6911 .unwrap();
6912 assert_eq!(
6913 serde_json::from_slice::<ModuleControlRequest>(&health_frame.body).unwrap(),
6914 ModuleControlRequest::HealthCheck {}
6915 );
6916
6917 handler
6918 .handle_control_frame(
6919 &module_ctx,
6920 health_response(health_frame.header.corr, HealthStatus::Degraded),
6921 )
6922 .await
6923 .unwrap();
6924 let health_response = health_task.await.unwrap();
6925 assert_eq!(health_response.len(), 1);
6926 match serde_json::from_slice::<ClientControlResponse>(&health_response[0].body).unwrap() {
6927 ClientControlResponse::SupervisorHealthProbe {
6928 module_id,
6929 status,
6930 detail,
6931 metrics,
6932 } => {
6933 assert_eq!(module_id, "aft");
6934 assert_eq!(status, HealthStatus::Degraded);
6935 assert_eq!(detail.as_deref(), Some("warming"));
6936 assert_eq!(metrics, Some(json!({"queue_depth": 3})));
6937 }
6938 other => panic!("unexpected health response: {other:?}"),
6939 }
6940
6941 handler
6942 .handle_control_frame(&module_ctx, route_bind_ack(bind_frame.header.corr))
6943 .await
6944 .unwrap();
6945 let route_response = route_task.await.unwrap();
6946 assert!(route_response.is_empty());
6947 let published = route_client_rx.recv().await.unwrap();
6948 assert!(matches!(
6949 serde_json::from_slice::<ClientControlResponse>(&published.body).unwrap(),
6950 ClientControlResponse::RouteOpen { .. }
6951 ));
6952 }
6953
6954 async fn relay_route_open(
6959 handler: &ControlHandler,
6960 client_connection: ConnectionId,
6961 client_egress: &FrameSink,
6962 module_rx: &mut mpsc::Receiver<crate::router::OutboundFrame>,
6963 corr: u64,
6964 module_id: &str,
6965 project_root_label: &str,
6966 ) -> (tokio::task::JoinHandle<Vec<Frame>>, Frame) {
6967 let ctx = RouteCtx {
6968 connection_id: client_connection,
6969 egress: client_egress.clone(),
6970 };
6971 let handler = handler.clone();
6972 let project_root = unique_project_root(project_root_label);
6973 let module_id = module_id.to_string();
6974 let dispatch = tracing::dispatcher::get_default(|dispatch| dispatch.clone());
6975 let task = tokio::spawn(async move {
6976 let _guard = tracing::dispatcher::set_default(&dispatch);
6977 handler
6978 .handle_control_frame(&ctx, route_open_frame(corr, &module_id, project_root))
6979 .await
6980 .unwrap()
6981 });
6982 let bind = tokio::time::timeout(Duration::from_secs(2), module_rx.recv())
6983 .await
6984 .expect("module receives the relayed route.bind")
6985 .expect("module egress is open");
6986 (task, bind.frame)
6987 }
6988
6989 fn route_bind_channel(frame: &Frame) -> (u16, u32) {
6990 match serde_json::from_slice::<ModuleControlRequest>(&frame.body).unwrap() {
6991 ModuleControlRequest::RouteBind {
6992 route_channel,
6993 epoch,
6994 ..
6995 } => (route_channel, epoch),
6996 other => panic!("expected a route.bind request, got {other:?}"),
6997 }
6998 }
6999
7000 fn published_route(frame: &Frame) -> (u16, u32) {
7001 match serde_json::from_slice::<ClientControlResponse>(&frame.body).unwrap() {
7002 ClientControlResponse::RouteOpen {
7003 route_channel,
7004 route_epoch,
7005 } => (route_channel, route_epoch),
7006 other => panic!("expected a route.open response, got {other:?}"),
7007 }
7008 }
7009
7010 #[tokio::test]
7030 async fn late_bind_ack_for_a_closing_client_keeps_the_module_connection_serving() {
7031 let registry = Arc::new(Registry::default());
7032 let forwarding = Arc::new(ForwardingTable::default());
7033 let handler =
7034 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
7035
7036 let module_connection = ConnectionId::new(30);
7037 let (module_ctx, mut module_rx) = route_ctx(module_connection);
7038 hello_via_sink(
7039 &handler,
7040 &module_ctx,
7041 &mut module_rx,
7042 hello_frame("aft", PROTOCOL_VERSION, 7),
7043 )
7044 .await;
7045
7046 let dying_client = ConnectionId::new(31);
7047 let (dying_ctx, mut dying_rx) = route_ctx(dying_client);
7048
7049 let (first_task, first_bind) = relay_route_open(
7052 &handler,
7053 dying_client,
7054 &dying_ctx.egress,
7055 &mut module_rx,
7056 100,
7057 "aft",
7058 "closing-first",
7059 )
7060 .await;
7061 handler
7062 .handle_control_frame(&module_ctx, route_bind_ack(first_bind.header.corr))
7063 .await
7064 .unwrap();
7065 assert!(first_task.await.unwrap().is_empty());
7066 let (first_channel, first_epoch) = published_route(&dying_rx.recv().await.unwrap());
7067
7068 let (second_task, second_bind) = relay_route_open(
7070 &handler,
7071 dying_client,
7072 &dying_ctx.egress,
7073 &mut module_rx,
7074 101,
7075 "aft",
7076 "closing-second",
7077 )
7078 .await;
7079 let (abandoned_channel, abandoned_epoch) = route_bind_channel(&second_bind);
7080
7081 assert!(forwarding
7084 .escalate_client_delivery_failure(
7085 dying_client,
7086 first_channel,
7087 first_epoch,
7088 CloseReason::new(
7089 "module_to_client_delivery_failed",
7090 "client egress refused a module frame",
7091 ),
7092 crate::forwarding::UndeliveredFrame {
7093 module_id: None,
7094 sink: &dying_ctx.egress,
7095 },
7096 )
7097 .unwrap());
7098 assert!(!dying_ctx.egress.is_closed());
7099
7100 let ack = handler
7102 .handle_control_frame(&module_ctx, route_bind_ack(second_bind.header.corr))
7103 .await;
7104 let module_loop_error = ack.as_ref().err().map(ToString::to_string);
7105 if module_loop_error.is_some() {
7106 handler.cleanup_connection(module_connection).unwrap();
7110 }
7111 let post_ack_module_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
7115 .await
7116 .ok()
7117 .flatten();
7118
7119 assert!(
7121 registry
7122 .get_module_by_connection(module_connection)
7123 .unwrap()
7124 .is_some(),
7125 "one client's closing connection ended the shared module connection: \
7126 {module_loop_error:?}"
7127 );
7128 let cotenant = ConnectionId::new(32);
7130 let (cotenant_ctx, mut cotenant_rx) = route_ctx(cotenant);
7131 let (cotenant_task, cotenant_bind) = relay_route_open(
7132 &handler,
7133 cotenant,
7134 &cotenant_ctx.egress,
7135 &mut module_rx,
7136 102,
7137 "aft",
7138 "closing-cotenant",
7139 )
7140 .await;
7141 handler
7142 .handle_control_frame(&module_ctx, route_bind_ack(cotenant_bind.header.corr))
7143 .await
7144 .unwrap();
7145 assert!(cotenant_task.await.unwrap().is_empty());
7146 let (cotenant_channel, cotenant_epoch) =
7147 published_route(&cotenant_rx.recv().await.unwrap());
7148 assert!(matches!(
7149 forwarding
7150 .lookup_data_route(cotenant, cotenant_channel, cotenant_epoch)
7151 .unwrap(),
7152 DataRoute::Client(DataRouteState::Bound(_))
7153 ));
7154
7155 let goodbye = post_ack_module_frame
7158 .expect("module receives a GOODBYE for the abandoned route channel");
7159 assert_eq!(goodbye.header.ty, FrameType::Goodbye);
7160 assert_eq!(goodbye.header.channel, abandoned_channel);
7161 assert_eq!(goodbye.header.epoch, abandoned_epoch);
7162
7163 assert!(dying_rx.try_recv().is_err());
7167 let second_response = second_task.await.unwrap();
7168 assert_eq!(second_response.len(), 1);
7169 assert_eq!(
7170 parse_error(&second_response[0])["code"],
7171 "target_unavailable"
7172 );
7173 }
7174
7175 #[test]
7182 fn only_the_modules_own_closing_connection_ends_the_module_loop() {
7183 let handler = ControlHandler::default();
7184 let module_connection = ConnectionId::new(30);
7185 let client_connection = ConnectionId::new(31);
7186
7187 handler
7188 .refuse_to_end_module_connection_for_a_client(
7189 module_connection,
7190 77,
7191 ForwardingError::ConnectionClosing {
7192 connection_id: client_connection,
7193 },
7194 )
7195 .expect("a closing client must never end the module connection");
7196
7197 assert!(matches!(
7198 handler.refuse_to_end_module_connection_for_a_client(
7199 module_connection,
7200 78,
7201 ForwardingError::ConnectionClosing {
7202 connection_id: module_connection,
7203 },
7204 ),
7205 Err(RouterError::Forwarding(ForwardingError::ConnectionClosing {
7206 connection_id
7207 })) if connection_id == module_connection
7208 ));
7209 assert!(matches!(
7210 handler.refuse_to_end_module_connection_for_a_client(
7211 module_connection,
7212 79,
7213 ForwardingError::Poisoned,
7214 ),
7215 Err(RouterError::Forwarding(ForwardingError::Poisoned))
7216 ));
7217 assert!(matches!(
7218 handler.refuse_to_end_module_connection_for_a_client(
7219 module_connection,
7220 80,
7221 ForwardingError::StaleModuleEndpoint,
7222 ),
7223 Err(RouterError::Forwarding(
7224 ForwardingError::StaleModuleEndpoint
7225 ))
7226 ));
7227 }
7228
7229 #[tokio::test]
7242 async fn route_open_refuses_consumer_identity_that_fails_spawn_attestation() {
7243 let registry = Arc::new(Registry::default());
7244 let forwarding = Arc::new(ForwardingTable::default());
7245 let supervisor = SupervisorHandle::new();
7246 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
7247 let handler =
7248 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
7249 .with_supervisor(supervisor);
7250
7251 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(90));
7252 hello_via_sink(
7253 &handler,
7254 &target_ctx,
7255 &mut target_rx,
7256 hello_frame("target", PROTOCOL_VERSION, 1),
7257 )
7258 .await;
7259
7260 let wrong_nonce = handler
7264 .handle_control_frame(
7265 &route_ctx(ConnectionId::new(91)).0,
7266 route_open_frame_with_admission_facts(
7267 20,
7268 "target",
7269 unique_project_root("admission-facts"),
7270 Some(subc_control::ConsumerIdentity {
7271 module_id: "fed".to_string(),
7272 launch_nonce: "not-the-real-nonce".to_string(),
7273 }),
7274 None,
7275 ),
7276 )
7277 .await
7278 .unwrap();
7279 assert_eq!(
7280 parse_error(&wrong_nonce[0])["code"],
7281 "bad_consumer_identity",
7282 "a mismatched launch nonce must be refused, not stamped Reserved"
7283 );
7284
7285 let never_spawned = handler
7289 .handle_control_frame(
7290 &route_ctx(ConnectionId::new(92)).0,
7291 route_open_frame_with_admission_facts(
7292 21,
7293 "target",
7294 unique_project_root("admission-facts"),
7295 Some(subc_control::ConsumerIdentity {
7296 module_id: "never-spawned".to_string(),
7297 launch_nonce: "any-nonce".to_string(),
7298 }),
7299 None,
7300 ),
7301 )
7302 .await
7303 .unwrap();
7304 assert_eq!(
7305 parse_error(&never_spawned[0])["code"],
7306 "bad_consumer_identity",
7307 "an unspawned module_id must be refused rather than accepted for lack of a record"
7308 );
7309 }
7310
7311 #[tokio::test]
7334 async fn route_open_stamps_reserved_for_a_correctly_attested_consumer() {
7335 let registry = Arc::new(Registry::default());
7336 let forwarding = Arc::new(ForwardingTable::default());
7337 let supervisor = SupervisorHandle::new();
7338 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
7339 let handler =
7340 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
7341 .with_supervisor(supervisor);
7342
7343 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(95));
7344 hello_via_sink(
7345 &handler,
7346 &target_ctx,
7347 &mut target_rx,
7348 hello_frame("target", PROTOCOL_VERSION, 1),
7349 )
7350 .await;
7351
7352 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(96));
7353 let route_handler = handler.clone();
7354 let route_task = tokio::spawn(async move {
7355 route_handler
7356 .handle_control_frame(
7357 &client_ctx,
7358 route_open_frame_with_admission_facts(
7359 30,
7360 "target",
7361 unique_project_root("admission-facts"),
7362 Some(subc_control::ConsumerIdentity {
7363 module_id: "fed".to_string(),
7364 launch_nonce: "fed-nonce".to_string(),
7365 }),
7366 None,
7367 ),
7368 )
7369 .await
7370 .unwrap()
7371 });
7372
7373 let bind_frame = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
7380 .await
7381 .expect("no route.bind within 5s: the consumer-identity guard refused a correctly attested consumer")
7382 .expect("module control channel closed before route.bind");
7383 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
7384 let ModuleControlRequest::RouteBind { principal, .. } = bind else {
7385 panic!("expected route.bind")
7386 };
7387 assert_eq!(
7388 principal,
7389 Some(Principal::Reserved {
7390 module_id: "fed".to_string()
7391 }),
7392 "a correctly attested consumer must be stamped Reserved for its own id"
7393 );
7394
7395 handler
7396 .handle_control_frame(&target_ctx, route_bind_ack(bind_frame.header.corr))
7397 .await
7398 .unwrap();
7399 assert!(route_task.await.unwrap().is_empty());
7400 assert!(
7401 matches!(
7402 serde_json::from_slice::<ClientControlResponse>(
7403 &client_rx.recv().await.unwrap().body
7404 )
7405 .unwrap(),
7406 ClientControlResponse::RouteOpen { .. }
7407 ),
7408 "the route must actually open, not merely avoid an error"
7409 );
7410 }
7411
7412 #[tokio::test(start_paused = true)]
7413 async fn supervisor_routes_serializes_live_draining_bindings_from_the_real_handler() {
7414 let registry = Arc::new(Registry::default());
7415 let forwarding = Arc::new(ForwardingTable::default());
7416 let supervisor = SupervisorHandle::new();
7417 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
7418 let handler =
7419 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
7420 .with_supervisor(supervisor);
7421
7422 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(101));
7423 hello_via_sink(
7424 &handler,
7425 &target_ctx,
7426 &mut target_rx,
7427 hello_frame("target", PROTOCOL_VERSION, 1),
7428 )
7429 .await;
7430
7431 let (direct_ctx, mut direct_rx) = route_ctx(ConnectionId::new(102));
7432 let direct_handler = handler.clone();
7433 let direct_open = tokio::spawn(async move {
7434 direct_handler
7435 .handle_control_frame(
7436 &direct_ctx,
7437 route_open_frame(2, "target", unique_project_root("route-census-direct")),
7438 )
7439 .await
7440 .unwrap()
7441 });
7442 let direct_bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
7443 .await
7444 .expect("no direct route.bind within 5s")
7445 .expect("target control channel closed before direct route.bind");
7446 handler
7447 .handle_control_frame(&target_ctx, route_bind_ack(direct_bind.header.corr))
7448 .await
7449 .unwrap();
7450 assert!(direct_open.await.unwrap().is_empty());
7451 let _ = direct_rx.recv().await.unwrap();
7452
7453 let (reserved_ctx, mut reserved_rx) = route_ctx(ConnectionId::new(103));
7454 let reserved_handler = handler.clone();
7455 let reserved_open = tokio::spawn(async move {
7456 reserved_handler
7457 .handle_control_frame(
7458 &reserved_ctx,
7459 route_open_frame_with_admission_facts(
7460 3,
7461 "target",
7462 unique_project_root("admission-facts"),
7463 Some(ConsumerIdentity {
7464 module_id: "fed".to_string(),
7465 launch_nonce: "fed-nonce".to_string(),
7466 }),
7467 None,
7468 ),
7469 )
7470 .await
7471 .unwrap()
7472 });
7473 let reserved_bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
7474 .await
7475 .expect("no reserved route.bind within 5s")
7476 .expect("target control channel closed before reserved route.bind");
7477 handler
7478 .handle_control_frame(&target_ctx, route_bind_ack(reserved_bind.header.corr))
7479 .await
7480 .unwrap();
7481 assert!(reserved_open.await.unwrap().is_empty());
7482 let _ = reserved_rx.recv().await.unwrap();
7483
7484 forwarding
7485 .begin_module_drain("target", subc_control::RouteCloseReason::Reload)
7486 .unwrap();
7487 let (census_ctx, _census_rx) = route_ctx(ConnectionId::new(104));
7488 let census_body = serde_json::to_vec(&ClientControlRequest::SupervisorRoutes {
7489 module_id: Some("target".to_string()),
7490 })
7491 .unwrap();
7492 let census_frame =
7493 Frame::build(FrameType::Request, control_flags(), 0, 0, 4, census_body).unwrap();
7494 let response = handler
7495 .handle_control_frame(&census_ctx, census_frame)
7496 .await
7497 .unwrap()
7498 .pop()
7499 .unwrap();
7500 let actual: Value = serde_json::from_slice(&response.body).unwrap();
7501 let decoded: ClientControlResponse = serde_json::from_value(actual.clone()).unwrap();
7502 assert!(matches!(
7503 decoded,
7504 ClientControlResponse::SupervisorRoutes { .. }
7505 ));
7506 let routes = actual["modules"][0]["routes"].as_array().unwrap();
7507 assert_eq!(routes.len(), 2);
7508 assert!(routes.iter().all(|route| route["draining"] == true));
7509 assert!(
7512 routes.iter().all(|route| route["drain_reason"] == "reload"),
7513 "draining routes must name the drain's reason: {routes:?}"
7514 );
7515 assert!(routes.iter().any(|route| {
7516 route["consumer"] == serde_json::json!({"kind": "direct", "connection_id": 102})
7517 }));
7518 assert!(routes.iter().any(|route| {
7519 route["consumer"] == serde_json::json!({"kind": "reserved", "module_id": "fed"})
7520 }));
7521
7522 let golden_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7523 .join("../subc-control/tests/golden/client_control_response_supervisor_routes.json");
7524 if std::env::var_os("UPDATE_GOLDEN").is_some() {
7525 std::fs::write(
7526 &golden_path,
7527 format!("{}\n", serde_json::to_string_pretty(&actual).unwrap()),
7528 )
7529 .unwrap();
7530 }
7531 let expected: Value =
7532 serde_json::from_str(&std::fs::read_to_string(golden_path).unwrap()).unwrap();
7533 assert_eq!(actual, expected);
7534 }
7535
7536 async fn query_live_roots(
7537 handler: &ControlHandler,
7538 module_ctx: &RouteCtx,
7539 ) -> ModuleControlResponseToModule {
7540 let body = serde_json::to_vec(&ModuleControlRequestFromModule::LiveRoots {}).unwrap();
7541 let frame = Frame::build(FrameType::Request, control_flags(), 0, 0, 900, body).unwrap();
7542 let response = handler
7543 .handle_control_frame(module_ctx, frame)
7544 .await
7545 .unwrap()
7546 .pop()
7547 .unwrap();
7548 serde_json::from_slice(&response.body).unwrap()
7549 }
7550
7551 #[tokio::test(start_paused = true)]
7552 async fn supervisor_live_roots_root_known_arm_counts_bound_and_pending_from_real_handler() {
7553 let registry = Arc::new(Registry::default());
7554 let forwarding = Arc::new(ForwardingTable::default());
7555 let handler = ControlHandler::with_forwarding(registry, forwarding);
7556 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(301));
7557 hello_via_sink(
7558 &handler,
7559 &target_ctx,
7560 &mut target_rx,
7561 hello_frame("target", PROTOCOL_VERSION, 1),
7562 )
7563 .await;
7564 let root = unique_project_root("live-roots-known");
7565 let path = ProjectRootId::from_path_allowing_missing(root.path())
7566 .unwrap()
7567 .as_path()
7568 .to_path_buf();
7569 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(302));
7570 let open_handler = handler.clone();
7571 let opened = tokio::spawn(async move {
7572 open_handler
7573 .handle_control_frame(&client_ctx, route_open_frame(2, "target", root))
7574 .await
7575 .unwrap()
7576 });
7577 let bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
7578 .await
7579 .unwrap()
7580 .unwrap();
7581 handler
7582 .handle_control_frame(&target_ctx, route_bind_ack(bind.header.corr))
7583 .await
7584 .unwrap();
7585 assert!(opened.await.unwrap().is_empty());
7586 let _ = client_rx.recv().await.unwrap();
7587
7588 let root = unique_project_root("live-roots-pending");
7589 let pending_path = ProjectRootId::from_path_allowing_missing(root.path())
7590 .unwrap()
7591 .as_path()
7592 .to_path_buf();
7593 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(303));
7594 let open_handler = handler.clone();
7595 let pending = tokio::spawn(async move {
7596 open_handler
7597 .handle_control_frame(&client_ctx, route_open_frame(3, "target", root))
7598 .await
7599 .unwrap()
7600 });
7601 let pending_bind = tokio::time::timeout(Duration::from_secs(5), target_rx.recv())
7602 .await
7603 .unwrap()
7604 .unwrap();
7605 let actual = query_live_roots(&handler, &target_ctx).await;
7606 let ModuleControlResponseToModule::LiveRoots {
7607 roots,
7608 unknown_root_bindings,
7609 total_bindings,
7610 } = actual
7611 else {
7612 panic!("expected live roots")
7613 };
7614 assert_eq!(total_bindings, 2, "root-known arm must count live routes");
7615 assert_eq!(unknown_root_bindings, 0);
7616 assert_eq!(
7617 roots.len(),
7618 2,
7619 "root-known arm must retain each canonical root"
7620 );
7621 assert_eq!(
7622 total_bindings,
7623 roots.iter().map(|r| r.bound + r.pending).sum::<u64>() + unknown_root_bindings
7624 );
7625 let counts = roots
7626 .iter()
7627 .map(|root| (root.project_root.clone(), root.bound, root.pending))
7628 .collect::<Vec<_>>();
7629 let mut expected = vec![(path, 1, 0), (pending_path, 0, 1)];
7630 expected.sort_by(|a, b| a.0.cmp(&b.0));
7631 assert_eq!(
7632 counts, expected,
7633 "roots must sort by path and count pending separately"
7634 );
7635 handler
7636 .handle_control_frame(&target_ctx, route_bind_ack(pending_bind.header.corr))
7637 .await
7638 .unwrap();
7639 assert!(pending.await.unwrap().is_empty());
7640 }
7641
7642 #[tokio::test(start_paused = true)]
7643 async fn supervisor_live_roots_unknown_root_arm_is_not_no_bindings() {
7644 let forwarding = Arc::new(ForwardingTable::default());
7645 let handler =
7646 ControlHandler::with_forwarding(Arc::new(Registry::default()), Arc::clone(&forwarding));
7647 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(311));
7648 hello_via_sink(
7649 &handler,
7650 &target_ctx,
7651 &mut target_rx,
7652 hello_frame("target", PROTOCOL_VERSION, 1),
7653 )
7654 .await;
7655 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(312));
7656 let pending = forwarding
7657 .begin_route_bind_relay_for_test(
7658 client_ctx.connection_id,
7659 client_ctx.egress.clone(),
7660 2,
7661 "target",
7662 )
7663 .unwrap();
7664 forwarding
7665 .complete_pending_relay(
7666 target_ctx.connection_id,
7667 pending.corr,
7668 RouteBindRelayOutcome::Accepted,
7669 )
7670 .unwrap();
7671 let actual = query_live_roots(&handler, &target_ctx).await;
7672 let ModuleControlResponseToModule::LiveRoots {
7673 roots,
7674 unknown_root_bindings,
7675 total_bindings,
7676 } = actual
7677 else {
7678 panic!("expected live roots")
7679 };
7680 assert!(roots.is_empty(), "unknown-root arm must not invent a root");
7681 assert_eq!(
7682 unknown_root_bindings, 1,
7683 "unknown-root arm must not read as no bindings"
7684 );
7685 assert_eq!(total_bindings, 1, "unknown-root arm has a live binding");
7686 assert_eq!(
7687 total_bindings,
7688 roots.iter().map(|r| r.bound + r.pending).sum::<u64>() + unknown_root_bindings
7689 );
7690 }
7691
7692 #[tokio::test(start_paused = true)]
7700 async fn hello_ack_reaches_the_module_before_a_route_bind_raced_into_the_reply_gap() {
7701 let forwarding = Arc::new(ForwardingTable::default());
7702 let handler =
7703 ControlHandler::with_forwarding(Arc::new(Registry::default()), Arc::clone(&forwarding));
7704 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(341));
7705 let replies = handler
7706 .handle_control_frame(&module_ctx, hello_frame("raced", PROTOCOL_VERSION, 7))
7707 .await
7708 .unwrap();
7709 let queued_by_hello = module_rx.len();
7710
7711 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(342));
7712 let open_handler = handler.clone();
7713 let open = tokio::spawn(async move {
7714 open_handler
7715 .handle_control_frame(
7716 &client_ctx,
7717 route_open_frame(2, "raced", unique_project_root("hello-ack-race")),
7718 )
7719 .await
7720 .unwrap()
7721 });
7722 let mut spins = 0;
7724 while module_rx.len() == queued_by_hello {
7725 spins += 1;
7726 assert!(spins < 10_000, "route.open never queued a route.bind");
7727 tokio::task::yield_now().await;
7728 }
7729
7730 for reply in replies {
7732 module_ctx.egress.send(reply).await.unwrap();
7733 }
7734
7735 let first = module_rx.recv().await.unwrap().frame;
7736 assert_eq!(
7737 first.header.ty,
7738 FrameType::HelloAck,
7739 "the first frame a registering module reads must be its HELLO_ACK"
7740 );
7741 assert_eq!(first.header.corr, 7);
7742 let second = module_rx.recv().await.unwrap().frame;
7743 assert_eq!(second.header.ty, FrameType::Request);
7744 assert!(
7745 matches!(
7746 serde_json::from_slice::<ModuleControlRequest>(&second.body).unwrap(),
7747 ModuleControlRequest::RouteBind { .. }
7748 ),
7749 "the route.bind follows the ack"
7750 );
7751 assert!(module_rx.try_recv().is_err(), "nothing else was queued");
7752
7753 handler
7754 .handle_control_frame(&module_ctx, route_bind_ack(second.header.corr))
7755 .await
7756 .unwrap();
7757 assert!(open.await.unwrap().is_empty());
7758 let _ = client_rx.recv().await.unwrap();
7759 }
7760
7761 #[tokio::test(start_paused = true)]
7762 async fn supervisor_live_roots_cross_module_scope_uses_requesting_connection() {
7763 let handler = ControlHandler::with_forwarding(
7764 Arc::new(Registry::default()),
7765 Arc::new(ForwardingTable::default()),
7766 );
7767 let (first_ctx, mut first_rx) = route_ctx(ConnectionId::new(315));
7768 let (second_ctx, mut second_rx) = route_ctx(ConnectionId::new(316));
7769 hello_via_sink(
7770 &handler,
7771 &first_ctx,
7772 &mut first_rx,
7773 hello_frame("first", PROTOCOL_VERSION, 1),
7774 )
7775 .await;
7776 hello_via_sink(
7777 &handler,
7778 &second_ctx,
7779 &mut second_rx,
7780 hello_frame("second", PROTOCOL_VERSION, 2),
7781 )
7782 .await;
7783 let root = unique_project_root("second-only");
7784 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(317));
7785 let cloned = handler.clone();
7786 let open = tokio::spawn(async move {
7787 cloned
7788 .handle_control_frame(&client_ctx, route_open_frame(3, "second", root))
7789 .await
7790 .unwrap()
7791 });
7792 let bind = tokio::time::timeout(Duration::from_secs(5), second_rx.recv())
7793 .await
7794 .unwrap()
7795 .unwrap();
7796 let first = query_live_roots(&handler, &first_ctx).await;
7797 let second = query_live_roots(&handler, &second_ctx).await;
7798 assert!(
7799 matches!(
7800 first,
7801 ModuleControlResponseToModule::LiveRoots {
7802 total_bindings: 0,
7803 ..
7804 }
7805 ),
7806 "cross-module scope must not expose another module's roots"
7807 );
7808 assert!(
7809 matches!(
7810 second,
7811 ModuleControlResponseToModule::LiveRoots {
7812 total_bindings: 1,
7813 ..
7814 }
7815 ),
7816 "second module must see its pending route"
7817 );
7818 handler
7819 .handle_control_frame(&second_ctx, route_bind_ack(bind.header.corr))
7820 .await
7821 .unwrap();
7822 assert!(open.await.unwrap().is_empty());
7823 }
7824
7825 #[tokio::test(start_paused = true)]
7826 async fn supervisor_live_roots_no_bindings_arm_is_empty() {
7827 let handler = ControlHandler::with_forwarding(
7828 Arc::new(Registry::default()),
7829 Arc::new(ForwardingTable::default()),
7830 );
7831 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(321));
7832 hello_via_sink(
7833 &handler,
7834 &target_ctx,
7835 &mut target_rx,
7836 hello_frame("target", PROTOCOL_VERSION, 1),
7837 )
7838 .await;
7839 let actual = query_live_roots(&handler, &target_ctx).await;
7840 let ModuleControlResponseToModule::LiveRoots {
7841 roots,
7842 unknown_root_bindings,
7843 total_bindings,
7844 } = actual
7845 else {
7846 panic!("expected live roots")
7847 };
7848 assert!(roots.is_empty());
7849 assert_eq!(unknown_root_bindings, 0);
7850 assert_eq!(total_bindings, 0);
7851 assert_eq!(
7852 total_bindings,
7853 roots.iter().map(|r| r.bound + r.pending).sum::<u64>() + unknown_root_bindings
7854 );
7855 }
7856
7857 fn fed_admission_facts_vectors() -> Vec<(String, Value)> {
7864 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7865 .join("tests/fixtures/fed/admission-facts-emit.jsonl");
7866 let text = std::fs::read_to_string(&path)
7867 .unwrap_or_else(|err| panic!("vendored fed corpus unreadable at {path:?}: {err}"));
7868 let vectors: Vec<(String, Value)> = text
7869 .lines()
7870 .filter(|line| !line.trim().is_empty())
7871 .map(|line| {
7872 let entry: Value = serde_json::from_str(line).expect("corpus line must be JSON");
7873 let id = entry["corpus_id"]
7874 .as_str()
7875 .expect("every vector carries a corpus_id")
7876 .to_string();
7877 (id, entry["package"].clone())
7878 })
7879 .collect();
7880 assert_eq!(
7884 vectors.len(),
7885 3,
7886 "vendored fed corpus changed size; re-sync from subc-federation"
7887 );
7888
7889 const COMMONLY_MODELLED: [&str; 3] = ["schema", "verified_class", "org"];
7904 let richest = vectors
7905 .iter()
7906 .filter_map(|(_, package)| package.as_object())
7907 .map(|object| {
7908 object
7909 .keys()
7910 .filter(|key| !COMMONLY_MODELLED.contains(&key.as_str()))
7911 .count()
7912 })
7913 .max()
7914 .unwrap_or(0);
7915 assert!(
7916 richest >= 2,
7917 "vendored corpus no longer carries a package with unmodelled fields, \
7918 so the relay test can no longer distinguish a verbatim relay from a lossy one"
7919 );
7920
7921 vectors
7922 }
7923
7924 #[tokio::test]
7932 async fn admission_facts_relay_carries_vendored_packages_verbatim() {
7933 for (corpus_id, package) in fed_admission_facts_vectors() {
7934 let registry = Arc::new(Registry::default());
7935 let forwarding = Arc::new(ForwardingTable::default());
7936 let supervisor = SupervisorHandle::new();
7937 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
7938 let handler =
7939 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
7940 .with_supervisor(supervisor)
7941 .with_admission_facts_config(
7942 Some("fed".to_string()),
7943 Some(vec!["target".to_string()]),
7944 );
7945
7946 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(90));
7947 hello_via_sink(
7948 &handler,
7949 &target_ctx,
7950 &mut target_rx,
7951 hello_frame("target", PROTOCOL_VERSION, 1),
7952 )
7953 .await;
7954
7955 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(91));
7956 let route_handler = handler.clone();
7957 let expected = package.clone();
7958 let route_task = tokio::spawn(async move {
7959 route_handler
7960 .handle_control_frame(
7961 &client_ctx,
7962 route_open_frame_with_admission_facts(
7963 20,
7964 "target",
7965 unique_project_root("admission-facts"),
7966 Some(subc_control::ConsumerIdentity {
7967 module_id: "fed".to_string(),
7968 launch_nonce: "fed-nonce".to_string(),
7969 }),
7970 Some(package),
7971 ),
7972 )
7973 .await
7974 .unwrap()
7975 });
7976
7977 let bind_frame = target_rx.recv().await.unwrap();
7978 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
7979 let ModuleControlRequest::RouteBind {
7980 admission_facts, ..
7981 } = bind
7982 else {
7983 panic!("{corpus_id}: expected route.bind")
7984 };
7985 assert_eq!(
7986 admission_facts,
7987 Some(expected),
7988 "{corpus_id}: relay must not add, drop or reshape any field"
7989 );
7990
7991 handler
7992 .handle_control_frame(&target_ctx, route_bind_ack(bind_frame.header.corr))
7993 .await
7994 .unwrap();
7995 route_task.await.unwrap();
7996 }
7997 }
7998
7999 #[tokio::test]
8000 async fn admission_facts_gate_checks_carrier_target_and_precedence() {
8001 let registry = Arc::new(Registry::default());
8002 let forwarding = Arc::new(ForwardingTable::default());
8003 let supervisor = SupervisorHandle::new();
8004 supervisor.set_spawn_nonce("fed", "fed-nonce".to_string());
8005 supervisor.set_spawn_nonce("other", "other-nonce".to_string());
8006 let handler =
8007 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
8008 .with_supervisor(supervisor)
8009 .with_admission_facts_config(
8010 Some("fed".to_string()),
8011 Some(vec!["target".to_string()]),
8012 );
8013
8014 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(70));
8015 hello_via_sink(
8016 &handler,
8017 &target_ctx,
8018 &mut target_rx,
8019 hello_frame("target", PROTOCOL_VERSION, 1),
8020 )
8021 .await;
8022 let (other_ctx, mut other_rx) = route_ctx(ConnectionId::new(71));
8023 hello_via_sink(
8024 &handler,
8025 &other_ctx,
8026 &mut other_rx,
8027 hello_frame("other", PROTOCOL_VERSION, 2),
8028 )
8029 .await;
8030
8031 let facts = json!({"schema": 1, "verified_class": "member", "org": "01H"});
8032 let expected_facts = facts.clone();
8033 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(72));
8034 let route_handler = handler.clone();
8035 let route_task = tokio::spawn(async move {
8036 route_handler
8037 .handle_control_frame(
8038 &client_ctx,
8039 route_open_frame_with_admission_facts(
8040 10,
8041 "target",
8042 unique_project_root("admission-facts"),
8043 Some(subc_control::ConsumerIdentity {
8044 module_id: "fed".to_string(),
8045 launch_nonce: "fed-nonce".to_string(),
8046 }),
8047 Some(facts.clone()),
8048 ),
8049 )
8050 .await
8051 .unwrap()
8052 });
8053 let bind_frame = target_rx.recv().await.unwrap();
8054 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
8055 let ModuleControlRequest::RouteBind {
8056 admission_facts, ..
8057 } = bind
8058 else {
8059 panic!("expected route.bind")
8060 };
8061 assert_eq!(admission_facts, Some(expected_facts));
8062 handler
8063 .handle_control_frame(&target_ctx, route_bind_ack(bind_frame.header.corr))
8064 .await
8065 .unwrap();
8066 assert!(route_task.await.unwrap().is_empty());
8067 assert!(matches!(
8068 serde_json::from_slice::<ClientControlResponse>(&client_rx.recv().await.unwrap().body)
8069 .unwrap(),
8070 ClientControlResponse::RouteOpen { .. }
8071 ));
8072
8073 let direct = handler
8074 .handle_control_frame(
8075 &route_ctx(ConnectionId::new(73)).0,
8076 route_open_frame_with_admission_facts(
8077 11,
8078 "target",
8079 unique_project_root("admission-facts"),
8080 None,
8081 Some(json!({"x": 1})),
8082 ),
8083 )
8084 .await
8085 .unwrap();
8086 assert_eq!(
8087 parse_error(&direct[0])["code"],
8088 "admission_facts_not_permitted"
8089 );
8090
8091 let different_reserved = handler
8092 .handle_control_frame(
8093 &route_ctx(ConnectionId::new(77)).0,
8094 route_open_frame_with_admission_facts(
8095 15,
8096 "target",
8097 unique_project_root("admission-facts"),
8098 Some(subc_control::ConsumerIdentity {
8099 module_id: "other".to_string(),
8100 launch_nonce: "other-nonce".to_string(),
8101 }),
8102 Some(json!({"x": 1})),
8103 ),
8104 )
8105 .await
8106 .unwrap();
8107 assert_eq!(
8108 parse_error(&different_reserved[0])["code"],
8109 "admission_facts_not_permitted"
8110 );
8111
8112 let other_target = handler
8113 .handle_control_frame(
8114 &route_ctx(ConnectionId::new(74)).0,
8115 route_open_frame_with_admission_facts(
8116 12,
8117 "other",
8118 unique_project_root("admission-facts"),
8119 Some(subc_control::ConsumerIdentity {
8120 module_id: "fed".to_string(),
8121 launch_nonce: "fed-nonce".to_string(),
8122 }),
8123 Some(json!({"x": 1})),
8124 ),
8125 )
8126 .await
8127 .unwrap();
8128 assert_eq!(
8129 parse_error(&other_target[0])["code"],
8130 "admission_facts_target_not_allowed"
8131 );
8132
8133 let nonexistent = handler
8134 .handle_control_frame(
8135 &route_ctx(ConnectionId::new(75)).0,
8136 route_open_frame_with_admission_facts(
8137 13,
8138 "missing",
8139 unique_project_root("admission-facts"),
8140 None,
8141 Some(json!({"x": 1})),
8142 ),
8143 )
8144 .await
8145 .unwrap();
8146 assert_eq!(parse_error(&nonexistent[0])["code"], "unknown_module");
8147
8148 let described = handler
8149 .handle_control_frame(
8150 &route_ctx(ConnectionId::new(76)).0,
8151 Frame::build(
8152 FrameType::Request,
8153 control_flags(),
8154 0,
8155 0,
8156 14,
8157 serde_json::to_vec(&ClientControlRequest::ServerDescribe {}).unwrap(),
8158 )
8159 .unwrap(),
8160 )
8161 .await
8162 .unwrap();
8163 let ClientControlResponse::ServerDescribe { capabilities, .. } =
8164 serde_json::from_slice(&described[0].body).unwrap()
8165 else {
8166 panic!("expected server.describe response")
8167 };
8168 assert!(capabilities
8169 .iter()
8170 .any(|cap| cap == "admission_facts_relay_v1"));
8171 }
8172
8173 #[tokio::test]
8174 async fn admission_facts_without_configured_carrier_are_rejected() {
8175 let registry = Arc::new(Registry::default());
8176 let forwarding = Arc::new(ForwardingTable::default());
8177 let handler = ControlHandler::with_forwarding(registry, forwarding);
8178 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(78));
8179 hello_via_sink(
8180 &handler,
8181 &target_ctx,
8182 &mut target_rx,
8183 hello_frame("target", PROTOCOL_VERSION, 1),
8184 )
8185 .await;
8186
8187 let responses = handler
8188 .handle_control_frame(
8189 &route_ctx(ConnectionId::new(79)).0,
8190 route_open_frame_with_admission_facts(
8191 16,
8192 "target",
8193 unique_project_root("admission-facts"),
8194 None,
8195 Some(json!({"x": 1})),
8196 ),
8197 )
8198 .await
8199 .unwrap();
8200 assert_eq!(
8201 parse_error(&responses[0])["code"],
8202 "admission_facts_not_permitted"
8203 );
8204 }
8205
8206 #[tokio::test]
8207 async fn route_open_relays_consumer_capabilities_verbatim() {
8208 let registry = Arc::new(Registry::default());
8209 let forwarding = Arc::new(ForwardingTable::default());
8210 let handler =
8211 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
8212 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(37));
8213 hello_via_sink(
8214 &handler,
8215 &module_ctx,
8216 &mut module_rx,
8217 hello_frame("aft", PROTOCOL_VERSION, 7),
8218 )
8219 .await;
8220
8221 let expected = vec!["elicitation".to_string(), "roots".to_string()];
8222 let expected_for_request = expected.clone();
8223 let project_root = unique_project_root("consumer-capabilities-present");
8224 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(38));
8225 let route_handler = handler.clone();
8226 let route_task = tokio::spawn(async move {
8227 route_handler
8228 .handle_control_frame(
8229 &client_ctx,
8230 route_open_frame_with_consumer_capabilities(
8231 401,
8232 "aft",
8233 project_root,
8234 Some(expected_for_request),
8235 ),
8236 )
8237 .await
8238 .unwrap()
8239 });
8240 let bind_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
8241 .await
8242 .unwrap()
8243 .unwrap();
8244 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
8245 let ModuleControlRequest::RouteBind {
8246 consumer_capabilities,
8247 ..
8248 } = bind
8249 else {
8250 panic!("expected route.bind request, got {bind:?}");
8251 };
8252 assert_eq!(consumer_capabilities, Some(expected.clone()));
8253
8254 handler
8255 .handle_control_frame(&module_ctx, route_bind_ack(bind_frame.header.corr))
8256 .await
8257 .unwrap();
8258 let route_response = route_task.await.unwrap();
8259 assert!(route_response.is_empty());
8260 let published = client_rx.recv().await.unwrap();
8261 assert!(matches!(
8262 serde_json::from_slice::<ClientControlResponse>(&published.body).unwrap(),
8263 ClientControlResponse::RouteOpen { .. }
8264 ));
8265 }
8266
8267 #[tokio::test]
8268 async fn route_open_without_consumer_capabilities_relays_none() {
8269 let registry = Arc::new(Registry::default());
8270 let forwarding = Arc::new(ForwardingTable::default());
8271 let handler =
8272 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
8273 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(39));
8274 hello_via_sink(
8275 &handler,
8276 &module_ctx,
8277 &mut module_rx,
8278 hello_frame("aft", PROTOCOL_VERSION, 7),
8279 )
8280 .await;
8281
8282 let project_root = unique_project_root("consumer-capabilities-absent");
8283 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(40));
8284 let route_handler = handler.clone();
8285 let route_task = tokio::spawn(async move {
8286 route_handler
8287 .handle_control_frame(&client_ctx, route_open_frame(402, "aft", project_root))
8288 .await
8289 .unwrap()
8290 });
8291 let bind_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
8292 .await
8293 .unwrap()
8294 .unwrap();
8295 let bind: ModuleControlRequest = serde_json::from_slice(&bind_frame.body).unwrap();
8296 let ModuleControlRequest::RouteBind {
8297 consumer_capabilities,
8298 ..
8299 } = bind
8300 else {
8301 panic!("expected route.bind request, got {bind:?}");
8302 };
8303 assert_eq!(consumer_capabilities, None);
8304
8305 handler
8306 .handle_control_frame(&module_ctx, route_bind_ack(bind_frame.header.corr))
8307 .await
8308 .unwrap();
8309 let route_response = route_task.await.unwrap();
8310 assert!(route_response.is_empty());
8311 let published = client_rx.recv().await.unwrap();
8312 assert!(matches!(
8313 serde_json::from_slice::<ClientControlResponse>(&published.body).unwrap(),
8314 ClientControlResponse::RouteOpen { .. }
8315 ));
8316 }
8317
8318 #[tokio::test]
8319 async fn supervision_only_module_health_probe_does_not_enable_route_open_and_cleans_up() {
8320 let registry = Arc::new(Registry::default());
8321 let forwarding = Arc::new(ForwardingTable::default());
8322 let handler =
8323 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
8324 .with_health_probe_timeout(Duration::from_secs(5));
8325 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(35));
8326 hello_via_sink(
8327 &handler,
8328 &module_ctx,
8329 &mut module_rx,
8330 non_routable_hello_frame_with_control_ops(
8331 "mcp",
8332 300,
8333 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
8334 ),
8335 )
8336 .await;
8337 assert!(registry
8338 .get_module("mcp")
8339 .unwrap()
8340 .unwrap()
8341 .manifest
8342 .provides
8343 .is_empty());
8344
8345 let (route_client_ctx, _route_client_rx) = route_ctx(ConnectionId::new(36));
8346 let route_response = handler
8347 .handle_control_frame(
8348 &route_client_ctx,
8349 route_open_frame(301, "mcp", unique_project_root("non-routable-mcp")),
8350 )
8351 .await
8352 .unwrap();
8353 assert_eq!(route_response[0].header.ty, FrameType::Error);
8354 assert_eq!(
8355 parse_error(&route_response[0])["code"],
8356 "target_unavailable"
8357 );
8358 assert!(parse_error(&route_response[0])["message"]
8359 .as_str()
8360 .unwrap()
8361 .contains("does not provide the requested target"));
8362 assert!(module_rx.try_recv().is_err());
8363
8364 let (health_client_ctx, _health_client_rx) = route_ctx(ConnectionId::new(37));
8365 let health_handler = handler.clone();
8366 let health_task = tokio::spawn(async move {
8367 health_handler
8368 .handle_control_frame(
8369 &health_client_ctx,
8370 supervisor_health_probe_frame(302, "mcp"),
8371 )
8372 .await
8373 .unwrap()
8374 });
8375 let health_frame = tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
8376 .await
8377 .unwrap()
8378 .unwrap();
8379 assert_eq!(
8380 serde_json::from_slice::<ModuleControlRequest>(&health_frame.body).unwrap(),
8381 ModuleControlRequest::HealthCheck {}
8382 );
8383 handler
8384 .handle_control_frame(
8385 &module_ctx,
8386 health_response(health_frame.header.corr, HealthStatus::Ok),
8387 )
8388 .await
8389 .unwrap();
8390 let health_response = health_task.await.unwrap();
8391 assert_eq!(health_response[0].header.ty, FrameType::Response);
8392 match serde_json::from_slice::<ClientControlResponse>(&health_response[0].body).unwrap() {
8393 ClientControlResponse::SupervisorHealthProbe {
8394 module_id, status, ..
8395 } => {
8396 assert_eq!(module_id, "mcp");
8397 assert_eq!(status, HealthStatus::Ok);
8398 }
8399 other => panic!("unexpected health response: {other:?}"),
8400 }
8401
8402 forwarding
8407 .cleanup_connection(module_ctx.connection_id)
8408 .unwrap();
8409 let (cleanup_probe_ctx, _cleanup_probe_rx) = route_ctx(ConnectionId::new(38));
8410 let cleanup_response = tokio::time::timeout(
8411 Duration::from_millis(200),
8412 handler.handle_control_frame(
8413 &cleanup_probe_ctx,
8414 supervisor_health_probe_frame(303, "mcp"),
8415 ),
8416 )
8417 .await
8418 .expect("probe should fail immediately when the control lane is gone")
8419 .unwrap();
8420 assert_eq!(cleanup_response[0].header.ty, FrameType::Error);
8421 assert_eq!(
8422 parse_error(&cleanup_response[0])["code"],
8423 "target_unavailable"
8424 );
8425 assert!(parse_error(&cleanup_response[0])["message"]
8426 .as_str()
8427 .unwrap()
8428 .contains("no module connection"));
8429
8430 handler
8431 .cleanup_connection(module_ctx.connection_id)
8432 .unwrap();
8433 }
8434
8435 #[tokio::test]
8436 async fn route_open_classifies_unregistered_running_supervised_module_as_warming() {
8437 let registry = Arc::new(Registry::default());
8438 let supervisor_handle = SupervisorHandle::new();
8439 let supervisor =
8440 Supervisor::new(Arc::clone(®istry), RestartPolicy::new(0, Duration::ZERO))
8441 .with_handle(supervisor_handle.clone())
8442 .with_connection_file_path(
8443 std::env::temp_dir()
8444 .join(format!("subc-route-open-warming-{}", std::process::id())),
8445 );
8446 let module = supervisor
8447 .supervise_configured(
8448 ModuleSpec {
8449 module_id: "warming".to_string(),
8450 program: fake_aft_stub_path(),
8451 args: Vec::new(),
8452 env: Vec::new(),
8453 reserved: false,
8454 reserved_prefixes: Vec::new(),
8455 protocol: ModuleProtocol::Subc,
8456 overlap: Default::default(),
8457 },
8458 true,
8459 )
8460 .unwrap();
8461 assert_eq!(module.state().unwrap(), ModuleState::Running);
8462
8463 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
8464 let (ctx, _rx) = route_ctx(ConnectionId::new(39));
8465 let response = handler
8466 .handle_control_frame(
8467 &ctx,
8468 route_open_frame(304, "warming", unique_project_root("warming")),
8469 )
8470 .await
8471 .unwrap();
8472 module.stop().await.unwrap();
8473
8474 assert_eq!(response[0].header.ty, FrameType::Error);
8475 let error = parse_error(&response[0]);
8476 assert_eq!(error["code"], "module_warming");
8477 assert!(error["message"]
8478 .as_str()
8479 .unwrap()
8480 .contains("state=running, enabled=true, live=false"));
8481 }
8482
8483 #[tokio::test]
8488 async fn route_open_refusal_names_the_check_that_refused() {
8489 let handler = ControlHandler::new(Arc::new(Registry::default()));
8490 let capture = EventCapture::default();
8491 let _subscriber =
8492 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
8493 let (ctx, _rx) = route_ctx(ConnectionId::new(95));
8494 let response = handler
8495 .handle_control_frame(
8496 &ctx,
8497 route_open_frame(395, "nobody", unique_project_root("refusal-reason")),
8498 )
8499 .await
8500 .unwrap();
8501
8502 assert_eq!(parse_error(&response[0])["code"], "unknown_module");
8503 let event = capture
8504 .events()
8505 .into_iter()
8506 .find(|event| {
8507 event.target == "control"
8508 && event.fields.get("code") == Some(&"\"unknown_module\"".to_string())
8509 })
8510 .expect("route.open refusal event");
8511 assert_eq!(
8512 event.fields.get("reason"),
8513 Some(&"\"not_registered\"".to_string())
8514 );
8515 }
8516
8517 #[tokio::test]
8518 async fn route_open_supervised_absence_emits_refusal_fields_and_counts_code() {
8519 let registry = Arc::new(Registry::default());
8520 let supervisor_handle = SupervisorHandle::new();
8521 let supervisor =
8522 Supervisor::new(Arc::clone(®istry), RestartPolicy::new(0, Duration::ZERO))
8523 .with_handle(supervisor_handle.clone())
8524 .with_connection_file_path(std::env::temp_dir().join(format!(
8525 "subc-route-open-refusal-info-{}",
8526 std::process::id()
8527 )));
8528 let module = supervisor
8529 .supervise_configured(
8530 ModuleSpec {
8531 module_id: "warming".to_string(),
8532 program: fake_aft_stub_path(),
8533 args: Vec::new(),
8534 env: Vec::new(),
8535 reserved: false,
8536 reserved_prefixes: Vec::new(),
8537 protocol: ModuleProtocol::Subc,
8538 overlap: Default::default(),
8539 },
8540 true,
8541 )
8542 .unwrap();
8543 assert_eq!(module.state().unwrap(), ModuleState::Running);
8544
8545 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
8546 assert!(handler
8547 .counters()
8548 .snapshot()
8549 .get("route_open_refused_by_code")
8550 .is_none());
8551 let capture = EventCapture::default();
8552 let _subscriber =
8553 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
8554 let (ctx, _rx) = route_ctx(ConnectionId::new(94));
8555 let response = handler
8556 .handle_control_frame(
8557 &ctx,
8558 route_open_frame(394, "warming", unique_project_root("refusal-info")),
8559 )
8560 .await
8561 .unwrap();
8562 module.stop().await.unwrap();
8563
8564 assert_eq!(parse_error(&response[0])["code"], "module_warming");
8565 let event = capture
8566 .events()
8567 .into_iter()
8568 .find(|event| {
8569 event.target == "control"
8570 && event.fields.get("code") == Some(&"\"module_warming\"".to_string())
8571 })
8572 .expect("route.open refusal event");
8573 assert_eq!(
8574 event.fields.get("module_id"),
8575 Some(&"\"warming\"".to_string())
8576 );
8577 assert_eq!(event.fields.get("connection_id"), Some(&"94".to_string()));
8578 assert_eq!(
8579 event.fields.get("reason"),
8580 Some(&"\"supervised_not_registered\"".to_string())
8581 );
8582 assert_eq!(event.fields.get("state"), Some(&"running".to_string()));
8583 assert_eq!(event.fields.get("enabled"), Some(&"true".to_string()));
8584 assert_eq!(event.fields.get("live"), Some(&"false".to_string()));
8585 assert_eq!(
8586 handler.counters().snapshot()["route_open_refused_by_code"],
8587 json!({ "module_warming": 1 })
8588 );
8589 }
8590
8591 #[tokio::test(flavor = "current_thread")]
8592 async fn route_open_unknown_module_escapes_target_module_id() {
8593 let handler = ControlHandler::new(Arc::new(Registry::default()));
8594 let capture = EventCapture::default();
8595 let _subscriber =
8596 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
8597 let hostile_module_id = "\u{1b}]52;c;AAAA\u{07}";
8598 let (ctx, _rx) = route_ctx(ConnectionId::new(95));
8599 let response = handler
8600 .handle_control_frame(
8601 &ctx,
8602 route_open_frame(
8603 395,
8604 hostile_module_id,
8605 unique_project_root("hostile-target-module-id"),
8606 ),
8607 )
8608 .await
8609 .unwrap();
8610
8611 assert_eq!(parse_error(&response[0])["code"], "unknown_module");
8612 let event = capture
8613 .events()
8614 .into_iter()
8615 .find(|event| {
8616 event.target == "control"
8617 && event.fields.get("code") == Some(&"\"unknown_module\"".to_string())
8618 })
8619 .expect("route.open unknown-module refusal event");
8620 let logged = event.fields.get("module_id").expect("module_id field");
8621 assert!(!logged.bytes().any(|byte| byte < 0x20));
8622 assert_eq!(logged, r#""\u{1b}]52;c;AAAA\u{7}""#);
8623 }
8624
8625 #[tokio::test(flavor = "current_thread")]
8626 async fn route_open_module_rejection_uses_daemon_counter_key() {
8627 let registry = Arc::new(Registry::default());
8628 let forwarding = Arc::new(ForwardingTable::default());
8629 let handler =
8630 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
8631 let module_connection = ConnectionId::new(95);
8632 let (module_ctx, mut module_rx) = route_ctx(module_connection);
8633 hello_via_sink(
8634 &handler,
8635 &module_ctx,
8636 &mut module_rx,
8637 hello_frame("aft", PROTOCOL_VERSION, 395),
8638 )
8639 .await;
8640
8641 let client_connection = ConnectionId::new(96);
8642 let (client_ctx, _client_rx) = route_ctx(client_connection);
8643 let capture = EventCapture::default();
8644 let _subscriber =
8645 tracing::subscriber::set_default(tracing_subscriber::registry().with(capture.clone()));
8646 let (route_task, bind) = relay_route_open(
8647 &handler,
8648 client_connection,
8649 &client_ctx.egress,
8650 &mut module_rx,
8651 396,
8652 "aft",
8653 "hostile-module-code",
8654 )
8655 .await;
8656 let hostile_code = "\u{1b}]52;c;AAAA\u{07}";
8657 let rejection = Frame::build(
8658 FrameType::Error,
8659 control_flags(),
8660 0,
8661 0,
8662 bind.header.corr,
8663 serde_json::to_vec(&ErrorBody::new(hostile_code, "module refused route.bind")).unwrap(),
8664 )
8665 .unwrap();
8666 handler
8667 .handle_control_frame(&module_ctx, rejection)
8668 .await
8669 .unwrap();
8670
8671 let response = route_task.await.unwrap();
8672 assert_eq!(parse_error(&response[0])["code"], hostile_code);
8673 let counters = handler.counters().snapshot();
8674 assert_eq!(
8675 counters["route_open_refused_by_code"],
8676 json!({ "module_rejected": 1 })
8677 );
8678 assert!(counters["route_open_refused_by_code"]
8679 .get(hostile_code)
8680 .is_none());
8681
8682 let event = capture
8683 .events()
8684 .into_iter()
8685 .find(|event| {
8686 event.target == "control"
8687 && event.fields.get("code") == Some(&"\"module_rejected\"".to_string())
8688 })
8689 .expect("route.open module-rejection refusal event");
8690 let logged = event.fields.get("module_code").expect("module_code field");
8691 assert!(!logged.bytes().any(|byte| byte < 0x20));
8692 assert_eq!(logged, r#""\u{1b}]52;c;AAAA\u{7}""#);
8693 }
8694
8695 #[tokio::test]
8696 async fn route_open_keeps_failed_unregistered_supervised_module_unavailable() {
8697 let registry = Arc::new(Registry::default());
8698 let supervisor_handle = SupervisorHandle::new();
8699 let missing_program = std::env::temp_dir().join(format!(
8700 "subc-route-open-missing-program-{}",
8701 std::process::id()
8702 ));
8703 let supervisor =
8704 Supervisor::new(Arc::clone(®istry), RestartPolicy::new(0, Duration::ZERO))
8705 .with_handle(supervisor_handle.clone());
8706 let module = supervisor
8707 .supervise_configured(
8708 ModuleSpec {
8709 module_id: "failed".to_string(),
8710 program: missing_program,
8711 args: Vec::new(),
8712 env: Vec::new(),
8713 reserved: false,
8714 reserved_prefixes: Vec::new(),
8715 protocol: ModuleProtocol::Subc,
8716 overlap: Default::default(),
8717 },
8718 true,
8719 )
8720 .unwrap();
8721 assert_eq!(module.state().unwrap(), ModuleState::Failed);
8722
8723 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor_handle);
8724 let (ctx, _rx) = route_ctx(ConnectionId::new(40));
8725 let response = handler
8726 .handle_control_frame(
8727 &ctx,
8728 route_open_frame(305, "failed", unique_project_root("failed")),
8729 )
8730 .await
8731 .unwrap();
8732
8733 assert_eq!(response[0].header.ty, FrameType::Error);
8734 let error = parse_error(&response[0]);
8735 assert_eq!(error["code"], "target_unavailable");
8736 assert!(error["message"]
8737 .as_str()
8738 .unwrap()
8739 .contains("state=failed, enabled=true, live=false"));
8740 }
8741
8742 #[tokio::test]
8743 async fn route_open_role_mismatch_remains_target_unavailable() {
8744 let registry = Arc::new(Registry::default());
8745 let handler = ControlHandler::new(Arc::clone(®istry));
8746 handler
8747 .handle_control(
8748 ConnectionId::new(41),
8749 non_routable_hello_frame_with_control_ops("health-only", 306, None),
8750 )
8751 .unwrap();
8752
8753 let (ctx, _rx) = route_ctx(ConnectionId::new(42));
8754 let response = handler
8755 .handle_control_frame(
8756 &ctx,
8757 route_open_frame(307, "health-only", unique_project_root("role-mismatch")),
8758 )
8759 .await
8760 .unwrap();
8761
8762 assert_eq!(parse_error(&response[0])["code"], "target_unavailable");
8763 assert!(parse_error(&response[0])["message"]
8764 .as_str()
8765 .unwrap()
8766 .contains("does not provide the requested target"));
8767 }
8768
8769 #[tokio::test]
8770 async fn route_open_inactive_registration_remains_target_unavailable() {
8771 let registry = Arc::new(Registry::default());
8772 let handler = ControlHandler::new(Arc::clone(®istry));
8773 handler
8774 .handle_control(
8775 ConnectionId::new(43),
8776 hello_frame("inactive", PROTOCOL_VERSION, 308),
8777 )
8778 .unwrap();
8779 assert!(registry
8780 .set_module_state_for_test("inactive", ChannelState::Closed)
8781 .unwrap());
8782
8783 let (ctx, _rx) = route_ctx(ConnectionId::new(44));
8784 let response = handler
8785 .handle_control_frame(
8786 &ctx,
8787 route_open_frame(309, "inactive", unique_project_root("inactive")),
8788 )
8789 .await
8790 .unwrap();
8791
8792 assert_eq!(parse_error(&response[0])["code"], "target_unavailable");
8793 assert!(parse_error(&response[0])["message"]
8794 .as_str()
8795 .unwrap()
8796 .contains("is not active"));
8797 }
8798
8799 #[tokio::test]
8800 async fn late_health_reply_is_recorded_through_the_module_response_path() {
8801 let registry = Arc::new(Registry::default());
8802 let forwarding = Arc::new(ForwardingTable::default());
8803 let supervisor_handle = SupervisorHandle::new();
8804 let supervisor = Supervisor::new(Arc::clone(®istry), crate::RestartPolicy::default())
8805 .with_forwarding(Arc::clone(&forwarding))
8806 .with_handle(supervisor_handle.clone());
8807 let module = supervisor
8808 .supervise_configured(
8809 crate::ModuleSpec {
8810 module_id: "late-health-response".to_string(),
8811 program: PathBuf::from("disabled-module"),
8812 args: Vec::new(),
8813 env: Vec::new(),
8814 reserved: false,
8815 reserved_prefixes: Vec::new(),
8816 protocol: ModuleProtocol::Subc,
8817 overlap: Default::default(),
8818 },
8819 false,
8820 )
8821 .unwrap();
8822 let handler =
8823 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
8824 .with_supervisor(supervisor_handle);
8825 let (module_ctx, _module_rx) = route_ctx(ConnectionId::new(39));
8826 handler
8827 .handle_control_frame(
8828 &module_ctx,
8829 hello_frame_with_control_ops(
8830 "late-health-response",
8831 PROTOCOL_VERSION,
8832 7,
8833 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
8834 ),
8835 )
8836 .await
8837 .unwrap();
8838 let probe_started_at = Instant::now() - Duration::from_millis(80);
8839 let pending = forwarding
8840 .begin_health_probe_rpc_for(
8841 "late-health-response",
8842 MODULE_CONTROL_OP_HEALTH_CHECK,
8843 probe_started_at,
8844 Instant::now() - Duration::from_millis(1),
8845 )
8846 .unwrap();
8847 assert!(forwarding
8848 .tombstone_health_probe_rpc(pending.endpoint, pending.corr)
8849 .unwrap());
8850
8851 let responses = handler
8852 .handle_control_frame(&module_ctx, health_response(pending.corr, HealthStatus::Ok))
8853 .await
8854 .unwrap();
8855
8856 assert!(responses.is_empty());
8857 let health = module.status().unwrap().health;
8858 assert_eq!(health.late_answer_count, 1);
8859 assert!(health.last_late_answer_latency_ms.unwrap() >= 80);
8860 }
8861
8862 #[tokio::test]
8863 async fn health_probe_timeout_and_module_death_are_typed() {
8864 let registry = Arc::new(Registry::default());
8865 let forwarding = Arc::new(ForwardingTable::default());
8866 let handler =
8867 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
8868 .with_health_probe_timeout(Duration::from_millis(50));
8869 let (module_ctx, mut module_rx) = route_ctx(ConnectionId::new(40));
8870 hello_via_sink(
8871 &handler,
8872 &module_ctx,
8873 &mut module_rx,
8874 hello_frame_with_control_ops(
8875 "aft",
8876 PROTOCOL_VERSION,
8877 7,
8878 Some(vec![MODULE_CONTROL_OP_HEALTH_CHECK.to_string()]),
8879 ),
8880 )
8881 .await;
8882
8883 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(41));
8884 let responses = handler
8885 .handle_control_frame(&client_ctx, supervisor_health_probe_frame(201, "aft"))
8886 .await
8887 .unwrap();
8888 assert_eq!(responses[0].header.ty, FrameType::Error);
8889 assert_eq!(parse_error(&responses[0])["code"], "module_timeout");
8890 let _ = module_rx.try_recv();
8891
8892 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(42));
8893 let health_handler = handler.clone();
8894 let death_task = tokio::spawn(async move {
8895 health_handler
8896 .handle_control_frame(&client_ctx, supervisor_health_probe_frame(202, "aft"))
8897 .await
8898 .unwrap()
8899 });
8900 tokio::time::timeout(Duration::from_secs(1), module_rx.recv())
8901 .await
8902 .unwrap()
8903 .unwrap();
8904 handler
8905 .cleanup_connection(module_ctx.connection_id)
8906 .unwrap();
8907 let responses = death_task.await.unwrap();
8908 assert_eq!(responses[0].header.ty, FrameType::Error);
8909 assert_eq!(parse_error(&responses[0])["code"], "target_unavailable");
8910 }
8911
8912 #[test]
8913 fn hello_requires_exact_protocol_version() {
8914 for (connection, offered) in [(1, PROTOCOL_VERSION - 1), (2, PROTOCOL_VERSION + 1)] {
8915 let registry = Arc::new(Registry::default());
8916 let handler = ControlHandler::new(Arc::clone(®istry));
8917 let responses = handler
8918 .handle_control(
8919 ConnectionId::new(connection),
8920 hello_frame("aft", offered, 9),
8921 )
8922 .unwrap();
8923
8924 assert_eq!(responses.len(), 1);
8925 assert_eq!(responses[0].header.ty, FrameType::Error);
8926 let error = parse_error(&responses[0]);
8927 assert_eq!(error["code"], "version_unsupported");
8928 assert!(registry.get_module("aft").unwrap().is_none());
8929 assert_eq!(registry.active_registration_count().unwrap(), 0);
8930 }
8931 }
8932
8933 #[test]
8934 fn unknown_module_push_op_is_ignored_but_malformed_known_op_errors() {
8935 let registry = Arc::new(Registry::default());
8936 let forwarding = Arc::new(ForwardingTable::default());
8937 let handler =
8938 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
8939 let module_connection = ConnectionId::new(301);
8940 let registration = registry
8941 .register_with_control_ops(
8942 manifest("aft-push", PROTOCOL_VERSION),
8943 PROTOCOL_VERSION,
8944 module_connection,
8945 module_baseline_control_ops(),
8946 )
8947 .unwrap();
8948 let (module_tx, _module_rx) = mpsc::channel(8);
8949 let endpoint = forwarding
8950 .register_module_connection(
8951 module_connection,
8952 "aft-push".to_string(),
8953 PROTOCOL_VERSION,
8954 manifest_concurrency(®istration.manifest),
8955 FrameSink::new(module_tx),
8956 )
8957 .unwrap();
8958
8959 let unknown = Frame::build(
8961 FrameType::Push,
8962 control_flags(),
8963 0,
8964 0,
8965 5,
8966 serde_json::to_vec(&json!({"op": "route.future.v2", "extra": 1})).unwrap(),
8967 )
8968 .unwrap();
8969 let out = handler.handle_status_update(endpoint, unknown).unwrap();
8970 assert!(
8971 out.is_empty(),
8972 "unknown push op must be ignored, got {out:?}"
8973 );
8974
8975 let malformed = Frame::build(
8977 FrameType::Push,
8978 control_flags(),
8979 0,
8980 0,
8981 6,
8982 serde_json::to_vec(&json!({"op": "route.status"})).unwrap(),
8983 )
8984 .unwrap();
8985 let out = handler.handle_status_update(endpoint, malformed).unwrap();
8986 assert_eq!(out.len(), 1);
8987 assert_eq!(out[0].header.ty, FrameType::Error);
8988 assert_eq!(parse_error(&out[0])["code"], "invalid_control_body");
8989 }
8990
8991 #[test]
8992 fn hello_rejected_when_connection_already_owns_client_routes() {
8993 let registry = Arc::new(Registry::default());
8994 let forwarding = Arc::new(ForwardingTable::default());
8995 let handler =
8996 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
8997 let _ = bind_liveness_route(®istry, &forwarding, "aft-module");
8999 let client_connection = ConnectionId::new(202);
9000
9001 let responses = handler
9004 .handle_control(
9005 client_connection,
9006 hello_frame("aft-second", PROTOCOL_VERSION, 9),
9007 )
9008 .unwrap();
9009 assert_eq!(responses[0].header.ty, FrameType::Error);
9010 assert_eq!(parse_error(&responses[0])["code"], "invalid_hello");
9011 assert!(registry.get_module("aft-second").unwrap().is_none());
9012 }
9013
9014 #[test]
9015 fn reserved_module_hello_requires_matching_launch_nonce() {
9016 let registry = Arc::new(Registry::default());
9017 let supervisor = SupervisorHandle::new();
9018 supervisor.set_reserved_nonce("vault", "the-real-nonce".to_string());
9021 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor);
9022
9023 let no_nonce = handler
9025 .handle_control(
9026 ConnectionId::new(1),
9027 hello_frame("vault", PROTOCOL_VERSION, 1),
9028 )
9029 .unwrap();
9030 assert_eq!(no_nonce[0].header.ty, FrameType::Error);
9031 assert_eq!(parse_error(&no_nonce[0])["code"], "reserved_module");
9032 assert!(registry.get_module("vault").unwrap().is_none());
9033
9034 let wrong = handler
9036 .handle_control(
9037 ConnectionId::new(2),
9038 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 2, Some("forged")),
9039 )
9040 .unwrap();
9041 assert_eq!(wrong[0].header.ty, FrameType::Error);
9042 assert_eq!(parse_error(&wrong[0])["code"], "reserved_module");
9043 assert!(registry.get_module("vault").unwrap().is_none());
9044
9045 let ok = handler
9047 .handle_control(
9048 ConnectionId::new(3),
9049 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 3, Some("the-real-nonce")),
9050 )
9051 .unwrap();
9052 assert_eq!(ok[0].header.ty, FrameType::HelloAck);
9053 assert!(registry.get_module("vault").unwrap().is_some());
9054 }
9055
9056 #[test]
9057 fn reserved_prefix_hello_uses_delimiter_sensitive_owner_nonce() {
9058 let registry = Arc::new(Registry::default());
9059 let supervisor = SupervisorHandle::new();
9060 supervisor.set_spawn_nonce("federation", "owner-nonce".to_string());
9061 supervisor.set_reserved_prefixes("federation", &["fed:".to_string()]);
9062 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor);
9063
9064 let squat = handler
9065 .handle_control(
9066 ConnectionId::new(1),
9067 hello_frame("fed:peerA:tool", PROTOCOL_VERSION, 1),
9068 )
9069 .unwrap();
9070 assert_eq!(squat[0].header.ty, FrameType::Error);
9071 assert_eq!(parse_error(&squat[0])["code"], "reserved_module");
9072 assert!(parse_error(&squat[0])["message"]
9073 .as_str()
9074 .unwrap()
9075 .contains("fed:"));
9076
9077 let accepted_peer = handler
9078 .handle_control(
9079 ConnectionId::new(2),
9080 hello_frame_with_nonce("fed:peerA:tool", PROTOCOL_VERSION, 2, Some("owner-nonce")),
9081 )
9082 .unwrap();
9083 assert_eq!(accepted_peer[0].header.ty, FrameType::HelloAck);
9084
9085 let accepted_short = handler
9086 .handle_control(
9087 ConnectionId::new(3),
9088 hello_frame_with_nonce("fed:x", PROTOCOL_VERSION, 3, Some("owner-nonce")),
9089 )
9090 .unwrap();
9091 assert_eq!(accepted_short[0].header.ty, FrameType::HelloAck);
9092
9093 for (conn, module_id) in [(4, "fedx:tool"), (5, "fed"), (6, "FED:x")] {
9094 let response = handler
9095 .handle_control(
9096 ConnectionId::new(conn),
9097 hello_frame(module_id, PROTOCOL_VERSION, conn),
9098 )
9099 .unwrap();
9100 assert_eq!(response[0].header.ty, FrameType::HelloAck, "{module_id}");
9101 }
9102 }
9103
9104 #[test]
9105 fn exact_reserved_module_takes_precedence_over_reserved_prefix() {
9106 let registry = Arc::new(Registry::default());
9107 let supervisor = SupervisorHandle::new();
9108 supervisor.set_spawn_nonce("federation", "owner-nonce".to_string());
9109 supervisor.set_reserved_prefixes("federation", &["fed:".to_string()]);
9110 supervisor.set_reserved_nonce("fed:special", "exact-nonce".to_string());
9111 let handler = ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor);
9112
9113 let owner_nonce = handler
9114 .handle_control(
9115 ConnectionId::new(1),
9116 hello_frame_with_nonce("fed:special", PROTOCOL_VERSION, 1, Some("owner-nonce")),
9117 )
9118 .unwrap();
9119 assert_eq!(owner_nonce[0].header.ty, FrameType::Error);
9120 assert_eq!(parse_error(&owner_nonce[0])["code"], "reserved_module");
9121 assert!(registry.get_module("fed:special").unwrap().is_none());
9122
9123 let exact_nonce = handler
9124 .handle_control(
9125 ConnectionId::new(2),
9126 hello_frame_with_nonce("fed:special", PROTOCOL_VERSION, 2, Some("exact-nonce")),
9127 )
9128 .unwrap();
9129 assert_eq!(exact_nonce[0].header.ty, FrameType::HelloAck);
9130 assert!(registry.get_module("fed:special").unwrap().is_some());
9131 }
9132
9133 #[test]
9134 fn non_reserved_module_ignores_launch_nonce() {
9135 let registry = Arc::new(Registry::default());
9136 let handler = ControlHandler::new(Arc::clone(®istry));
9139 let no_nonce = handler
9140 .handle_control(
9141 ConnectionId::new(1),
9142 hello_frame("aft-no-nonce", PROTOCOL_VERSION, 1),
9143 )
9144 .unwrap();
9145 assert_eq!(no_nonce[0].header.ty, FrameType::HelloAck);
9146 assert!(registry.get_module("aft-no-nonce").unwrap().is_some());
9147
9148 let echoed_nonce = handler
9149 .handle_control(
9150 ConnectionId::new(2),
9151 hello_frame_with_nonce("aft-with-nonce", PROTOCOL_VERSION, 2, Some("spawn-nonce")),
9152 )
9153 .unwrap();
9154 assert_eq!(echoed_nonce[0].header.ty, FrameType::HelloAck);
9155 assert!(registry.get_module("aft-with-nonce").unwrap().is_some());
9156 }
9157
9158 #[test]
9159 fn malformed_hello_returns_error_and_handler_still_answers_ping() {
9160 let handler = ControlHandler::default();
9161 let conn = ConnectionId::new(1);
9162 let malformed = Frame::build(
9163 FrameType::Hello,
9164 control_flags(),
9165 0,
9166 0,
9167 3,
9168 b"{not json".to_vec(),
9169 )
9170 .unwrap();
9171
9172 let error = handler.handle_control(conn, malformed).unwrap();
9173 assert_eq!(error[0].header.ty, FrameType::Error);
9174 assert_eq!(parse_error(&error[0])["code"], "invalid_hello");
9175
9176 let ping = Frame::build(FrameType::Ping, control_flags(), 0, 0, 4, Vec::new()).unwrap();
9177 let pong = handler.handle_control(conn, ping).unwrap();
9178 assert_eq!(pong[0].header.ty, FrameType::Pong);
9179 assert_eq!(pong[0].header.corr, 4);
9180 }
9181
9182 #[test]
9183 fn duplicate_module_id_is_rejected_without_replacing_active_registration() {
9184 let registry = Arc::new(Registry::default());
9185 let handler = ControlHandler::new(Arc::clone(®istry));
9186
9187 handler
9188 .handle_control(
9189 ConnectionId::new(1),
9190 hello_frame("aft", PROTOCOL_VERSION, 1),
9191 )
9192 .unwrap();
9193 let duplicate = handler
9194 .handle_control(
9195 ConnectionId::new(2),
9196 hello_frame("aft", PROTOCOL_VERSION, 2),
9197 )
9198 .unwrap();
9199
9200 assert_eq!(duplicate[0].header.ty, FrameType::Error);
9201 assert_eq!(parse_error(&duplicate[0])["code"], "duplicate_module_id");
9202 let registration = registry.get_module("aft").unwrap().unwrap();
9203 assert_eq!(registration.connection_id, ConnectionId::new(1));
9204 }
9205
9206 #[test]
9207 fn liveness_poll_reports_false_when_process_liveness_reports_dead() {
9208 let registry = Arc::new(Registry::default());
9209 let forwarding = Arc::new(ForwardingTable::default());
9210 let process_liveness = Arc::new(FakeProcessLiveness { live: Some(false) });
9211 let handler =
9212 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9213 .with_process_liveness(process_liveness);
9214 let (ctx, route_channel, route_epoch) =
9215 bind_liveness_route(®istry, &forwarding, "aft-dead");
9216 let responses = handler
9217 .handle_route_poll(
9218 &ctx,
9219 route_poll_frame(41, PollKind::Liveness, route_channel),
9220 route_channel,
9221 route_epoch,
9222 PollKind::Liveness,
9223 )
9224 .unwrap();
9225
9226 assert_eq!(responses.len(), 1);
9227 assert_eq!(responses[0].header.ty, FrameType::Response);
9228 assert_route_poll_liveness(&responses[0], false);
9229 }
9230
9231 #[test]
9232 fn liveness_poll_without_process_source_uses_bound_route() {
9233 let registry = Arc::new(Registry::default());
9234 let forwarding = Arc::new(ForwardingTable::default());
9235 let handler =
9236 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
9237 let (ctx, route_channel, route_epoch) =
9238 bind_liveness_route(®istry, &forwarding, "aft-bound-only");
9239 let responses = handler
9240 .handle_route_poll(
9241 &ctx,
9242 route_poll_frame(42, PollKind::Liveness, route_channel),
9243 route_channel,
9244 route_epoch,
9245 PollKind::Liveness,
9246 )
9247 .unwrap();
9248
9249 assert_route_poll_liveness(&responses[0], true);
9250 }
9251
9252 #[test]
9253 fn liveness_poll_untracked_process_source_uses_bound_route() {
9254 let registry = Arc::new(Registry::default());
9255 let forwarding = Arc::new(ForwardingTable::default());
9256 let process_liveness = Arc::new(FakeProcessLiveness { live: None });
9257 let handler =
9258 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9259 .with_process_liveness(process_liveness);
9260 let (ctx, route_channel, route_epoch) =
9261 bind_liveness_route(®istry, &forwarding, "aft-untracked");
9262 let responses = handler
9263 .handle_route_poll(
9264 &ctx,
9265 route_poll_frame(43, PollKind::Liveness, route_channel),
9266 route_channel,
9267 route_epoch,
9268 PollKind::Liveness,
9269 )
9270 .unwrap();
9271
9272 assert_route_poll_liveness(&responses[0], true);
9273 }
9274
9275 #[tokio::test]
9276 async fn unknown_op_returns_unknown_control_op() {
9277 let handler = ControlHandler::default();
9278 let (ctx, _rx) = route_ctx(ConnectionId::new(77));
9279 let request = Frame::build(
9280 FrameType::Request,
9281 control_flags(),
9282 0,
9283 0,
9284 55,
9285 br#"{"op":"route.nope","route_channel":1}"#.to_vec(),
9286 )
9287 .unwrap();
9288
9289 let response = handler.handle_control_frame(&ctx, request).await.unwrap();
9290
9291 assert_eq!(response.len(), 1);
9292 assert_eq!(response[0].header.ty, FrameType::Error);
9293 assert_eq!(response[0].header.corr, 55);
9294 assert_eq!(parse_error(&response[0])["code"], "unknown_control_op");
9295 }
9296
9297 #[tokio::test]
9298 async fn supervisor_provenance_rejects_unknown_exact_module() {
9299 let handler = ControlHandler::default();
9300 let (ctx, _rx) = route_ctx(ConnectionId::new(79));
9301 let request = Frame::build(
9302 FrameType::Request,
9303 control_flags(),
9304 0,
9305 0,
9306 57,
9307 br#"{"op":"supervisor.provenance","module_id":"missing"}"#.to_vec(),
9308 )
9309 .unwrap();
9310
9311 let response = handler.handle_control_frame(&ctx, request).await.unwrap();
9312
9313 assert_eq!(response.len(), 1);
9314 assert_eq!(response[0].header.ty, FrameType::Error);
9315 assert_eq!(response[0].header.corr, 57);
9316 let error = parse_error(&response[0]);
9317 assert_eq!(error["code"], "unknown_module");
9318 assert_eq!(error["message"], "module_id 'missing' is not supervised");
9319 }
9320
9321 #[test]
9322 fn provenance_probe_override_keeps_handler_tests_deterministic() {
9323 let expected = subc_control::RunningImageAgreement::Unavailable {
9324 reason: subc_control::RunningImageUnavailableReason::HashFailed,
9325 };
9326 let handler = ControlHandler::default().with_provenance_probe_result(expected.clone());
9327 assert_eq!(handler.provenance_probe_override, Some(expected));
9328 }
9329
9330 #[tokio::test]
9331 async fn malformed_control_bodies_return_invalid_control_body() {
9332 let handler = ControlHandler::default();
9333 let (ctx, _rx) = route_ctx(ConnectionId::new(78));
9334
9335 for (corr, body) in [
9336 (56, br#"{"route_channel":1}"#.as_slice()),
9337 (57, br#"{"op":17,"route_channel":1}"#.as_slice()),
9338 (
9339 58,
9340 br#"{"op":"route.poll","route_channel":"bad","kind":"status"}"#.as_slice(),
9341 ),
9342 ] {
9343 let request = Frame::build(
9344 FrameType::Request,
9345 control_flags(),
9346 0,
9347 0,
9348 corr,
9349 body.to_vec(),
9350 )
9351 .unwrap();
9352 let response = handler.handle_control_frame(&ctx, request).await.unwrap();
9353
9354 assert_eq!(response.len(), 1);
9355 assert_eq!(response[0].header.ty, FrameType::Error);
9356 assert_eq!(response[0].header.corr, corr);
9357 assert_eq!(parse_error(&response[0])["code"], "invalid_control_body");
9358 }
9359 }
9360
9361 #[tokio::test]
9362 async fn goodbye_tears_down_registration_and_later_channel_is_unknown() {
9363 let registry = Arc::new(Registry::default());
9364 let control = Arc::new(ControlHandler::new(Arc::clone(®istry)));
9365 let router = Router::with_control_handler(Arc::clone(&control));
9366 let connection = router.begin_connection();
9367 let (ctx, mut rx) = route_ctx(connection.id());
9368
9369 router
9370 .route_for_connection(&ctx, hello_frame("aft", PROTOCOL_VERSION, 11))
9371 .await
9372 .unwrap();
9373 let response = rx.recv().await.unwrap();
9374 let ack = parse_ack(&response);
9375 assert_eq!(ack.negotiated_ver, PROTOCOL_VERSION);
9376 let channel = 1;
9377
9378 let goodbye =
9379 Frame::build(FrameType::Goodbye, control_flags(), 0, 0, 12, Vec::new()).unwrap();
9380 router.route_for_connection(&ctx, goodbye).await.unwrap();
9381 assert!(rx.try_recv().is_err());
9382 assert!(registry.get_module("aft").unwrap().is_none());
9383
9384 router
9385 .route_for_connection(&ctx, channel_request(channel, 13))
9386 .await
9387 .unwrap();
9388 let error_frame = rx.recv().await.unwrap();
9389 assert_eq!(error_frame.header.ty, FrameType::Error);
9390 assert_eq!(error_frame.header.channel, channel);
9391 }
9392
9393 #[tokio::test]
9394 async fn dropping_router_connection_releases_registration() {
9395 let registry = Arc::new(Registry::default());
9396 let control = Arc::new(ControlHandler::new(Arc::clone(®istry)));
9397 let router = Router::with_control_handler(control);
9398 let connection = router.begin_connection();
9399 let (ctx, mut rx) = route_ctx(connection.id());
9400
9401 router
9402 .route_for_connection(&ctx, hello_frame("aft", PROTOCOL_VERSION, 31))
9403 .await
9404 .unwrap();
9405 let response = rx.recv().await.unwrap();
9406 let ack = parse_ack(&response);
9407 assert_eq!(ack.negotiated_ver, PROTOCOL_VERSION);
9408 assert!(registry.get_module("aft").unwrap().is_some());
9409
9410 drop(connection);
9411
9412 assert!(registry.get_module("aft").unwrap().is_none());
9413 assert_eq!(registry.active_registration_count().unwrap(), 0);
9414 }
9415
9416 fn capability_manifest(
9417 module_id: &str,
9418 provides: &[&str],
9419 must_never_reach: &[&str],
9420 ) -> ModuleManifest {
9421 let mut manifest = manifest(module_id, PROTOCOL_VERSION);
9422 manifest.capabilities = Some(CapabilityDeclarations {
9423 provides: provides
9424 .iter()
9425 .map(|capability| (*capability).to_string())
9426 .collect(),
9427 requires: Vec::new(),
9428 must_never_reach: must_never_reach
9429 .iter()
9430 .map(|capability| (*capability).to_string())
9431 .collect(),
9432 });
9433 manifest
9434 }
9435
9436 fn hello_frame_with_manifest(manifest: ModuleManifest, corr: u64) -> Frame {
9437 Frame::build(
9438 FrameType::Hello,
9439 control_flags(),
9440 0,
9441 0,
9442 corr,
9443 serde_json::to_vec(&ModuleHelloBody {
9444 protocol_ver: manifest.protocol_ver,
9445 manifest,
9446 control_ops: None,
9447 launch_nonce: None,
9448 })
9449 .expect("capability test HELLO serializes"),
9450 )
9451 .expect("capability test HELLO frame builds")
9452 }
9453
9454 fn catalog_update_with_capabilities_frame(
9455 corr: u64,
9456 capabilities: CapabilityDeclarations,
9457 ) -> Frame {
9458 Frame::build(
9459 FrameType::Request,
9460 control_flags(),
9461 0,
9462 0,
9463 corr,
9464 serde_json::to_vec(&ModuleControlRequestFromModule::CatalogUpdate {
9465 provides: manifest("catalog-update-placeholder", PROTOCOL_VERSION).provides,
9466 capabilities: Some(capabilities),
9467 ready: None,
9468 })
9469 .expect("capability catalog.update serializes"),
9470 )
9471 .expect("capability catalog.update frame builds")
9472 }
9473
9474 async fn register_capability_manifest(
9475 handler: &ControlHandler,
9476 ctx: &RouteCtx,
9477 rx: &mut mpsc::Receiver<crate::router::OutboundFrame>,
9478 manifest: ModuleManifest,
9479 corr: u64,
9480 ) {
9481 hello_via_sink(handler, ctx, rx, hello_frame_with_manifest(manifest, corr)).await;
9482 }
9483
9484 async fn open_route_for_capability_test(
9485 handler: &ControlHandler,
9486 target_ctx: &RouteCtx,
9487 target_rx: &mut mpsc::Receiver<crate::router::OutboundFrame>,
9488 client_connection_id: u64,
9489 corr: u64,
9490 target_module_id: &str,
9491 consumer_identity: Option<ConsumerIdentity>,
9492 ) -> (
9493 mpsc::Receiver<crate::router::OutboundFrame>,
9494 ModuleControlRequest,
9495 ) {
9496 let (client_ctx, mut client_rx) = route_ctx(ConnectionId::new(client_connection_id));
9497 let route_handler = handler.clone();
9498 let target_module_id = target_module_id.to_string();
9499 let route_task = tokio::spawn(async move {
9500 route_handler
9501 .handle_control_frame(
9502 &client_ctx,
9503 route_open_frame_with_admission_facts(
9504 corr,
9505 &target_module_id,
9506 unique_project_root("admission-facts"),
9507 consumer_identity,
9508 None,
9509 ),
9510 )
9511 .await
9512 .expect("capability test route.open succeeds")
9513 });
9514 let bind = tokio::time::timeout(Duration::from_secs(1), target_rx.recv())
9515 .await
9516 .expect("capability test route.open must reach route.bind")
9517 .expect("target control receiver stays open");
9518 let bind_request: ModuleControlRequest =
9519 serde_json::from_slice(&bind.body).expect("route.bind decodes");
9520 handler
9521 .handle_control_frame(target_ctx, route_bind_ack(bind.header.corr))
9522 .await
9523 .expect("capability test route.bind ACK succeeds");
9524 assert!(route_task.await.expect("route.open task joins").is_empty());
9525 let opened = client_rx
9526 .recv()
9527 .await
9528 .expect("successful route.open publishes a response");
9529 assert!(matches!(
9530 serde_json::from_slice::<ClientControlResponse>(&opened.body),
9531 Ok(ClientControlResponse::RouteOpen { .. })
9532 ));
9533 (client_rx, bind_request)
9534 }
9535
9536 fn assert_capability_denied_push(frame: Frame, target_module_id: &str) {
9537 assert_eq!(frame.header.ty, FrameType::Push);
9538 assert_eq!(frame.header.channel, 0);
9539 assert_eq!(
9540 serde_json::from_slice::<ClientControlPush>(&frame.body)
9541 .expect("route.closed control push decodes"),
9542 ClientControlPush::RouteClosed {
9543 module_id: target_module_id.to_string(),
9544 reason: RouteCloseReason::CapabilityDenied,
9545 drained: false,
9546 abandoned: 0,
9547 excluded_subscriptions: 0,
9548 terminal: Some(false),
9549 }
9550 );
9551 }
9552
9553 #[tokio::test]
9554 async fn route_open_capability_forbidden_mutation_proof_creates_no_route() {
9555 let registry = Arc::new(Registry::default());
9556 let forwarding = Arc::new(ForwardingTable::default());
9557 let supervisor = SupervisorHandle::new();
9558 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
9559 let handler =
9560 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9561 .with_supervisor(supervisor);
9562 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(700));
9563 let (opener_ctx, mut opener_rx) = route_ctx(ConnectionId::new(701));
9564 register_capability_manifest(
9565 &handler,
9566 &target_ctx,
9567 &mut target_rx,
9568 capability_manifest("target", &["credentials-provider/v1"], &[]),
9569 1,
9570 )
9571 .await;
9572 register_capability_manifest(
9573 &handler,
9574 &opener_ctx,
9575 &mut opener_rx,
9576 capability_manifest("opener", &[], &["credentials-provider/v1"]),
9577 2,
9578 )
9579 .await;
9580
9581 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(702));
9582 let replies = handler
9583 .handle_control_frame(
9584 &client_ctx,
9585 route_open_frame_with_admission_facts(
9586 3,
9587 "target",
9588 unique_project_root("admission-facts"),
9589 Some(ConsumerIdentity {
9590 module_id: "opener".to_string(),
9591 launch_nonce: "opener-nonce".to_string(),
9592 }),
9593 None,
9594 ),
9595 )
9596 .await
9597 .expect("denied route.open returns a typed frame");
9598 assert_eq!(parse_error(&replies[0])["code"], "capability_forbidden");
9599 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
9600 assert!(
9601 target_rx.try_recv().is_err(),
9602 "forbidden route.open must not relay route.bind"
9603 );
9604 }
9605
9606 #[tokio::test]
9607 async fn capability_deny_edge_hello_mutation_proof_force_closes_existing_route() {
9608 let registry = Arc::new(Registry::default());
9609 let forwarding = Arc::new(ForwardingTable::default());
9610 let supervisor = SupervisorHandle::new();
9611 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
9612 let handler =
9613 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9614 .with_supervisor(supervisor);
9615 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(710));
9616 let (old_opener_ctx, mut old_opener_rx) = route_ctx(ConnectionId::new(711));
9617 register_capability_manifest(
9618 &handler,
9619 &target_ctx,
9620 &mut target_rx,
9621 capability_manifest("target", &["credentials-provider/v1"], &[]),
9622 1,
9623 )
9624 .await;
9625 register_capability_manifest(
9626 &handler,
9627 &old_opener_ctx,
9628 &mut old_opener_rx,
9629 capability_manifest("opener", &[], &[]),
9630 2,
9631 )
9632 .await;
9633 let (mut client_rx, _) = open_route_for_capability_test(
9634 &handler,
9635 &target_ctx,
9636 &mut target_rx,
9637 712,
9638 3,
9639 "target",
9640 Some(ConsumerIdentity {
9641 module_id: "opener".to_string(),
9642 launch_nonce: "opener-nonce".to_string(),
9643 }),
9644 )
9645 .await;
9646 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
9647
9648 handler
9649 .cleanup_connection(old_opener_ctx.connection_id)
9650 .expect("old opener registration cleans up");
9651 let (new_opener_ctx, mut new_opener_rx) = route_ctx(ConnectionId::new(713));
9652 register_capability_manifest(
9653 &handler,
9654 &new_opener_ctx,
9655 &mut new_opener_rx,
9656 capability_manifest("opener", &[], &["credentials-provider/v1"]),
9657 4,
9658 )
9659 .await;
9660
9661 assert_capability_denied_push(
9662 client_rx
9663 .try_recv()
9664 .expect("HELLO deny addition must emit route.closed")
9665 .frame,
9666 "target",
9667 );
9668 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
9669 assert!(matches!(
9670 target_rx.try_recv(),
9671 Ok(outbound) if outbound.header.ty == FrameType::Goodbye
9672 ));
9673 }
9674
9675 #[tokio::test]
9676 async fn capability_claim_catalog_update_mutation_proof_force_closes_existing_route() {
9677 let registry = Arc::new(Registry::default());
9678 let forwarding = Arc::new(ForwardingTable::default());
9679 let supervisor = SupervisorHandle::new();
9680 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
9681 let handler =
9682 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9683 .with_supervisor(supervisor);
9684 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(720));
9685 let (opener_ctx, mut opener_rx) = route_ctx(ConnectionId::new(721));
9686 register_capability_manifest(
9687 &handler,
9688 &target_ctx,
9689 &mut target_rx,
9690 capability_manifest("target", &[], &[]),
9691 1,
9692 )
9693 .await;
9694 register_capability_manifest(
9695 &handler,
9696 &opener_ctx,
9697 &mut opener_rx,
9698 capability_manifest("opener", &[], &["credentials-provider/v1"]),
9699 2,
9700 )
9701 .await;
9702 let (mut client_rx, _) = open_route_for_capability_test(
9703 &handler,
9704 &target_ctx,
9705 &mut target_rx,
9706 722,
9707 3,
9708 "target",
9709 Some(ConsumerIdentity {
9710 module_id: "opener".to_string(),
9711 launch_nonce: "opener-nonce".to_string(),
9712 }),
9713 )
9714 .await;
9715 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
9716
9717 let replies = handler
9718 .handle_control_frame(
9719 &target_ctx,
9720 catalog_update_with_capabilities_frame(
9721 4,
9722 CapabilityDeclarations {
9723 provides: vec!["credentials-provider/v1".to_string()],
9724 requires: Vec::new(),
9725 must_never_reach: Vec::new(),
9726 },
9727 ),
9728 )
9729 .await
9730 .expect("claim catalog.update succeeds");
9731 assert!(matches!(
9732 serde_json::from_slice::<ModuleControlResponseToModule>(&replies[0].body),
9733 Ok(ModuleControlResponseToModule::CatalogUpdate {})
9734 ));
9735 assert_capability_denied_push(
9736 client_rx
9737 .try_recv()
9738 .expect("claim addition must emit route.closed")
9739 .frame,
9740 "target",
9741 );
9742 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
9743 assert!(matches!(
9744 target_rx.try_recv(),
9745 Ok(outbound) if outbound.header.ty == FrameType::Goodbye
9746 ));
9747 }
9748
9749 #[tokio::test]
9750 async fn capability_claim_removal_mutation_proof_keeps_route_open_without_close_frame() {
9751 let registry = Arc::new(Registry::default());
9752 let forwarding = Arc::new(ForwardingTable::default());
9753 let supervisor = SupervisorHandle::new();
9754 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
9755 let handler =
9756 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9757 .with_supervisor(supervisor);
9758 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(730));
9759 let (opener_ctx, mut opener_rx) = route_ctx(ConnectionId::new(731));
9760 register_capability_manifest(
9761 &handler,
9762 &target_ctx,
9763 &mut target_rx,
9764 capability_manifest("target", &["credentials-provider/v1"], &[]),
9765 1,
9766 )
9767 .await;
9768 register_capability_manifest(
9769 &handler,
9770 &opener_ctx,
9771 &mut opener_rx,
9772 capability_manifest("opener", &[], &[]),
9773 2,
9774 )
9775 .await;
9776 let (mut client_rx, _) = open_route_for_capability_test(
9777 &handler,
9778 &target_ctx,
9779 &mut target_rx,
9780 732,
9781 3,
9782 "target",
9783 Some(ConsumerIdentity {
9784 module_id: "opener".to_string(),
9785 launch_nonce: "opener-nonce".to_string(),
9786 }),
9787 )
9788 .await;
9789 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
9790
9791 handler
9792 .handle_control_frame(
9793 &target_ctx,
9794 catalog_update_with_capabilities_frame(
9795 4,
9796 CapabilityDeclarations {
9797 provides: Vec::new(),
9798 requires: Vec::new(),
9799 must_never_reach: Vec::new(),
9800 },
9801 ),
9802 )
9803 .await
9804 .expect("claim removal catalog.update succeeds");
9805 assert_eq!(
9806 forwarding.active_binding_count().unwrap(),
9807 1,
9808 "removing an attested target claim must leave the route census unchanged"
9809 );
9810 assert!(
9811 client_rx.try_recv().is_err(),
9812 "claim removal must not emit route.closed capability_denied"
9813 );
9814 assert!(
9815 target_rx.try_recv().is_err(),
9816 "claim removal must not send the target a route GOODBYE"
9817 );
9818 }
9819
9820 #[tokio::test]
9823 async fn direct_client_scope_honesty_mutation_proof_opens_denied_capability_provider() {
9824 let registry = Arc::new(Registry::default());
9825 let forwarding = Arc::new(ForwardingTable::default());
9826 let supervisor = SupervisorHandle::new();
9827 supervisor.set_spawn_nonce("opener", "opener-nonce".to_string());
9828 let handler =
9829 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9830 .with_supervisor(supervisor);
9831 let (target_ctx, mut target_rx) = route_ctx(ConnectionId::new(740));
9832 let (opener_ctx, mut opener_rx) = route_ctx(ConnectionId::new(741));
9833 register_capability_manifest(
9834 &handler,
9835 &target_ctx,
9836 &mut target_rx,
9837 capability_manifest("target", &["credentials-provider/v1"], &[]),
9838 1,
9839 )
9840 .await;
9841 register_capability_manifest(
9842 &handler,
9843 &opener_ctx,
9844 &mut opener_rx,
9845 capability_manifest("opener", &[], &["credentials-provider/v1"]),
9846 2,
9847 )
9848 .await;
9849
9850 let (_client_rx, bind) = open_route_for_capability_test(
9851 &handler,
9852 &target_ctx,
9853 &mut target_rx,
9854 742,
9855 3,
9856 "target",
9857 None,
9858 )
9859 .await;
9860 let ModuleControlRequest::RouteBind { principal, .. } = bind else {
9861 panic!("direct scope-honesty route must bind");
9862 };
9863 assert_eq!(principal, Some(Principal::Direct));
9864 assert_eq!(forwarding.active_binding_count().unwrap(), 1);
9865 }
9866
9867 #[tokio::test]
9870 async fn must_never_reach_self_route_is_capability_forbidden() {
9871 let registry = Arc::new(Registry::default());
9872 let forwarding = Arc::new(ForwardingTable::default());
9873 let supervisor = SupervisorHandle::new();
9874 supervisor.set_spawn_nonce("self-provider", "self-nonce".to_string());
9875 let handler =
9876 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding))
9877 .with_supervisor(supervisor);
9878 let (self_ctx, mut self_rx) = route_ctx(ConnectionId::new(750));
9879 register_capability_manifest(
9880 &handler,
9881 &self_ctx,
9882 &mut self_rx,
9883 capability_manifest(
9884 "self-provider",
9885 &["credentials-provider/v1"],
9886 &["credentials-provider/v1"],
9887 ),
9888 1,
9889 )
9890 .await;
9891
9892 let (client_ctx, _client_rx) = route_ctx(ConnectionId::new(751));
9893 let replies = handler
9894 .handle_control_frame(
9895 &client_ctx,
9896 route_open_frame_with_admission_facts(
9897 2,
9898 "self-provider",
9899 unique_project_root("admission-facts"),
9900 Some(ConsumerIdentity {
9901 module_id: "self-provider".to_string(),
9902 launch_nonce: "self-nonce".to_string(),
9903 }),
9904 None,
9905 ),
9906 )
9907 .await
9908 .expect("self-route refusal returns a typed frame");
9909 assert_eq!(parse_error(&replies[0])["code"], "capability_forbidden");
9910 assert_eq!(forwarding.active_binding_count().unwrap(), 0);
9911 assert!(
9912 self_rx.try_recv().is_err(),
9913 "self denial must not relay route.bind"
9914 );
9915 }
9916
9917 #[test]
9918 fn unsupported_channel_zero_frame_returns_error() {
9919 let handler = ControlHandler::default();
9920 let request = Frame::build(
9921 FrameType::Request,
9922 control_flags(),
9923 0,
9924 0,
9925 21,
9926 b"opaque".to_vec(),
9927 )
9928 .unwrap();
9929
9930 let response = handler
9931 .handle_control(ConnectionId::new(1), request)
9932 .unwrap();
9933
9934 assert_eq!(response[0].header.ty, FrameType::Error);
9935 assert_eq!(
9936 parse_error(&response[0])["code"],
9937 "unsupported_control_frame"
9938 );
9939 }
9940
9941 mod swap {
9946 use super::*;
9947
9948 const INCUMBENT: ConnectionId = ConnectionId::new(30);
9949 const CANDIDATE: ConnectionId = ConnectionId::new(40);
9950
9951 struct Swap {
9952 registry: Arc<Registry>,
9953 forwarding: Arc<ForwardingTable>,
9954 handler: ControlHandler,
9955 incumbent_ctx: RouteCtx,
9956 incumbent_rx: mpsc::Receiver<crate::router::OutboundFrame>,
9957 candidate_ctx: RouteCtx,
9958 candidate_rx: mpsc::Receiver<crate::router::OutboundFrame>,
9959 }
9960
9961 async fn swap_with_incumbent() -> Swap {
9962 let registry = Arc::new(Registry::default());
9963 let forwarding = Arc::new(ForwardingTable::default());
9964 let handler =
9965 ControlHandler::with_forwarding(Arc::clone(®istry), Arc::clone(&forwarding));
9966 let (incumbent_ctx, mut incumbent_rx) = route_ctx(INCUMBENT);
9967 hello_via_sink(
9968 &handler,
9969 &incumbent_ctx,
9970 &mut incumbent_rx,
9971 hello_frame("aft", PROTOCOL_VERSION, 7),
9972 )
9973 .await;
9974 let (candidate_ctx, candidate_rx) = route_ctx(CANDIDATE);
9975 Swap {
9976 registry,
9977 forwarding,
9978 handler,
9979 incumbent_ctx,
9980 incumbent_rx,
9981 candidate_ctx,
9982 candidate_rx,
9983 }
9984 }
9985
9986 fn register_candidate(swap: &Swap, ready: Option<bool>) {
9987 let mut candidate_manifest = manifest("aft", PROTOCOL_VERSION);
9988 candidate_manifest.ready = ready;
9989 let registration = swap
9990 .registry
9991 .register_candidate_with_control_ops(
9992 candidate_manifest,
9993 PROTOCOL_VERSION,
9994 CANDIDATE,
9995 module_baseline_control_ops(),
9996 )
9997 .unwrap();
9998 swap.forwarding
9999 .register_candidate_module_connection(
10000 CANDIDATE,
10001 "aft".to_string(),
10002 PROTOCOL_VERSION,
10003 manifest_concurrency(®istration.manifest),
10004 swap.candidate_ctx.egress.clone(),
10005 )
10006 .unwrap();
10007 }
10008
10009 fn cutover(swap: &Swap) -> crate::forwarding::ModuleEndpointId {
10010 let cutover = swap.forwarding.cutover_candidate("aft").unwrap().unwrap();
10011 swap.registry.promote_candidate("aft").unwrap().unwrap();
10012 cutover.incumbent.unwrap()
10013 }
10014
10015 fn keyed_total(counters: &Value, key: &str) -> u64 {
10016 counters[key]
10017 .as_object()
10018 .map(|counts| counts.values().filter_map(Value::as_u64).sum())
10019 .unwrap_or(0)
10020 }
10021
10022 #[tokio::test]
10028 async fn incumbent_ack_between_promotion_and_drain_keeps_the_incumbent_serving() {
10029 let mut swap = swap_with_incumbent().await;
10030 let handler = swap.handler.clone();
10031
10032 let cotenant = ConnectionId::new(31);
10034 let (cotenant_ctx, mut cotenant_rx) = route_ctx(cotenant);
10035 let (cotenant_task, cotenant_bind) = relay_route_open(
10036 &handler,
10037 cotenant,
10038 &cotenant_ctx.egress,
10039 &mut swap.incumbent_rx,
10040 100,
10041 "aft",
10042 "swap-cotenant",
10043 )
10044 .await;
10045 handler
10046 .handle_control_frame(
10047 &swap.incumbent_ctx,
10048 route_bind_ack(cotenant_bind.header.corr),
10049 )
10050 .await
10051 .unwrap();
10052 assert!(cotenant_task.await.unwrap().is_empty());
10053 let (cotenant_channel, cotenant_epoch) =
10054 published_route(&cotenant_rx.recv().await.unwrap());
10055
10056 let caller = ConnectionId::new(32);
10058 let (caller_ctx, mut caller_rx) = route_ctx(caller);
10059 let (caller_task, caller_bind) = relay_route_open(
10060 &handler,
10061 caller,
10062 &caller_ctx.egress,
10063 &mut swap.incumbent_rx,
10064 101,
10065 "aft",
10066 "swap-caller",
10067 )
10068 .await;
10069 let (abandoned_channel, abandoned_epoch) = route_bind_channel(&caller_bind);
10070
10071 register_candidate(&swap, None);
10072 cutover(&swap);
10073
10074 let ack = handler
10076 .handle_control_frame(&swap.incumbent_ctx, route_bind_ack(caller_bind.header.corr))
10077 .await;
10078 let module_loop_error = ack.as_ref().err().map(ToString::to_string);
10079 if module_loop_error.is_some() {
10080 handler.cleanup_connection(INCUMBENT).unwrap();
10083 }
10084
10085 assert!(
10087 cotenant_rx.try_recv().is_err(),
10088 "the co-tenant route on the incumbent was torn down by one late ack: \
10089 {module_loop_error:?}"
10090 );
10091 assert!(matches!(
10092 swap.forwarding
10093 .lookup_data_route(cotenant, cotenant_channel, cotenant_epoch)
10094 .unwrap(),
10095 DataRoute::Client(DataRouteState::Bound(_))
10096 ));
10097 assert_eq!(module_loop_error, None);
10098 assert!(swap
10099 .registry
10100 .get_module_by_connection(INCUMBENT)
10101 .unwrap()
10102 .is_some());
10103
10104 let goodbye = tokio::time::timeout(Duration::from_secs(1), swap.incumbent_rx.recv())
10106 .await
10107 .expect("the incumbent is told to drop the abandoned binding")
10108 .unwrap()
10109 .frame;
10110 assert_eq!(goodbye.header.ty, FrameType::Goodbye);
10111 assert_eq!(goodbye.header.channel, abandoned_channel);
10112 assert_eq!(goodbye.header.epoch, abandoned_epoch);
10113 assert!(swap.incumbent_rx.try_recv().is_err());
10114
10115 let response = caller_task.await.unwrap();
10117 assert_eq!(response.len(), 1);
10118 assert_eq!(parse_error(&response[0])["code"], "module_reloading");
10119 assert!(caller_rx.try_recv().is_err());
10120
10121 assert_eq!(swap.forwarding.reserved_route_count().unwrap(), (0, 0));
10125 let counters = handler.counters().snapshot();
10126 assert_eq!(
10127 keyed_total(&counters, "route_open_accepted_by_principal"),
10128 1
10129 );
10130 assert_eq!(keyed_total(&counters, "route_open_refused_by_code"), 1);
10131 assert_eq!(counters["route_open_refused_by_code"]["module_rejected"], 1);
10132 }
10133
10134 #[tokio::test]
10138 async fn route_open_after_cutover_and_incumbent_drain_is_relayed_to_the_candidate() {
10139 let mut swap = swap_with_incumbent().await;
10140 register_candidate(&swap, None);
10141 let incumbent = cutover(&swap);
10142 swap.forwarding
10143 .begin_endpoint_drain(incumbent, RouteCloseReason::Restart)
10144 .unwrap()
10145 .expect("the incumbent is still registered");
10146
10147 let client = ConnectionId::new(33);
10148 let (client_ctx, mut client_rx) = route_ctx(client);
10149 let route_handler = swap.handler.clone();
10150 let open_ctx = RouteCtx {
10151 connection_id: client,
10152 egress: client_ctx.egress.clone(),
10153 };
10154 let mut route_task = tokio::spawn(async move {
10155 route_handler
10156 .handle_control_frame(
10157 &open_ctx,
10158 route_open_frame(90, "aft", unique_project_root("swap-after-drain")),
10159 )
10160 .await
10161 .unwrap()
10162 });
10163 let bind = tokio::select! {
10164 bind = swap.candidate_rx.recv() => bind.expect("candidate egress is open").frame,
10165 response = &mut route_task => {
10166 let response = response.unwrap();
10167 panic!(
10168 "post-cutover route.open was refused instead of relayed to the candidate: {}",
10169 parse_error(&response[0])["code"]
10170 );
10171 }
10172 };
10173 swap.handler
10174 .handle_control_frame(&swap.candidate_ctx, route_bind_ack(bind.header.corr))
10175 .await
10176 .unwrap();
10177 assert!(route_task.await.unwrap().is_empty());
10178 let (channel, epoch) = published_route(&client_rx.recv().await.unwrap());
10179 match swap
10180 .forwarding
10181 .lookup_data_route(client, channel, epoch)
10182 .unwrap()
10183 {
10184 DataRoute::Client(DataRouteState::Bound(route)) => {
10185 assert_eq!(route.module_endpoint.connection_id, CANDIDATE)
10186 }
10187 other => panic!("expected a bound route on the candidate, got {other:?}"),
10188 }
10189 assert!(swap.incumbent_rx.try_recv().is_err());
10190 }
10191
10192 #[tokio::test]
10197 async fn candidate_catalog_update_ready_reaches_the_candidate_registration() {
10198 let swap = swap_with_incumbent().await;
10199 register_candidate(&swap, Some(false));
10200 let update = Frame::build(
10201 FrameType::Request,
10202 control_flags(),
10203 0,
10204 0,
10205 55,
10206 serde_json::to_vec(&ModuleControlRequestFromModule::CatalogUpdate {
10207 provides: manifest("aft", PROTOCOL_VERSION).provides,
10208 capabilities: None,
10209 ready: Some(true),
10210 })
10211 .unwrap(),
10212 )
10213 .unwrap();
10214
10215 let replies = swap
10216 .handler
10217 .handle_control_frame(&swap.candidate_ctx, update)
10218 .await
10219 .unwrap();
10220
10221 assert_eq!(replies.len(), 1);
10222 assert_eq!(
10223 replies[0].header.ty,
10224 FrameType::Response,
10225 "candidate catalog.update was refused: {:?}",
10226 serde_json::from_slice::<Value>(&replies[0].body).ok()
10227 );
10228 assert!(swap.registry.get_candidate("aft").unwrap().unwrap().ready);
10229 assert_eq!(
10230 swap.registry
10231 .get_module("aft")
10232 .unwrap()
10233 .unwrap()
10234 .connection_id,
10235 INCUMBENT
10236 );
10237 }
10238 }
10239
10240 mod swap_admission {
10244 use super::*;
10245
10246 const INCUMBENT_NONCE: &str = "incumbent-nonce";
10247 const CANDIDATE_NONCE: &str = "candidate-nonce";
10248
10249 fn handler_with_incumbent(
10250 module_id: &str,
10251 reserved: bool,
10252 ) -> (Arc<Registry>, SupervisorHandle, ControlHandler) {
10253 let registry = Arc::new(Registry::default());
10254 let supervisor = SupervisorHandle::new();
10255 supervisor.set_spawn_nonce(module_id, INCUMBENT_NONCE.to_string());
10256 if reserved {
10257 supervisor.set_reserved_nonce(module_id, INCUMBENT_NONCE.to_string());
10258 }
10259 let handler =
10260 ControlHandler::new(Arc::clone(®istry)).with_supervisor(supervisor.clone());
10261 let incumbent = handler
10262 .handle_control(
10263 ConnectionId::new(1),
10264 hello_frame_with_nonce(module_id, PROTOCOL_VERSION, 1, Some(INCUMBENT_NONCE)),
10265 )
10266 .unwrap();
10267 assert_eq!(incumbent[0].header.ty, FrameType::HelloAck);
10268 supervisor.open_swap(module_id, CANDIDATE_NONCE.to_string());
10269 (registry, supervisor, handler)
10270 }
10271
10272 #[test]
10278 fn unminted_nonce_on_an_unreserved_id_with_an_open_swap_is_refused() {
10279 let (registry, _supervisor, handler) = handler_with_incumbent("aft", false);
10280
10281 for (connection, nonce) in [(2, Some("forged")), (3, None)] {
10282 let replies = handler
10283 .handle_control(
10284 ConnectionId::new(connection),
10285 hello_frame_with_nonce("aft", PROTOCOL_VERSION, connection, nonce),
10286 )
10287 .unwrap();
10288 assert_eq!(replies[0].header.ty, FrameType::Error);
10289 assert_eq!(
10290 parse_error(&replies[0])["code"],
10291 "swap_token_invalid",
10292 "nonce {nonce:?}"
10293 );
10294 }
10295 assert!(registry.get_candidate("aft").unwrap().is_none());
10296 assert_eq!(
10297 registry.get_module("aft").unwrap().unwrap().connection_id,
10298 ConnectionId::new(1)
10299 );
10300
10301 let admitted = handler
10304 .handle_control(
10305 ConnectionId::new(4),
10306 hello_frame_with_nonce("aft", PROTOCOL_VERSION, 4, Some(CANDIDATE_NONCE)),
10307 )
10308 .unwrap();
10309 assert_eq!(admitted[0].header.ty, FrameType::HelloAck);
10310 assert_eq!(
10311 registry
10312 .get_candidate("aft")
10313 .unwrap()
10314 .unwrap()
10315 .connection_id,
10316 ConnectionId::new(4)
10317 );
10318 assert_eq!(
10319 registry.get_module("aft").unwrap().unwrap().connection_id,
10320 ConnectionId::new(1),
10321 "the candidate must not take the active slot"
10322 );
10323 let replayed = handler
10324 .handle_control(
10325 ConnectionId::new(5),
10326 hello_frame_with_nonce("aft", PROTOCOL_VERSION, 5, Some(CANDIDATE_NONCE)),
10327 )
10328 .unwrap();
10329 assert_eq!(parse_error(&replayed[0])["code"], "swap_token_invalid");
10330
10331 handler.cleanup_connection(ConnectionId::new(1)).unwrap();
10335 let squatter = handler
10336 .handle_control(
10337 ConnectionId::new(6),
10338 hello_frame_with_nonce("aft", PROTOCOL_VERSION, 6, Some("forged")),
10339 )
10340 .unwrap();
10341 assert_eq!(parse_error(&squatter[0])["code"], "swap_token_invalid");
10342 assert!(
10343 registry.get_module("aft").unwrap().is_none(),
10344 "a squatter took the active slot of an id being swapped"
10345 );
10346 }
10347
10348 #[test]
10353 fn reserved_module_candidate_is_admitted_ahead_of_the_reserved_gate() {
10354 let (registry, _supervisor, handler) = handler_with_incumbent("vault", true);
10355
10356 let replies = handler
10357 .handle_control(
10358 ConnectionId::new(2),
10359 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 2, Some(CANDIDATE_NONCE)),
10360 )
10361 .unwrap();
10362
10363 assert_eq!(
10364 replies[0].header.ty,
10365 FrameType::HelloAck,
10366 "reserved candidate refused: {:?}",
10367 serde_json::from_slice::<Value>(&replies[0].body).ok()
10368 );
10369 assert_eq!(
10370 registry
10371 .get_candidate("vault")
10372 .unwrap()
10373 .unwrap()
10374 .connection_id,
10375 ConnectionId::new(2)
10376 );
10377 }
10378
10379 #[test]
10382 fn without_an_open_swap_the_ordinary_gates_decide() {
10383 let (registry, supervisor, handler) = handler_with_incumbent("vault", true);
10384 supervisor.close_swap("vault");
10385
10386 let candidate = handler
10387 .handle_control(
10388 ConnectionId::new(2),
10389 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 2, Some(CANDIDATE_NONCE)),
10390 )
10391 .unwrap();
10392 assert_eq!(parse_error(&candidate[0])["code"], "reserved_module");
10393 let duplicate = handler
10394 .handle_control(
10395 ConnectionId::new(3),
10396 hello_frame_with_nonce("vault", PROTOCOL_VERSION, 3, Some(INCUMBENT_NONCE)),
10397 )
10398 .unwrap();
10399 assert_eq!(parse_error(&duplicate[0])["code"], "duplicate_module_id");
10400 assert!(registry.get_candidate("vault").unwrap().is_none());
10401 }
10402 }
10403}
10404
10405#[cfg(test)]
10406mod concurrency_default_exposure_tests {
10407 use super::*;
10408
10409 fn hello_body(role_json: &str) -> Vec<u8> {
10410 format!(
10411 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":[]}}}}}}}}"#
10412 )
10413 .into_bytes()
10414 }
10415
10416 fn manifest_from(body: &[u8]) -> ModuleManifest {
10417 let value: serde_json::Value = serde_json::from_slice(body).expect("hello parses");
10418 serde_json::from_value(value.get("manifest").expect("manifest key").clone())
10419 .expect("manifest parses")
10420 }
10421
10422 const SURFACE_TAIL: &str = r#""operations":[],"config_schema":{"type":"object"},"observability":[],"identity_scope":[]"#;
10423
10424 #[test]
10425 fn absent_concurrency_on_management_surface_is_reported_as_defaulted() {
10426 let body = hello_body(&format!(
10427 r#"{{"role":"management_surface",{SURFACE_TAIL}}}"#
10428 ));
10429 let manifest = manifest_from(&body);
10430 assert_eq!(manifest_concurrency(&manifest), Concurrency::ModuleManaged);
10433 assert!(manifest_concurrency_was_defaulted(&body, &manifest));
10434 }
10435
10436 #[test]
10437 fn declared_concurrency_is_not_reported_even_when_it_equals_the_default() {
10438 let body = hello_body(&format!(
10439 r#"{{"role":"management_surface",{SURFACE_TAIL},"concurrency":"module_managed"}}"#
10440 ));
10441 let manifest = manifest_from(&body);
10442 assert_eq!(manifest_concurrency(&manifest), Concurrency::ModuleManaged);
10443 assert!(!manifest_concurrency_was_defaulted(&body, &manifest));
10444 }
10445
10446 #[test]
10447 fn non_management_roles_are_never_reported() {
10448 let body = hello_body(
10449 r#"{"role":"internal_service","service_id":"s","transport":"bulk","agent_facing":false,"operations":[]}"#,
10450 );
10451 let manifest = manifest_from(&body);
10452 assert!(!manifest_concurrency_was_defaulted(&body, &manifest));
10453 }
10454}