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