1#![forbid(unsafe_code)]
9
10use std::path::PathBuf;
11
12use serde::{
13 de::{Error as _, MapAccess, SeqAccess, Visitor},
14 ser::SerializeMap,
15 Deserialize, Deserializer, Serialize, Serializer,
16};
17use subc_protocol::{
18 manifest::{CapabilityDeclarations, ManifestProvenance, ProviderRole, SelfSignalDeclaration},
19 session::HealthStatus,
20 BindIdentity, RouteTarget,
21};
22
23pub use subc_protocol::RouteCloseReason;
24
25macro_rules! open_string_enum {
26 (
27 $(#[$meta:meta])*
28 $name:ident {
29 $( $(#[$variant_meta:meta])* $variant:ident => $wire_name:literal ),+ $(,)?
30 }
31 ) => {
32 $(#[$meta])*
33 #[derive(Debug, Clone, PartialEq, Eq)]
34 pub enum $name {
35 $( $(#[$variant_meta])* $variant, )+
36 Unknown(String),
37 }
38
39 impl $name {
40 fn wire_name(&self) -> &str {
41 match self {
42 $( Self::$variant => $wire_name, )+
43 Self::Unknown(value) => value,
44 }
45 }
46 }
47
48 impl Serialize for $name {
49 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
50 where
51 S: serde::Serializer,
52 {
53 serializer.serialize_str(self.wire_name())
54 }
55 }
56
57 impl<'de> Deserialize<'de> for $name {
58 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59 where
60 D: serde::Deserializer<'de>,
61 {
62 let value = String::deserialize(deserializer)?;
63 Ok(match value.as_str() {
64 $( $wire_name => Self::$variant, )+
65 _ => Self::Unknown(value),
66 })
67 }
68 }
69 };
70}
71
72#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
74pub struct ConsumerIdentity {
75 pub module_id: String,
76 pub launch_nonce: String,
77}
78
79impl std::fmt::Debug for ConsumerIdentity {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.debug_struct("ConsumerIdentity")
86 .field("module_id", &self.module_id)
87 .field(
88 "launch_nonce",
89 &format_args!("<{} bytes redacted>", self.launch_nonce.len()),
90 )
91 .finish()
92 }
93}
94
95pub mod ops {
106 pub const SERVER: &str = "server.";
107 pub const CATALOG: &str = "catalog.";
108 pub const ROUTE: &str = "route.";
109 pub const SUPERVISOR: &str = "supervisor.";
110 pub const CONFIG: &str = "config.";
111
112 pub const SERVER_DESCRIBE: &str = "server.describe";
113 pub const CATALOG_LIST: &str = "catalog.list";
114 pub const ROUTE_OPEN: &str = "route.open";
115 pub const ROUTE_POLL: &str = "route.poll";
116 pub const ROUTE_CLOSING: &str = "route.closing";
117 pub const ROUTE_CLOSED: &str = "route.closed";
118 pub const SUPERVISOR_LIST: &str = "supervisor.list";
119 pub const SUPERVISOR_RESTART: &str = "supervisor.restart";
120 pub const SUPERVISOR_SWAP: &str = "supervisor.swap";
121 pub const SUPERVISOR_RELOAD: &str = "supervisor.reload";
122 pub const SUPERVISOR_RESCAN: &str = "supervisor.rescan";
123 pub const SUPERVISOR_RELEASE_RESERVED: &str = "supervisor.release_reserved";
124 pub const SUPERVISOR_SET_ENABLED: &str = "supervisor.set_enabled";
125 pub const SUPERVISOR_HEALTH_PROBE: &str = "supervisor.health_probe";
126 pub const SUPERVISOR_HEALTH: &str = "supervisor.health";
127 pub const SUPERVISOR_STDERR_TAIL: &str = "supervisor.stderr_tail";
128 pub const SUPERVISOR_TERMINALS: &str = "supervisor.terminals";
129 pub const SUPERVISOR_ROUTES: &str = "supervisor.routes";
130 pub const SUPERVISOR_PROVENANCE: &str = "supervisor.provenance";
131 pub const SUPERVISOR_SPAWN_SNAPSHOT: &str = "supervisor.spawn_snapshot";
132 pub const SUPERVISOR_SPAWN_SUBSCRIBE: &str = "supervisor.spawn_subscribe";
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137#[serde(tag = "op")]
138#[allow(clippy::large_enum_variant)]
141pub enum ClientControlRequest {
142 #[serde(rename = "server.describe")]
143 ServerDescribe {},
144 #[serde(rename = "catalog.list")]
145 CatalogList {
146 #[serde(default)]
151 module_id: Option<String>,
152 },
153 #[serde(rename = "route.open")]
154 RouteOpen {
155 target: RouteTarget,
156 identity: BindIdentity,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
166 consumer_identity: Option<ConsumerIdentity>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
175 consumer_capabilities: Option<Vec<String>>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 admission_facts: Option<serde_json::Value>,
179 },
180 #[serde(rename = "route.poll")]
181 RoutePoll {
182 route_channel: u16,
183 route_epoch: u32,
184 kind: PollKind,
185 },
186 #[serde(rename = "supervisor.list")]
187 SupervisorList {},
188 #[serde(rename = "supervisor.spawn_snapshot")]
190 SupervisorSpawnSnapshot {},
191 #[serde(rename = "supervisor.spawn_subscribe")]
207 SupervisorSpawnSubscribe {
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 since: Option<SpawnCursor>,
210 },
211 #[serde(rename = "supervisor.restart")]
212 SupervisorRestart {
213 module_id: String,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
222 drain_timeout_ms: Option<u64>,
223 },
224 #[serde(rename = "supervisor.swap")]
239 SupervisorSwap {
240 module_id: String,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
244 ready_timeout_ms: Option<u64>,
245 },
246 #[serde(rename = "supervisor.reload")]
247 SupervisorReload { module_id: String },
248 #[serde(rename = "supervisor.rescan")]
249 SupervisorRescan {
250 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
270 preview: bool,
271 },
272 #[serde(rename = "supervisor.release_reserved")]
276 SupervisorReleaseReserved { module_id: String },
277 #[serde(rename = "supervisor.set_enabled")]
278 SupervisorSetEnabled { module_id: String, enabled: bool },
279 #[serde(rename = "supervisor.health_probe")]
280 SupervisorHealthProbe { module_id: String },
281 #[serde(rename = "supervisor.health")]
282 SupervisorHealth {},
283 #[serde(rename = "supervisor.routes")]
296 SupervisorRoutes {
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 module_id: Option<String>,
299 },
300 #[serde(rename = "supervisor.provenance")]
303 SupervisorProvenance {
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 module_id: Option<String>,
306 },
307 #[serde(rename = "supervisor.stderr_tail")]
315 SupervisorStderrTail {
316 module_id: String,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 max_lines: Option<u32>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 max_bytes: Option<u32>,
321 },
322 #[serde(rename = "supervisor.terminals")]
335 SupervisorTerminals { module_id: String },
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
340#[serde(tag = "op")]
341pub enum ClientControlResponse {
342 #[serde(rename = "server.describe")]
343 ServerDescribe {
344 protocol_ver: u8,
345 subc_ops: Vec<String>,
346 capabilities: Vec<String>,
347 connected_clients: u64,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
349 counters: Option<serde_json::Value>,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
357 build_git_sha: Option<String>,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
364 build_lock_digest: Option<String>,
365 #[serde(default, skip_serializing_if = "Vec::is_empty")]
369 capability_requirements: Vec<CapabilityRequirementStatus>,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
375 machine_id: Option<String>,
376 },
377 #[serde(rename = "catalog.list")]
378 CatalogList {
379 generation: u64,
380 modules: Vec<CatalogEntry>,
381 subc_ops: Vec<String>,
382 },
383 #[serde(rename = "route.open")]
384 RouteOpen {
385 route_channel: u16,
386 route_epoch: u32,
387 },
388 #[serde(rename = "route.poll")]
389 RoutePoll {
390 route_channel: u16,
391 route_epoch: u32,
392 status: Option<String>,
393 live: Option<bool>,
394 },
395 #[serde(rename = "supervisor.list")]
396 SupervisorList {
397 generation: u64,
398 modules: Vec<SupervisorEntry>,
399 },
400 #[serde(rename = "supervisor.spawn_snapshot")]
401 SupervisorSpawnSnapshot {
402 #[serde(flatten)]
403 snapshot: SpawnSnapshot,
404 },
405 #[serde(rename = "supervisor.ack")]
406 SupervisorAck { module_id: String, applied: bool },
407 #[serde(rename = "supervisor.rescan")]
408 SupervisorRescan {
409 #[serde(flatten)]
410 result: SupervisorRescanResult,
411 },
412 #[serde(rename = "supervisor.health_probe")]
413 SupervisorHealthProbe {
414 module_id: String,
415 status: HealthStatus,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
417 detail: Option<String>,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
419 metrics: Option<serde_json::Value>,
420 },
421 #[serde(rename = "supervisor.health")]
422 SupervisorHealth {
423 generation: u64,
424 modules: Vec<SupervisorHealthEntry>,
425 },
426 #[serde(rename = "supervisor.routes")]
427 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
428 #[serde(rename = "supervisor.provenance")]
429 SupervisorProvenance {
430 daemon: SupervisorDaemonProvenance,
431 modules: Vec<SupervisorModuleProvenance>,
432 },
433 #[serde(rename = "supervisor.stderr_tail")]
434 SupervisorStderrTail {
435 module_id: String,
436 #[serde(flatten)]
437 tail: StderrTail,
438 },
439 #[serde(rename = "supervisor.terminals")]
440 SupervisorTerminals {
441 module_id: String,
442 #[serde(flatten)]
443 terminals: TerminalHistory,
444 },
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
452#[serde(tag = "op")]
453pub enum ClientControlPush {
454 #[serde(rename = "route.closing")]
455 RouteClosing {
456 module_id: String,
457 reason: RouteCloseReason,
458 },
459 #[serde(rename = "route.closed")]
460 RouteClosed {
461 module_id: String,
462 reason: RouteCloseReason,
463 drained: bool,
465 abandoned: u32,
468 #[serde(default)]
470 excluded_subscriptions: u32,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
477 terminal: Option<bool>,
478 },
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
483pub struct SpawnCursor {
484 pub daemon_incarnation: String,
485 pub seq: u64,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
490pub struct LiveSpawn {
491 pub module_id: String,
492 pub spawn_generation: u64,
493 pub pid: u32,
494 pub spawned_at_ms: u64,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
499pub struct SpawnSnapshot {
500 pub cursor: SpawnCursor,
501 pub ring_bound: u64,
503 pub live: Vec<LiveSpawn>,
504}
505
506#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
508#[serde(rename_all = "snake_case")]
509pub enum SpawnEventKind {
510 Spawned,
511 Exited,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
520pub struct SpawnEvent {
521 pub cursor: SpawnCursor,
522 pub kind: SpawnEventKind,
523 pub module_id: String,
524 pub spawn_generation: u64,
525 pub pid: u32,
526 #[serde(default, skip_serializing_if = "Option::is_none")]
527 pub exit_code: Option<i32>,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
529 pub exit_signal: Option<i32>,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
534pub struct StderrTail {
535 pub capture: StderrCaptureState,
536 pub entries: Vec<StderrTailEntry>,
537 #[serde(default, skip_serializing_if = "is_zero_u64")]
546 pub dropped_lines: u64,
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
551pub struct SupervisorRouteModule {
552 pub module_id: String,
553 pub routes: Vec<SupervisorRoute>,
554}
555
556#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
558pub struct SupervisorRoute {
559 pub consumer: SupervisorRouteConsumer,
560 pub age_ms: u64,
562 pub draining: bool,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
571 pub drain_reason: Option<RouteCloseReason>,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
576pub struct SupervisorModuleProvenance {
577 pub module_id: String,
578 pub module_declared: ModuleDeclaredProvenance,
579 pub daemon_observed: SupervisorObservedProcess,
580}
581
582#[derive(Debug, Clone, PartialEq)]
584pub enum ModuleDeclaredProvenance {
585 Reported {
586 build: ManifestProvenance,
587 },
588 Unverifiable,
589 Unknown {
592 tag: String,
593 body: OrderedJsonObject,
594 },
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
602pub struct SupervisorObservedProcess {
603 #[serde(default, skip_serializing_if = "Option::is_none")]
604 pub pid: Option<u32>,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
606 pub spawned_at_ms: Option<u64>,
607 #[serde(default, skip_serializing_if = "Option::is_none")]
608 pub spawned_from: Option<PathBuf>,
609 pub running_image: RunningImageAgreement,
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
615pub struct PendingReloadVerdict {
616 pub path: ReloadPathAgreement,
617 pub image: RunningImageAgreement,
618}
619
620#[derive(Debug, Clone, PartialEq)]
622pub enum ReloadPathAgreement {
623 Match,
624 Mismatch {
625 configured: PathBuf,
626 spawned_from: PathBuf,
627 },
628 Unavailable {
629 reason: ReloadPathUnavailableReason,
630 },
631 Unknown {
632 tag: String,
633 body: OrderedJsonObject,
634 },
635}
636
637open_string_enum! {
638 ReloadPathUnavailableReason {
640 NotRunning => "not_running",
641 SpawnedPathUnavailable => "spawned_path_unavailable",
642 }
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
647pub struct SupervisorDaemonProvenance {
648 pub daemon_build: DaemonBuildProvenance,
649 pub daemon_observed: DaemonObservedProcess,
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
654pub struct DaemonBuildProvenance {
655 #[serde(default, skip_serializing_if = "Option::is_none")]
656 pub build_git_sha: Option<String>,
657 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub build_lock_digest: Option<String>,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
663pub struct DaemonObservedProcess {
664 #[serde(default, skip_serializing_if = "Option::is_none")]
665 pub pid: Option<u32>,
666 #[serde(default, skip_serializing_if = "Option::is_none")]
671 pub started_at_ms: Option<u64>,
672 pub running_image: RunningImageAgreement,
673}
674
675#[derive(Debug, Clone, PartialEq)]
677pub enum RunningImageAgreement {
678 Match {
679 evidence: RunningImageEvidence,
680 },
681 Mismatch {
682 running: RunningImageEvidence,
683 disk: RunningImageEvidence,
684 },
685 Unavailable {
686 reason: RunningImageUnavailableReason,
687 },
688 Unknown {
691 tag: String,
692 body: OrderedJsonObject,
693 },
694}
695
696#[derive(Debug, Clone, PartialEq)]
698pub enum RunningImageEvidence {
699 LinuxProcSha256 {
700 digest: String,
701 },
702 MacosSpawnInode {
703 device: u64,
704 inode: u64,
705 },
706 Unknown {
709 tag: String,
710 body: OrderedJsonObject,
711 },
712}
713
714open_string_enum! {
715 RunningImageUnavailableReason {
717 NotRunning => "not_running",
718 UnsupportedPlatform => "unsupported_platform",
719 RunningExecutableUnreadable => "running_executable_unreadable",
720 SpawnedPathUnreadable => "spawned_path_unreadable",
721 HashFailed => "hash_failed",
722 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
723 }
724}
725
726#[derive(Debug, Clone, PartialEq)]
732pub enum SupervisorRouteConsumer {
733 Reserved {
734 module_id: String,
735 },
736 Direct {
737 connection_id: u64,
738 },
739 Unknown {
742 tag: String,
743 body: OrderedJsonObject,
744 },
745}
746
747#[derive(Debug, Clone, PartialEq)]
754pub enum StderrCaptureState {
755 Captured,
758 Incomplete { reason: String },
760 NotCaptured { reason: String },
762 Unknown {
765 tag: String,
766 body: OrderedJsonObject,
767 },
768}
769
770#[derive(Debug, Clone, PartialEq)]
771pub enum StderrTailEntry {
772 Line {
773 text: String,
774 truncated: bool,
779 },
780 ProcessStart,
785 Unknown {
788 tag: String,
789 body: OrderedJsonObject,
790 },
791}
792
793#[derive(Debug, Serialize, Deserialize)]
794#[serde(tag = "status", rename_all = "snake_case")]
795enum ModuleDeclaredProvenanceWire {
796 Reported { build: ManifestProvenance },
797 Unverifiable,
798}
799
800#[derive(Debug, Serialize, Deserialize)]
801#[serde(tag = "status", rename_all = "snake_case")]
802enum RunningImageAgreementWire {
803 Match {
804 evidence: RunningImageEvidence,
805 },
806 Mismatch {
807 running: RunningImageEvidence,
808 disk: RunningImageEvidence,
809 },
810 Unavailable {
811 reason: RunningImageUnavailableReason,
812 },
813}
814
815#[derive(Debug, Serialize, Deserialize)]
816#[serde(tag = "status", rename_all = "snake_case")]
817enum ReloadPathAgreementWire {
818 Match,
819 Mismatch {
820 configured: PathBuf,
821 spawned_from: PathBuf,
822 },
823 Unavailable {
824 reason: ReloadPathUnavailableReason,
825 },
826}
827
828#[derive(Debug, Serialize, Deserialize)]
829#[serde(tag = "method", rename_all = "snake_case")]
830enum RunningImageEvidenceWire {
831 LinuxProcSha256 { digest: String },
832 MacosSpawnInode { device: u64, inode: u64 },
833}
834
835#[derive(Debug, Serialize, Deserialize)]
836#[serde(tag = "kind", rename_all = "snake_case")]
837enum SupervisorRouteConsumerWire {
838 Reserved { module_id: String },
839 Direct { connection_id: u64 },
840}
841
842#[derive(Debug, Serialize, Deserialize)]
843#[serde(tag = "state", rename_all = "snake_case")]
844enum StderrCaptureStateWire {
845 Captured,
846 Incomplete { reason: String },
847 NotCaptured { reason: String },
848}
849
850#[derive(Debug, Serialize, Deserialize)]
851#[serde(tag = "kind", rename_all = "snake_case")]
852enum StderrTailEntryWire {
853 Line {
854 text: String,
855 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
856 truncated: bool,
857 },
858 ProcessStart,
859}
860
861#[derive(Debug, Clone, PartialEq)]
863pub enum OrderedJsonValue {
864 Null,
865 Bool(bool),
866 Number(serde_json::Number),
867 String(String),
868 Array(Vec<Self>),
869 Object(OrderedJsonObject),
870}
871
872#[derive(Debug, Clone, PartialEq)]
874pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
875
876impl OrderedJsonObject {
877 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
879 &self.0
880 }
881
882 fn into_value(self) -> serde_json::Value {
883 serde_json::Value::Object(
884 self.0
885 .into_iter()
886 .map(|(key, value)| (key, value.into_value()))
887 .collect(),
888 )
889 }
890}
891
892impl OrderedJsonValue {
893 fn into_value(self) -> serde_json::Value {
894 match self {
895 Self::Null => serde_json::Value::Null,
896 Self::Bool(value) => serde_json::Value::Bool(value),
897 Self::Number(value) => serde_json::Value::Number(value),
898 Self::String(value) => serde_json::Value::String(value),
899 Self::Array(values) => {
900 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
901 }
902 Self::Object(value) => value.into_value(),
903 }
904 }
905}
906
907impl Serialize for OrderedJsonValue {
908 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
909 where
910 S: Serializer,
911 {
912 match self {
913 Self::Null => serializer.serialize_unit(),
914 Self::Bool(value) => serializer.serialize_bool(*value),
915 Self::Number(value) => value.serialize(serializer),
916 Self::String(value) => serializer.serialize_str(value),
917 Self::Array(values) => values.serialize(serializer),
918 Self::Object(value) => value.serialize(serializer),
919 }
920 }
921}
922
923impl<'de> Deserialize<'de> for OrderedJsonValue {
924 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
925 where
926 D: Deserializer<'de>,
927 {
928 struct OrderedValueVisitor;
929
930 impl<'de> Visitor<'de> for OrderedValueVisitor {
931 type Value = OrderedJsonValue;
932
933 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
934 formatter.write_str("a JSON value with ordered object members")
935 }
936
937 fn visit_unit<E>(self) -> Result<Self::Value, E>
938 where
939 E: serde::de::Error,
940 {
941 Ok(OrderedJsonValue::Null)
942 }
943
944 fn visit_none<E>(self) -> Result<Self::Value, E>
945 where
946 E: serde::de::Error,
947 {
948 Ok(OrderedJsonValue::Null)
949 }
950
951 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
952 where
953 D: Deserializer<'de>,
954 {
955 OrderedJsonValue::deserialize(deserializer)
956 }
957
958 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
959 where
960 E: serde::de::Error,
961 {
962 Ok(OrderedJsonValue::Bool(value))
963 }
964
965 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
966 where
967 E: serde::de::Error,
968 {
969 Ok(OrderedJsonValue::Number(value.into()))
970 }
971
972 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
973 where
974 E: serde::de::Error,
975 {
976 Ok(OrderedJsonValue::Number(value.into()))
977 }
978
979 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
980 where
981 E: serde::de::Error,
982 {
983 serde_json::Number::from_f64(value)
984 .map(OrderedJsonValue::Number)
985 .ok_or_else(|| E::custom("non-finite JSON number"))
986 }
987
988 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
989 where
990 E: serde::de::Error,
991 {
992 Ok(OrderedJsonValue::String(value.to_owned()))
993 }
994
995 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
996 where
997 E: serde::de::Error,
998 {
999 Ok(OrderedJsonValue::String(value))
1000 }
1001
1002 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1003 where
1004 A: SeqAccess<'de>,
1005 {
1006 let mut values = Vec::new();
1007 while let Some(value) = sequence.next_element()? {
1008 values.push(value);
1009 }
1010 Ok(OrderedJsonValue::Array(values))
1011 }
1012
1013 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1014 where
1015 A: MapAccess<'de>,
1016 {
1017 let mut entries = Vec::new();
1018 while let Some((key, value)) = map.next_entry()? {
1019 entries.push((key, value));
1020 }
1021 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
1022 }
1023 }
1024
1025 deserializer.deserialize_any(OrderedValueVisitor)
1026 }
1027}
1028
1029impl Serialize for OrderedJsonObject {
1030 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1031 where
1032 S: Serializer,
1033 {
1034 let mut map = serializer.serialize_map(Some(self.0.len()))?;
1035 for (key, value) in &self.0 {
1036 map.serialize_entry(key, value)?;
1037 }
1038 map.end()
1039 }
1040}
1041
1042impl<'de> Deserialize<'de> for OrderedJsonObject {
1043 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1044 where
1045 D: Deserializer<'de>,
1046 {
1047 struct OrderedObjectVisitor;
1048
1049 impl<'de> Visitor<'de> for OrderedObjectVisitor {
1050 type Value = OrderedJsonObject;
1051
1052 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1053 formatter.write_str("an object with ordered JSON members")
1054 }
1055
1056 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1057 where
1058 A: MapAccess<'de>,
1059 {
1060 let mut entries = Vec::new();
1061 while let Some((key, value)) = map.next_entry()? {
1062 entries.push((key, value));
1063 }
1064 Ok(OrderedJsonObject(entries))
1065 }
1066 }
1067
1068 deserializer.deserialize_map(OrderedObjectVisitor)
1069 }
1070}
1071
1072fn read_tagged<'de, D>(
1073 deserializer: D,
1074 field: &'static str,
1075) -> Result<(String, OrderedJsonObject), D::Error>
1076where
1077 D: Deserializer<'de>,
1078{
1079 let body = OrderedJsonObject::deserialize(deserializer)?;
1080 let mut tag = None;
1081 for (key, value) in body.as_entries() {
1082 if key != field {
1083 continue;
1084 }
1085 if tag.is_some() {
1086 return Err(D::Error::custom(format!(
1087 "tagged object has duplicate `{field}` field"
1088 )));
1089 }
1090 let OrderedJsonValue::String(value) = value else {
1091 return Err(D::Error::custom(format!(
1092 "tagged object has no string `{field}` field"
1093 )));
1094 };
1095 tag = Some(value);
1096 }
1097 let Some(tag) = tag else {
1098 return Err(D::Error::custom(format!(
1099 "tagged object has no string `{field}` field"
1100 )));
1101 };
1102 Ok((tag.to_string(), body))
1103}
1104
1105fn read_ordered_tagged(
1106 value: OrderedJsonValue,
1107 field: &'static str,
1108) -> Result<(String, OrderedJsonObject), String> {
1109 let OrderedJsonValue::Object(body) = value else {
1110 return Err(format!("expected tagged object with `{field}` field"));
1111 };
1112 let mut tag = None;
1113 for (key, value) in body.as_entries() {
1114 if key != field {
1115 continue;
1116 }
1117 if tag.is_some() {
1118 return Err(format!("tagged object has duplicate `{field}` field"));
1119 }
1120 let OrderedJsonValue::String(value) = value else {
1121 return Err(format!("tagged object has no string `{field}` field"));
1122 };
1123 tag = Some(value);
1124 }
1125 let Some(tag) = tag else {
1126 return Err(format!("tagged object has no string `{field}` field"));
1127 };
1128 Ok((tag.to_string(), body))
1129}
1130
1131fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1132 body.as_entries()
1133 .iter()
1134 .find_map(|(key, value)| (key == field).then_some(value))
1135}
1136
1137fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1138 match ordered_field(body, field) {
1139 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1140 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1141 None => Err(format!("tagged object has no `{field}` field")),
1142 }
1143}
1144
1145fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1146 let (tag, body) = read_ordered_tagged(value, "method")?;
1147 match tag.as_str() {
1148 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1149 digest: ordered_string(&body, "digest")?,
1150 }),
1151 "macos_spawn_inode" => {
1152 let device = ordered_field(&body, "device")
1153 .and_then(|value| match value {
1154 OrderedJsonValue::Number(number) => number.as_u64(),
1155 _ => None,
1156 })
1157 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1158 let inode = ordered_field(&body, "inode")
1159 .and_then(|value| match value {
1160 OrderedJsonValue::Number(number) => number.as_u64(),
1161 _ => None,
1162 })
1163 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
1164 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1165 }
1166 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1167 }
1168}
1169
1170impl Serialize for ModuleDeclaredProvenance {
1171 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1172 where
1173 S: Serializer,
1174 {
1175 match self {
1176 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1177 build: build.clone(),
1178 }
1179 .serialize(serializer),
1180 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1181 Self::Unknown { body, .. } => body.serialize(serializer),
1182 }
1183 }
1184}
1185
1186impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1187 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1188 where
1189 D: serde::Deserializer<'de>,
1190 {
1191 let (tag, value) = read_tagged(deserializer, "status")?;
1192 match tag.as_str() {
1193 "reported" => match serde_json::from_value(value.into_value())
1194 .map_err(D::Error::custom)?
1195 {
1196 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1197 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1198 },
1199 "unverifiable" => {
1200 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1201 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1202 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1203 }
1204 }
1205 _ => Ok(Self::Unknown { tag, body: value }),
1206 }
1207 }
1208}
1209
1210impl Serialize for RunningImageAgreement {
1211 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1212 where
1213 S: Serializer,
1214 {
1215 match self {
1216 Self::Match { evidence } => RunningImageAgreementWire::Match {
1217 evidence: evidence.clone(),
1218 }
1219 .serialize(serializer),
1220 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1221 running: running.clone(),
1222 disk: disk.clone(),
1223 }
1224 .serialize(serializer),
1225 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1226 reason: reason.clone(),
1227 }
1228 .serialize(serializer),
1229 Self::Unknown { body, .. } => body.serialize(serializer),
1230 }
1231 }
1232}
1233
1234impl Serialize for ReloadPathAgreement {
1235 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1236 where
1237 S: Serializer,
1238 {
1239 match self {
1240 Self::Match => ReloadPathAgreementWire::Match.serialize(serializer),
1241 Self::Mismatch {
1242 configured,
1243 spawned_from,
1244 } => ReloadPathAgreementWire::Mismatch {
1245 configured: configured.clone(),
1246 spawned_from: spawned_from.clone(),
1247 }
1248 .serialize(serializer),
1249 Self::Unavailable { reason } => ReloadPathAgreementWire::Unavailable {
1250 reason: reason.clone(),
1251 }
1252 .serialize(serializer),
1253 Self::Unknown { body, .. } => body.serialize(serializer),
1254 }
1255 }
1256}
1257
1258impl<'de> Deserialize<'de> for ReloadPathAgreement {
1259 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1260 where
1261 D: Deserializer<'de>,
1262 {
1263 let (tag, body) = read_tagged(deserializer, "status")?;
1264 match tag.as_str() {
1265 "match" => Ok(Self::Match),
1266 "mismatch" => {
1267 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1268 ReloadPathAgreementWire::Mismatch {
1269 configured,
1270 spawned_from,
1271 } => Ok(Self::Mismatch {
1272 configured,
1273 spawned_from,
1274 }),
1275 _ => unreachable!(),
1276 }
1277 }
1278 "unavailable" => match serde_json::from_value(body.into_value())
1279 .map_err(D::Error::custom)?
1280 {
1281 ReloadPathAgreementWire::Unavailable { reason } => Ok(Self::Unavailable { reason }),
1282 _ => unreachable!(),
1283 },
1284 _ => Ok(Self::Unknown { tag, body }),
1285 }
1286 }
1287}
1288
1289impl<'de> Deserialize<'de> for RunningImageAgreement {
1290 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1291 where
1292 D: serde::Deserializer<'de>,
1293 {
1294 let (tag, value) = read_tagged(deserializer, "status")?;
1295 match tag.as_str() {
1296 "match" => Ok(Self::Match {
1297 evidence: decode_running_image_evidence(
1298 ordered_field(&value, "evidence")
1299 .cloned()
1300 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1301 )
1302 .map_err(D::Error::custom)?,
1303 }),
1304 "mismatch" => Ok(Self::Mismatch {
1305 running: decode_running_image_evidence(
1306 ordered_field(&value, "running")
1307 .cloned()
1308 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1309 )
1310 .map_err(D::Error::custom)?,
1311 disk: decode_running_image_evidence(
1312 ordered_field(&value, "disk")
1313 .cloned()
1314 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1315 )
1316 .map_err(D::Error::custom)?,
1317 }),
1318 "unavailable" => Ok(Self::Unavailable {
1319 reason: serde_json::from_value(
1320 ordered_field(&value, "reason")
1321 .cloned()
1322 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1323 .into_value(),
1324 )
1325 .map_err(D::Error::custom)?,
1326 }),
1327 _ => Ok(Self::Unknown { tag, body: value }),
1328 }
1329 }
1330}
1331
1332impl Serialize for RunningImageEvidence {
1333 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1334 where
1335 S: Serializer,
1336 {
1337 match self {
1338 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1339 digest: digest.clone(),
1340 }
1341 .serialize(serializer),
1342 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1343 device: *device,
1344 inode: *inode,
1345 }
1346 .serialize(serializer),
1347 Self::Unknown { body, .. } => body.serialize(serializer),
1348 }
1349 }
1350}
1351
1352impl<'de> Deserialize<'de> for RunningImageEvidence {
1353 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1354 where
1355 D: serde::Deserializer<'de>,
1356 {
1357 let (tag, value) = read_tagged(deserializer, "method")?;
1358 match tag.as_str() {
1359 "linux_proc_sha256" => {
1360 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1361 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1362 Ok(Self::LinuxProcSha256 { digest })
1363 }
1364 _ => unreachable!(),
1365 }
1366 }
1367 "macos_spawn_inode" => {
1368 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1369 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1370 Ok(Self::MacosSpawnInode { device, inode })
1371 }
1372 _ => unreachable!(),
1373 }
1374 }
1375 _ => Ok(Self::Unknown { tag, body: value }),
1376 }
1377 }
1378}
1379
1380impl Serialize for SupervisorRouteConsumer {
1381 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1382 where
1383 S: Serializer,
1384 {
1385 match self {
1386 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1387 module_id: module_id.clone(),
1388 }
1389 .serialize(serializer),
1390 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1391 connection_id: *connection_id,
1392 }
1393 .serialize(serializer),
1394 Self::Unknown { body, .. } => body.serialize(serializer),
1395 }
1396 }
1397}
1398
1399impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1400 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1401 where
1402 D: serde::Deserializer<'de>,
1403 {
1404 let (tag, value) = read_tagged(deserializer, "kind")?;
1405 match tag.as_str() {
1406 "reserved" => {
1407 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1408 SupervisorRouteConsumerWire::Reserved { module_id } => {
1409 Ok(Self::Reserved { module_id })
1410 }
1411 _ => unreachable!(),
1412 }
1413 }
1414 "direct" => {
1415 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1416 SupervisorRouteConsumerWire::Direct { connection_id } => {
1417 Ok(Self::Direct { connection_id })
1418 }
1419 _ => unreachable!(),
1420 }
1421 }
1422 _ => Ok(Self::Unknown { tag, body: value }),
1423 }
1424 }
1425}
1426
1427impl Serialize for StderrCaptureState {
1428 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1429 where
1430 S: Serializer,
1431 {
1432 match self {
1433 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1434 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1435 reason: reason.clone(),
1436 }
1437 .serialize(serializer),
1438 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1439 reason: reason.clone(),
1440 }
1441 .serialize(serializer),
1442 Self::Unknown { body, .. } => body.serialize(serializer),
1443 }
1444 }
1445}
1446
1447impl<'de> Deserialize<'de> for StderrCaptureState {
1448 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1449 where
1450 D: serde::Deserializer<'de>,
1451 {
1452 let (tag, value) = read_tagged(deserializer, "state")?;
1453 match tag.as_str() {
1454 "captured" => {
1455 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1456 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1457 _ => unreachable!(),
1458 }
1459 }
1460 "incomplete" => match serde_json::from_value(value.into_value())
1461 .map_err(D::Error::custom)?
1462 {
1463 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1464 _ => unreachable!(),
1465 },
1466 "not_captured" => match serde_json::from_value(value.into_value())
1467 .map_err(D::Error::custom)?
1468 {
1469 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1470 _ => unreachable!(),
1471 },
1472 _ => Ok(Self::Unknown { tag, body: value }),
1473 }
1474 }
1475}
1476
1477impl Serialize for StderrTailEntry {
1478 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1479 where
1480 S: Serializer,
1481 {
1482 match self {
1483 Self::Line { text, truncated } => StderrTailEntryWire::Line {
1484 text: text.clone(),
1485 truncated: *truncated,
1486 }
1487 .serialize(serializer),
1488 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1489 Self::Unknown { body, .. } => body.serialize(serializer),
1490 }
1491 }
1492}
1493
1494impl<'de> Deserialize<'de> for StderrTailEntry {
1495 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1496 where
1497 D: serde::Deserializer<'de>,
1498 {
1499 let (tag, value) = read_tagged(deserializer, "kind")?;
1500 match tag.as_str() {
1501 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1502 StderrTailEntryWire::Line { text, truncated } => Ok(Self::Line { text, truncated }),
1503 _ => unreachable!(),
1504 },
1505 "process_start" => {
1506 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1507 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1508 _ => unreachable!(),
1509 }
1510 }
1511 _ => Ok(Self::Unknown { tag, body: value }),
1512 }
1513 }
1514}
1515
1516fn is_zero_u64(value: &u64) -> bool {
1517 *value == 0
1518}
1519
1520fn default_true() -> bool {
1521 true
1522}
1523
1524#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1526pub struct TerminalHistory {
1527 pub daemon_started_at_ms: u64,
1529 pub entries: Vec<TerminalEntry>,
1530 #[serde(default, skip_serializing_if = "is_zero_u64")]
1533 pub dropped: u64,
1534 #[serde(default, skip_serializing_if = "is_zero_u64")]
1537 pub journal_skipped_lines: u64,
1538 #[serde(default, skip_serializing_if = "is_zero_u64")]
1540 pub journal_read_errors: u64,
1541 #[serde(default, skip_serializing_if = "is_zero_u64")]
1543 pub journal_write_failures: u64,
1544}
1545
1546#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1548pub struct TerminalEntry {
1549 #[serde(default, skip_serializing_if = "Option::is_none")]
1552 pub daemon_incarnation: Option<String>,
1553 #[serde(default, skip_serializing_if = "Option::is_none")]
1554 pub exit_code: Option<i32>,
1555 #[serde(default, skip_serializing_if = "Option::is_none")]
1556 pub exit_signal: Option<i32>,
1557 pub at_ms: u64,
1558 pub disposition: TerminalDisposition,
1559 #[serde(default, skip_serializing_if = "Option::is_none")]
1563 pub exit_kind: Option<TerminalExitKind>,
1564 #[serde(default, skip_serializing_if = "Option::is_none")]
1571 pub disposition_detail: Option<String>,
1572}
1573
1574#[derive(Debug, Clone, PartialEq, Eq)]
1579pub enum TerminalExitKind {
1580 Clean,
1581 Crash,
1582 DeliberateSeverance,
1583 Unknown(String),
1584}
1585
1586impl TerminalExitKind {
1587 fn wire_name(&self) -> &str {
1588 match self {
1589 Self::Clean => "clean",
1590 Self::Crash => "crash",
1591 Self::DeliberateSeverance => "deliberate_severance",
1592 Self::Unknown(value) => value,
1593 }
1594 }
1595}
1596
1597impl Serialize for TerminalExitKind {
1598 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1599 where
1600 S: serde::Serializer,
1601 {
1602 serializer.serialize_str(self.wire_name())
1603 }
1604}
1605
1606impl<'de> Deserialize<'de> for TerminalExitKind {
1607 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1608 where
1609 D: serde::Deserializer<'de>,
1610 {
1611 let value = String::deserialize(deserializer)?;
1612 Ok(match value.as_str() {
1613 "clean" => Self::Clean,
1614 "crash" => Self::Crash,
1615 "deliberate_severance" => Self::DeliberateSeverance,
1616 _ => Self::Unknown(value),
1617 })
1618 }
1619}
1620
1621open_string_enum! {
1622 TerminalDisposition {
1624 Stopped => "stopped",
1625 Disabled => "disabled",
1626 Failed => "failed",
1627 Restarting => "restarting",
1628 DaemonShutdown => "daemon_shutdown",
1633 }
1634}
1635
1636#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1637#[serde(rename_all = "snake_case")]
1638pub enum PollKind {
1639 Status,
1640 Liveness,
1641}
1642
1643#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1644pub struct CatalogEntry {
1645 pub module_id: String,
1646 #[serde(default = "default_true")]
1657 pub ready: bool,
1658 #[serde(default, skip_serializing_if = "Option::is_none")]
1662 pub not_ready: Option<NotReadyReason>,
1663 #[serde(default, skip_serializing_if = "Option::is_none")]
1684 pub module_version: Option<String>,
1685 pub roles: Vec<ProviderRole>,
1686 pub control_ops: Vec<String>,
1687 #[serde(default, skip_serializing_if = "Option::is_none")]
1692 pub capabilities: Option<CapabilityDeclarations>,
1693 #[serde(default, skip_serializing_if = "Option::is_none")]
1696 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1697}
1698
1699#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1701pub struct NotReadyReason {
1702 pub reason: String,
1707 #[serde(default, skip_serializing_if = "Option::is_none")]
1710 pub capability: Option<String>,
1711}
1712
1713impl NotReadyReason {
1714 pub const DECLARED_NOT_READY: &'static str = "declared_not_ready";
1715 pub const REQUIRED_CAPABILITY_UNPROVIDED: &'static str = "required_capability_unprovided";
1716}
1717
1718#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1719pub struct CapabilityRequirementStatus {
1720 pub consumer: String,
1721 pub capability: String,
1722 pub need: String,
1723 pub verdict: String,
1724 pub episode_seq: u64,
1725 pub config_satisfiable: bool,
1726 pub runtime_available: bool,
1727 pub detail: String,
1728}
1729
1730#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1731pub struct SupervisorRescanResult {
1732 pub added: Vec<String>,
1733 pub removed: Vec<String>,
1734 pub changed_pending_reload: Vec<String>,
1735 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1748 pub enabled_changes: Vec<String>,
1749 pub unchanged: u32,
1750 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1758 pub preview: bool,
1759 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1777 pub restart_required: Vec<String>,
1778 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1782 pub capability_warnings: Vec<String>,
1783}
1784
1785#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1801#[serde(rename_all = "snake_case")]
1802pub enum ModuleProtocol {
1803 #[default]
1807 Subc,
1808 None,
1810}
1811
1812#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1813pub struct SupervisorEntry {
1814 pub module_id: String,
1815 pub state: String,
1816 pub enabled: bool,
1817 pub live: bool,
1827 #[serde(default)]
1831 pub protocol: ModuleProtocol,
1832 pub health: SupervisorHealthStatus,
1833 #[serde(default, skip_serializing_if = "Option::is_none")]
1836 pub pending_reload: Option<PendingReloadVerdict>,
1837 #[serde(default)]
1843 pub last_probe_ms: Option<u64>,
1844 #[serde(default, skip_serializing_if = "Option::is_none")]
1848 pub last_exit_code: Option<i32>,
1849 #[serde(default, skip_serializing_if = "Option::is_none")]
1853 pub last_exit_signal: Option<i32>,
1854 #[serde(default, skip_serializing_if = "Option::is_none")]
1858 pub last_exit_ms: Option<u64>,
1859 #[serde(default, skip_serializing_if = "Option::is_none")]
1862 pub last_exit_kind: Option<TerminalExitKind>,
1863 #[serde(default, skip_serializing_if = "Option::is_none")]
1880 pub restart_count: Option<u32>,
1881 #[serde(default, skip_serializing_if = "Option::is_none")]
1884 pub max_restarts: Option<u32>,
1885 #[serde(default, skip_serializing_if = "Option::is_none")]
1888 pub lifetime_restarts: Option<u32>,
1889 #[serde(default, skip_serializing_if = "Option::is_none")]
1893 pub spawn_generation: Option<u64>,
1894 #[serde(default, skip_serializing_if = "Option::is_none")]
1904 pub restart_window_secs: Option<u64>,
1905 #[serde(default, skip_serializing_if = "Option::is_none")]
1909 pub drain_timeout_ms: Option<u64>,
1910 #[serde(default, skip_serializing_if = "Option::is_none")]
1913 pub restart_backoff_ms: Option<u64>,
1914 #[serde(default, skip_serializing_if = "Option::is_none")]
1917 pub restart_max_backoff_ms: Option<u64>,
1918}
1919
1920#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1921#[serde(rename_all = "snake_case")]
1922pub enum SupervisorHealthStatus {
1923 Ok,
1924 Degraded,
1925 Failing,
1926 Unresponsive,
1927 Unknown,
1928}
1929
1930#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1931pub struct SupervisorHealthEntry {
1932 pub module_id: String,
1933 pub status: SupervisorHealthStatus,
1934 #[serde(default, skip_serializing_if = "Option::is_none")]
1940 pub detail: Option<String>,
1941 #[serde(default, skip_serializing_if = "Option::is_none")]
1946 pub metrics: Option<serde_json::Value>,
1947 pub consecutive_failures: u32,
1948 #[serde(default)]
1951 pub late_answer_count: u64,
1952 #[serde(default, skip_serializing_if = "Option::is_none")]
1954 pub last_late_answer_latency_ms: Option<u64>,
1955 #[serde(default)]
1960 pub last_action: Option<String>,
1961 #[serde(default)]
1964 pub last_action_ms: Option<u64>,
1965 #[serde(default, skip_serializing_if = "Option::is_none")]
1978 pub last_probe_ms: Option<u64>,
1979}
1980
1981#[cfg(test)]
1982mod tests {
1983 use super::*;
1984 use subc_protocol::{BindIdentity, RouteTarget};
1985
1986 #[test]
1987 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
1988 let entry = TerminalEntry {
1989 daemon_incarnation: Some("daemon-before-restart".into()),
1990 exit_code: Some(1),
1991 exit_signal: None,
1992 at_ms: 1_700_000_000_123,
1993 disposition: TerminalDisposition::Restarting,
1994 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
1995 disposition_detail: None,
1996 };
1997 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
1998 assert_eq!(
1999 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
2000 ["exit_kind"],
2001 "deliberate_severance"
2002 );
2003
2004 #[derive(serde::Deserialize)]
2005 struct LegacyTerminalEntry {
2006 exit_code: Option<i32>,
2007 exit_signal: Option<i32>,
2008 at_ms: u64,
2009 disposition: TerminalDisposition,
2010 }
2011
2012 let decoded: LegacyTerminalEntry =
2013 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
2014 assert_eq!(decoded.exit_code, Some(1));
2015 assert_eq!(decoded.exit_signal, None);
2016 assert_eq!(decoded.at_ms, 1_700_000_000_123);
2017 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
2018
2019 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
2020 let future: TerminalEntry =
2021 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
2022 assert_eq!(
2023 future.exit_kind,
2024 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
2025 );
2026 }
2027
2028 #[test]
2029 fn terminal_incarnation_is_optional_for_older_daemons() {
2030 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
2031 "at_ms": 123,
2032 "disposition": "stopped"
2033 }))
2034 .unwrap();
2035 let encoded = serde_json::to_value(&entry).unwrap();
2036 assert_eq!(
2037 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
2038 (None, None)
2039 );
2040 }
2041
2042 #[test]
2043 fn route_poll_uses_kind_field() {
2044 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
2045 route_channel: 7,
2046 route_epoch: 11,
2047 kind: PollKind::Status,
2048 })
2049 .unwrap();
2050
2051 assert_eq!(body["op"], "route.poll");
2052 assert_eq!(body["route_epoch"], 11);
2053 assert_eq!(body["kind"], "status");
2054 assert!(body.get("op").is_some());
2055 }
2056
2057 #[test]
2058 fn route_open_is_internally_tagged() {
2059 let request = ClientControlRequest::RouteOpen {
2060 target: RouteTarget::ToolProvider {
2061 module_id: "aft".to_string(),
2062 },
2063 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
2064 consumer_identity: None,
2065 consumer_capabilities: None,
2066 admission_facts: None,
2067 };
2068
2069 let body = serde_json::to_value(request).unwrap();
2070 assert_eq!(body["op"], "route.open");
2071 assert_eq!(body["target"]["kind"], "tool_provider");
2072 assert!(body.get("consumer_identity").is_none());
2073 assert!(body.get("consumer_capabilities").is_none());
2074 }
2075
2076 #[test]
2077 fn route_open_without_optional_fields_still_decodes() {
2078 let body = serde_json::json!({
2079 "op": "route.open",
2080 "target": { "kind": "tool_provider", "module_id": "aft" },
2081 "identity": {
2082 "project_root": "/tmp/project",
2083 "harness": "opencode",
2084 "session": "session-1"
2085 }
2086 });
2087
2088 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
2089 let ClientControlRequest::RouteOpen {
2090 consumer_identity,
2091 consumer_capabilities,
2092 admission_facts,
2093 ..
2094 } = decoded
2095 else {
2096 panic!("decoded wrong request variant");
2097 };
2098 assert_eq!(consumer_identity, None);
2099 assert_eq!(consumer_capabilities, None);
2100 assert_eq!(admission_facts, None);
2101 }
2102
2103 #[test]
2104 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
2105 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
2106 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
2107 match decoded {
2108 ClientControlPush::RouteClosed {
2109 excluded_subscriptions,
2110 terminal,
2111 ..
2112 } => {
2113 assert_eq!(excluded_subscriptions, 0);
2114 assert_eq!(terminal, None);
2115 }
2116 other => panic!("unexpected push: {other:?}"),
2117 }
2118 assert!(!serde_json::to_string(&decoded)
2119 .unwrap()
2120 .contains("terminal"));
2121 }
2122
2123 #[test]
2124 fn old_route_closed_decoder_ignores_new_terminal_field() {
2125 #[derive(serde::Deserialize)]
2126 #[serde(tag = "op")]
2127 enum LegacyClientControlPush {
2128 #[serde(rename = "route.closed")]
2129 RouteClosed {
2130 module_id: String,
2131 reason: RouteCloseReason,
2132 drained: bool,
2133 abandoned: u32,
2134 },
2135 }
2136
2137 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
2138 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
2139 match decoded {
2140 LegacyClientControlPush::RouteClosed {
2141 module_id,
2142 reason,
2143 drained,
2144 abandoned,
2145 } => {
2146 assert_eq!(module_id, "aft-tools");
2147 assert_eq!(reason, RouteCloseReason::Crash);
2148 assert!(!drained);
2149 assert_eq!(abandoned, 0);
2150 }
2151 }
2152 }
2153
2154 #[test]
2155 fn supervisor_routes_is_a_control_plane_request() {
2156 let body = serde_json::json!({
2157 "op": "supervisor.routes",
2158 "module_id": "aft"
2159 });
2160
2161 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
2162 assert_eq!(serde_json::to_value(request).unwrap(), body);
2163 }
2164
2165 #[test]
2166 fn diagnostic_string_enums_retain_unknown_wire_values() {
2167 let reason: RunningImageUnavailableReason =
2168 serde_json::from_str("\"future_reason\"").unwrap();
2169 let disposition: TerminalDisposition =
2170 serde_json::from_str("\"future_disposition\"").unwrap();
2171
2172 assert_eq!(
2173 reason,
2174 RunningImageUnavailableReason::Unknown("future_reason".to_string())
2175 );
2176 assert_eq!(
2177 disposition,
2178 TerminalDisposition::Unknown("future_disposition".to_string())
2179 );
2180 }
2181
2182 #[test]
2183 fn diagnostic_string_enums_preserve_existing_wire_names() {
2184 let names = [
2185 (RunningImageUnavailableReason::NotRunning, "not_running"),
2186 (
2187 RunningImageUnavailableReason::UnsupportedPlatform,
2188 "unsupported_platform",
2189 ),
2190 (
2191 RunningImageUnavailableReason::RunningExecutableUnreadable,
2192 "running_executable_unreadable",
2193 ),
2194 (
2195 RunningImageUnavailableReason::SpawnedPathUnreadable,
2196 "spawned_path_unreadable",
2197 ),
2198 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2199 (
2200 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2201 "process_identity_unconfirmed",
2202 ),
2203 ];
2204 for (value, expected) in names {
2205 let wire = serde_json::to_string(&value).unwrap();
2206 assert_eq!(wire, format!("\"{expected}\""));
2207 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2208 assert_eq!(decoded, value);
2209 }
2210
2211 for (value, expected) in [
2212 (TerminalDisposition::Stopped, "stopped"),
2213 (TerminalDisposition::Disabled, "disabled"),
2214 (TerminalDisposition::Failed, "failed"),
2215 (TerminalDisposition::Restarting, "restarting"),
2216 (TerminalDisposition::DaemonShutdown, "daemon_shutdown"),
2217 ] {
2218 let wire = serde_json::to_string(&value).unwrap();
2219 assert_eq!(wire, format!("\"{expected}\""));
2220 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2221 assert_eq!(decoded, value);
2222 }
2223 }
2224
2225 #[test]
2226 fn diagnostic_string_enums_reject_non_string_bodies() {
2227 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2228 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2229 }
2230
2231 #[test]
2232 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2233 let body = serde_json::json!({
2234 "op": "supervisor.provenance",
2235 "daemon": {
2236 "daemon_build": {},
2237 "daemon_observed": {
2238 "running_image": {
2239 "status": "unavailable",
2240 "reason": "not_running"
2241 }
2242 }
2243 },
2244 "modules": [
2245 {
2246 "module_id": "future",
2247 "module_declared": { "status": "unverifiable" },
2248 "daemon_observed": {
2249 "running_image": {
2250 "status": "unavailable",
2251 "reason": "future_reason"
2252 }
2253 }
2254 },
2255 {
2256 "module_id": "healthy-a",
2257 "module_declared": { "status": "unverifiable" },
2258 "daemon_observed": {
2259 "running_image": {
2260 "status": "match",
2261 "evidence": {
2262 "method": "linux_proc_sha256",
2263 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2264 }
2265 }
2266 }
2267 },
2268 {
2269 "module_id": "healthy-b",
2270 "module_declared": { "status": "unverifiable" },
2271 "daemon_observed": {
2272 "running_image": {
2273 "status": "unavailable",
2274 "reason": "unsupported_platform"
2275 }
2276 }
2277 }
2278 ]
2279 });
2280
2281 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2282 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2283 panic!("decoded wrong response variant");
2284 };
2285 assert_eq!(modules.len(), 3);
2286 assert_eq!(modules[0].module_id, "future");
2287 assert_eq!(
2288 modules[0].daemon_observed.running_image,
2289 RunningImageAgreement::Unavailable {
2290 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2291 }
2292 );
2293 assert_eq!(modules[1].module_id, "healthy-a");
2294 assert_eq!(modules[2].module_id, "healthy-b");
2295 }
2296
2297 #[test]
2298 fn tagged_unknown_values_retain_tag_and_body() {
2299 macro_rules! assert_unknown_round_trip {
2300 ($ty:ident, $field:literal, $value:expr) => {
2301 let value = $value;
2302 let wire = serde_json::to_string(&value).unwrap();
2303 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2304 match decoded {
2305 $ty::Unknown { tag, body } => {
2306 assert_eq!(tag, value[$field].as_str().unwrap());
2307 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2308 }
2309 _ => panic!("decoded known variant"),
2310 }
2311 };
2312 }
2313
2314 assert_unknown_round_trip!(
2315 ModuleDeclaredProvenance,
2316 "status",
2317 serde_json::json!({"status": "future", "build": {"version": 7}})
2318 );
2319 assert_unknown_round_trip!(
2320 RunningImageAgreement,
2321 "status",
2322 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2323 );
2324 assert_unknown_round_trip!(
2325 RunningImageEvidence,
2326 "method",
2327 serde_json::json!({"method": "future", "digest": "abc"})
2328 );
2329 assert_unknown_round_trip!(
2330 SupervisorRouteConsumer,
2331 "kind",
2332 serde_json::json!({"kind": "future", "module_id": "m"})
2333 );
2334 assert_unknown_round_trip!(
2335 StderrCaptureState,
2336 "state",
2337 serde_json::json!({"state": "future", "reason": "because"})
2338 );
2339 assert_unknown_round_trip!(
2340 StderrTailEntry,
2341 "kind",
2342 serde_json::json!({"kind": "future", "text": "line"})
2343 );
2344 }
2345
2346 #[test]
2347 fn tagged_unknown_values_round_trip_the_original_json() {
2348 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2349 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2350 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2351 }
2352
2353 #[test]
2354 fn tagged_unknown_values_round_trip_trailing_tag() {
2355 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2356 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2357 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2358
2359 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2360 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2361 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2362 }
2363
2364 #[test]
2365 fn tagged_unknown_values_round_trip_middle_tag() {
2366 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2367 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2368 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2369
2370 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2371 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2372 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2373 }
2374
2375 #[test]
2376 fn tagged_unknown_values_round_trip_deep_payload() {
2377 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2378 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2379 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2380
2381 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2382 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2383 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2384 }
2385
2386 #[test]
2387 fn tagged_unknown_values_reject_non_object_bodies() {
2388 for wire in ["42", r#""future""#, "[]"] {
2389 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2390 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2391 }
2392 }
2393
2394 #[test]
2395 fn duplicate_discriminators_reject_without_panicking() {
2396 assert_eq!(
2397 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2398 .unwrap(),
2399 ModuleDeclaredProvenance::Unverifiable
2400 );
2401 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2402 .unwrap()
2403 {
2404 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2405 _ => panic!("future discriminator decoded as a known variant"),
2406 }
2407
2408 let wires = [
2409 r#"{"status":"reported","status":"unverifiable"}"#,
2410 r#"{"status":"unverifiable","status":"reported"}"#,
2411 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2412 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2413 ];
2414
2415 for wire in wires {
2416 let result =
2417 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2418 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2419 assert!(
2420 result.unwrap().is_err(),
2421 "duplicate discriminator decoded: {wire}"
2422 );
2423 }
2424
2425 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2426 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2427 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2428 assert!(
2429 result.unwrap().is_err(),
2430 "duplicate discriminator decoded: {wire}"
2431 );
2432 }
2433
2434 #[test]
2435 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2436 let known_wire =
2437 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2438 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2439 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2440
2441 for wire in [
2442 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2443 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2444 ] {
2445 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2446 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2447 }
2448
2449 for wire in [
2450 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2451 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2452 ] {
2453 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2454 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2455 }
2456
2457 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2458 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2459 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2460
2461 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2462 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2463 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2464 }
2465
2466 #[test]
2467 fn tagged_unknown_member_does_not_discard_known_siblings() {
2468 let body = serde_json::json!({
2469 "modules": [{
2470 "module_id": "target",
2471 "routes": [
2472 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2473 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2474 ]
2475 }]
2476 });
2477 let decoded: ClientControlResponse = serde_json::from_value(
2478 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2479 )
2480 .unwrap();
2481 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2482 panic!("decoded wrong response variant");
2483 };
2484 assert_eq!(modules[0].routes.len(), 2);
2485 assert_eq!(
2486 modules[0].routes[1].consumer,
2487 SupervisorRouteConsumer::Direct { connection_id: 7 }
2488 );
2489 }
2490}
2491
2492#[cfg(test)]
2493mod launch_nonce_redaction_tests {
2494 use super::*;
2495
2496 const NONCE: &str = "nonce-f00dfeed1234abcd";
2497
2498 fn identity() -> ConsumerIdentity {
2499 ConsumerIdentity {
2500 module_id: "wernicke".to_string(),
2501 launch_nonce: NONCE.to_string(),
2502 }
2503 }
2504
2505 #[test]
2506 fn consumer_identity_debug_names_the_module_and_never_the_nonce() {
2507 let printed = format!("{:?}", identity());
2508 assert!(printed.contains("wernicke"), "{printed}");
2509 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
2510 }
2511
2512 #[test]
2513 fn route_open_request_debug_never_prints_the_nonce() {
2514 let request = ClientControlRequest::RouteOpen {
2515 target: subc_protocol::RouteTarget::ToolProvider {
2516 module_id: "broca".to_string(),
2517 },
2518 identity: subc_protocol::BindIdentity::new(
2519 PathBuf::from("/tmp/project"),
2520 "test".to_string(),
2521 "session".to_string(),
2522 ),
2523 consumer_identity: Some(identity()),
2524 consumer_capabilities: None,
2525 admission_facts: None,
2526 };
2527 let printed = format!("{request:?}");
2528 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
2529 }
2530}