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:ident => $wire_name:literal ),+ $(,)?
30 }
31 ) => {
32 $(#[$meta])*
33 #[derive(Debug, Clone, PartialEq, Eq)]
34 pub enum $name {
35 $( $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(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
74pub struct ConsumerIdentity {
75 pub module_id: String,
76 pub launch_nonce: String,
77}
78
79pub mod ops {
90 pub const SERVER: &str = "server.";
91 pub const CATALOG: &str = "catalog.";
92 pub const ROUTE: &str = "route.";
93 pub const SUPERVISOR: &str = "supervisor.";
94 pub const CONFIG: &str = "config.";
95
96 pub const SERVER_DESCRIBE: &str = "server.describe";
97 pub const CATALOG_LIST: &str = "catalog.list";
98 pub const ROUTE_OPEN: &str = "route.open";
99 pub const ROUTE_POLL: &str = "route.poll";
100 pub const ROUTE_CLOSING: &str = "route.closing";
101 pub const ROUTE_CLOSED: &str = "route.closed";
102 pub const SUPERVISOR_LIST: &str = "supervisor.list";
103 pub const SUPERVISOR_RESTART: &str = "supervisor.restart";
104 pub const SUPERVISOR_RELOAD: &str = "supervisor.reload";
105 pub const SUPERVISOR_RESCAN: &str = "supervisor.rescan";
106 pub const SUPERVISOR_RELEASE_RESERVED: &str = "supervisor.release_reserved";
107 pub const SUPERVISOR_SET_ENABLED: &str = "supervisor.set_enabled";
108 pub const SUPERVISOR_HEALTH_PROBE: &str = "supervisor.health_probe";
109 pub const SUPERVISOR_HEALTH: &str = "supervisor.health";
110 pub const SUPERVISOR_STDERR_TAIL: &str = "supervisor.stderr_tail";
111 pub const SUPERVISOR_TERMINALS: &str = "supervisor.terminals";
112 pub const SUPERVISOR_ROUTES: &str = "supervisor.routes";
113 pub const SUPERVISOR_PROVENANCE: &str = "supervisor.provenance";
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
118#[serde(tag = "op")]
119#[allow(clippy::large_enum_variant)]
122pub enum ClientControlRequest {
123 #[serde(rename = "server.describe")]
124 ServerDescribe {},
125 #[serde(rename = "catalog.list")]
126 CatalogList {
127 #[serde(default)]
132 module_id: Option<String>,
133 },
134 #[serde(rename = "route.open")]
135 RouteOpen {
136 target: RouteTarget,
137 identity: BindIdentity,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
147 consumer_identity: Option<ConsumerIdentity>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
156 consumer_capabilities: Option<Vec<String>>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
159 admission_facts: Option<serde_json::Value>,
160 },
161 #[serde(rename = "route.poll")]
162 RoutePoll {
163 route_channel: u16,
164 route_epoch: u32,
165 kind: PollKind,
166 },
167 #[serde(rename = "supervisor.list")]
168 SupervisorList {},
169 #[serde(rename = "supervisor.restart")]
170 SupervisorRestart {
171 module_id: String,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
180 drain_timeout_ms: Option<u64>,
181 },
182 #[serde(rename = "supervisor.reload")]
183 SupervisorReload { module_id: String },
184 #[serde(rename = "supervisor.rescan")]
185 SupervisorRescan {
186 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
206 preview: bool,
207 },
208 #[serde(rename = "supervisor.release_reserved")]
212 SupervisorReleaseReserved { module_id: String },
213 #[serde(rename = "supervisor.set_enabled")]
214 SupervisorSetEnabled { module_id: String, enabled: bool },
215 #[serde(rename = "supervisor.health_probe")]
216 SupervisorHealthProbe { module_id: String },
217 #[serde(rename = "supervisor.health")]
218 SupervisorHealth {},
219 #[serde(rename = "supervisor.routes")]
232 SupervisorRoutes {
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 module_id: Option<String>,
235 },
236 #[serde(rename = "supervisor.provenance")]
239 SupervisorProvenance {
240 #[serde(default, skip_serializing_if = "Option::is_none")]
241 module_id: Option<String>,
242 },
243 #[serde(rename = "supervisor.stderr_tail")]
251 SupervisorStderrTail {
252 module_id: String,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
254 max_lines: Option<u32>,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
256 max_bytes: Option<u32>,
257 },
258 #[serde(rename = "supervisor.terminals")]
271 SupervisorTerminals { module_id: String },
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
276#[serde(tag = "op")]
277pub enum ClientControlResponse {
278 #[serde(rename = "server.describe")]
279 ServerDescribe {
280 protocol_ver: u8,
281 subc_ops: Vec<String>,
282 capabilities: Vec<String>,
283 connected_clients: u64,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
285 counters: Option<serde_json::Value>,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
293 build_git_sha: Option<String>,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
300 build_lock_digest: Option<String>,
301 #[serde(default, skip_serializing_if = "Vec::is_empty")]
305 capability_requirements: Vec<CapabilityRequirementStatus>,
306 },
307 #[serde(rename = "catalog.list")]
308 CatalogList {
309 generation: u64,
310 modules: Vec<CatalogEntry>,
311 subc_ops: Vec<String>,
312 },
313 #[serde(rename = "route.open")]
314 RouteOpen {
315 route_channel: u16,
316 route_epoch: u32,
317 },
318 #[serde(rename = "route.poll")]
319 RoutePoll {
320 route_channel: u16,
321 route_epoch: u32,
322 status: Option<String>,
323 live: Option<bool>,
324 },
325 #[serde(rename = "supervisor.list")]
326 SupervisorList {
327 generation: u64,
328 modules: Vec<SupervisorEntry>,
329 },
330 #[serde(rename = "supervisor.ack")]
331 SupervisorAck { module_id: String, applied: bool },
332 #[serde(rename = "supervisor.rescan")]
333 SupervisorRescan {
334 #[serde(flatten)]
335 result: SupervisorRescanResult,
336 },
337 #[serde(rename = "supervisor.health_probe")]
338 SupervisorHealthProbe {
339 module_id: String,
340 status: HealthStatus,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 detail: Option<String>,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
344 metrics: Option<serde_json::Value>,
345 },
346 #[serde(rename = "supervisor.health")]
347 SupervisorHealth {
348 generation: u64,
349 modules: Vec<SupervisorHealthEntry>,
350 },
351 #[serde(rename = "supervisor.routes")]
352 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
353 #[serde(rename = "supervisor.provenance")]
354 SupervisorProvenance {
355 daemon: SupervisorDaemonProvenance,
356 modules: Vec<SupervisorModuleProvenance>,
357 },
358 #[serde(rename = "supervisor.stderr_tail")]
359 SupervisorStderrTail {
360 module_id: String,
361 #[serde(flatten)]
362 tail: StderrTail,
363 },
364 #[serde(rename = "supervisor.terminals")]
365 SupervisorTerminals {
366 module_id: String,
367 #[serde(flatten)]
368 terminals: TerminalHistory,
369 },
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
377#[serde(tag = "op")]
378pub enum ClientControlPush {
379 #[serde(rename = "route.closing")]
380 RouteClosing {
381 module_id: String,
382 reason: RouteCloseReason,
383 },
384 #[serde(rename = "route.closed")]
385 RouteClosed {
386 module_id: String,
387 reason: RouteCloseReason,
388 drained: bool,
390 abandoned: u32,
393 #[serde(default)]
395 excluded_subscriptions: u32,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
402 terminal: Option<bool>,
403 },
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
408pub struct StderrTail {
409 pub capture: StderrCaptureState,
410 pub entries: Vec<StderrTailEntry>,
411 #[serde(default, skip_serializing_if = "is_zero_u64")]
420 pub dropped_lines: u64,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
425pub struct SupervisorRouteModule {
426 pub module_id: String,
427 pub routes: Vec<SupervisorRoute>,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
432pub struct SupervisorRoute {
433 pub consumer: SupervisorRouteConsumer,
434 pub age_ms: u64,
436 pub draining: bool,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub drain_reason: Option<RouteCloseReason>,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
450pub struct SupervisorModuleProvenance {
451 pub module_id: String,
452 pub module_declared: ModuleDeclaredProvenance,
453 pub daemon_observed: SupervisorObservedProcess,
454}
455
456#[derive(Debug, Clone, PartialEq)]
458pub enum ModuleDeclaredProvenance {
459 Reported {
460 build: ManifestProvenance,
461 },
462 Unverifiable,
463 Unknown {
466 tag: String,
467 body: OrderedJsonObject,
468 },
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
476pub struct SupervisorObservedProcess {
477 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub pid: Option<u32>,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub spawned_at_ms: Option<u64>,
481 #[serde(default, skip_serializing_if = "Option::is_none")]
482 pub spawned_from: Option<PathBuf>,
483 pub running_image: RunningImageAgreement,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
488pub struct SupervisorDaemonProvenance {
489 pub daemon_build: DaemonBuildProvenance,
490 pub daemon_observed: DaemonObservedProcess,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
495pub struct DaemonBuildProvenance {
496 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub build_git_sha: Option<String>,
498 #[serde(default, skip_serializing_if = "Option::is_none")]
499 pub build_lock_digest: Option<String>,
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
504pub struct DaemonObservedProcess {
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub pid: Option<u32>,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub started_at_ms: Option<u64>,
509 pub running_image: RunningImageAgreement,
510}
511
512#[derive(Debug, Clone, PartialEq)]
514pub enum RunningImageAgreement {
515 Match {
516 evidence: RunningImageEvidence,
517 },
518 Mismatch {
519 running: RunningImageEvidence,
520 disk: RunningImageEvidence,
521 },
522 Unavailable {
523 reason: RunningImageUnavailableReason,
524 },
525 Unknown {
528 tag: String,
529 body: OrderedJsonObject,
530 },
531}
532
533#[derive(Debug, Clone, PartialEq)]
535pub enum RunningImageEvidence {
536 LinuxProcSha256 {
537 digest: String,
538 },
539 MacosSpawnInode {
540 device: u64,
541 inode: u64,
542 },
543 Unknown {
546 tag: String,
547 body: OrderedJsonObject,
548 },
549}
550
551open_string_enum! {
552 RunningImageUnavailableReason {
554 NotRunning => "not_running",
555 UnsupportedPlatform => "unsupported_platform",
556 RunningExecutableUnreadable => "running_executable_unreadable",
557 SpawnedPathUnreadable => "spawned_path_unreadable",
558 HashFailed => "hash_failed",
559 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
560 }
561}
562
563#[derive(Debug, Clone, PartialEq)]
569pub enum SupervisorRouteConsumer {
570 Reserved {
571 module_id: String,
572 },
573 Direct {
574 connection_id: u64,
575 },
576 Unknown {
579 tag: String,
580 body: OrderedJsonObject,
581 },
582}
583
584#[derive(Debug, Clone, PartialEq)]
591pub enum StderrCaptureState {
592 Captured,
595 Incomplete { reason: String },
597 NotCaptured { reason: String },
599 Unknown {
602 tag: String,
603 body: OrderedJsonObject,
604 },
605}
606
607#[derive(Debug, Clone, PartialEq)]
608pub enum StderrTailEntry {
609 Line {
610 text: String,
611 truncated: bool,
616 },
617 ProcessStart,
622 Unknown {
625 tag: String,
626 body: OrderedJsonObject,
627 },
628}
629
630#[derive(Debug, Serialize, Deserialize)]
631#[serde(tag = "status", rename_all = "snake_case")]
632enum ModuleDeclaredProvenanceWire {
633 Reported { build: ManifestProvenance },
634 Unverifiable,
635}
636
637#[derive(Debug, Serialize, Deserialize)]
638#[serde(tag = "status", rename_all = "snake_case")]
639enum RunningImageAgreementWire {
640 Match {
641 evidence: RunningImageEvidence,
642 },
643 Mismatch {
644 running: RunningImageEvidence,
645 disk: RunningImageEvidence,
646 },
647 Unavailable {
648 reason: RunningImageUnavailableReason,
649 },
650}
651
652#[derive(Debug, Serialize, Deserialize)]
653#[serde(tag = "method", rename_all = "snake_case")]
654enum RunningImageEvidenceWire {
655 LinuxProcSha256 { digest: String },
656 MacosSpawnInode { device: u64, inode: u64 },
657}
658
659#[derive(Debug, Serialize, Deserialize)]
660#[serde(tag = "kind", rename_all = "snake_case")]
661enum SupervisorRouteConsumerWire {
662 Reserved { module_id: String },
663 Direct { connection_id: u64 },
664}
665
666#[derive(Debug, Serialize, Deserialize)]
667#[serde(tag = "state", rename_all = "snake_case")]
668enum StderrCaptureStateWire {
669 Captured,
670 Incomplete { reason: String },
671 NotCaptured { reason: String },
672}
673
674#[derive(Debug, Serialize, Deserialize)]
675#[serde(tag = "kind", rename_all = "snake_case")]
676enum StderrTailEntryWire {
677 Line {
678 text: String,
679 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
680 truncated: bool,
681 },
682 ProcessStart,
683}
684
685#[derive(Debug, Clone, PartialEq)]
687pub enum OrderedJsonValue {
688 Null,
689 Bool(bool),
690 Number(serde_json::Number),
691 String(String),
692 Array(Vec<Self>),
693 Object(OrderedJsonObject),
694}
695
696#[derive(Debug, Clone, PartialEq)]
698pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
699
700impl OrderedJsonObject {
701 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
703 &self.0
704 }
705
706 fn into_value(self) -> serde_json::Value {
707 serde_json::Value::Object(
708 self.0
709 .into_iter()
710 .map(|(key, value)| (key, value.into_value()))
711 .collect(),
712 )
713 }
714}
715
716impl OrderedJsonValue {
717 fn into_value(self) -> serde_json::Value {
718 match self {
719 Self::Null => serde_json::Value::Null,
720 Self::Bool(value) => serde_json::Value::Bool(value),
721 Self::Number(value) => serde_json::Value::Number(value),
722 Self::String(value) => serde_json::Value::String(value),
723 Self::Array(values) => {
724 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
725 }
726 Self::Object(value) => value.into_value(),
727 }
728 }
729}
730
731impl Serialize for OrderedJsonValue {
732 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
733 where
734 S: Serializer,
735 {
736 match self {
737 Self::Null => serializer.serialize_unit(),
738 Self::Bool(value) => serializer.serialize_bool(*value),
739 Self::Number(value) => value.serialize(serializer),
740 Self::String(value) => serializer.serialize_str(value),
741 Self::Array(values) => values.serialize(serializer),
742 Self::Object(value) => value.serialize(serializer),
743 }
744 }
745}
746
747impl<'de> Deserialize<'de> for OrderedJsonValue {
748 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
749 where
750 D: Deserializer<'de>,
751 {
752 struct OrderedValueVisitor;
753
754 impl<'de> Visitor<'de> for OrderedValueVisitor {
755 type Value = OrderedJsonValue;
756
757 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
758 formatter.write_str("a JSON value with ordered object members")
759 }
760
761 fn visit_unit<E>(self) -> Result<Self::Value, E>
762 where
763 E: serde::de::Error,
764 {
765 Ok(OrderedJsonValue::Null)
766 }
767
768 fn visit_none<E>(self) -> Result<Self::Value, E>
769 where
770 E: serde::de::Error,
771 {
772 Ok(OrderedJsonValue::Null)
773 }
774
775 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
776 where
777 D: Deserializer<'de>,
778 {
779 OrderedJsonValue::deserialize(deserializer)
780 }
781
782 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
783 where
784 E: serde::de::Error,
785 {
786 Ok(OrderedJsonValue::Bool(value))
787 }
788
789 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
790 where
791 E: serde::de::Error,
792 {
793 Ok(OrderedJsonValue::Number(value.into()))
794 }
795
796 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
797 where
798 E: serde::de::Error,
799 {
800 Ok(OrderedJsonValue::Number(value.into()))
801 }
802
803 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
804 where
805 E: serde::de::Error,
806 {
807 serde_json::Number::from_f64(value)
808 .map(OrderedJsonValue::Number)
809 .ok_or_else(|| E::custom("non-finite JSON number"))
810 }
811
812 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
813 where
814 E: serde::de::Error,
815 {
816 Ok(OrderedJsonValue::String(value.to_owned()))
817 }
818
819 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
820 where
821 E: serde::de::Error,
822 {
823 Ok(OrderedJsonValue::String(value))
824 }
825
826 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
827 where
828 A: SeqAccess<'de>,
829 {
830 let mut values = Vec::new();
831 while let Some(value) = sequence.next_element()? {
832 values.push(value);
833 }
834 Ok(OrderedJsonValue::Array(values))
835 }
836
837 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
838 where
839 A: MapAccess<'de>,
840 {
841 let mut entries = Vec::new();
842 while let Some((key, value)) = map.next_entry()? {
843 entries.push((key, value));
844 }
845 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
846 }
847 }
848
849 deserializer.deserialize_any(OrderedValueVisitor)
850 }
851}
852
853impl Serialize for OrderedJsonObject {
854 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
855 where
856 S: Serializer,
857 {
858 let mut map = serializer.serialize_map(Some(self.0.len()))?;
859 for (key, value) in &self.0 {
860 map.serialize_entry(key, value)?;
861 }
862 map.end()
863 }
864}
865
866impl<'de> Deserialize<'de> for OrderedJsonObject {
867 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
868 where
869 D: Deserializer<'de>,
870 {
871 struct OrderedObjectVisitor;
872
873 impl<'de> Visitor<'de> for OrderedObjectVisitor {
874 type Value = OrderedJsonObject;
875
876 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
877 formatter.write_str("an object with ordered JSON members")
878 }
879
880 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
881 where
882 A: MapAccess<'de>,
883 {
884 let mut entries = Vec::new();
885 while let Some((key, value)) = map.next_entry()? {
886 entries.push((key, value));
887 }
888 Ok(OrderedJsonObject(entries))
889 }
890 }
891
892 deserializer.deserialize_map(OrderedObjectVisitor)
893 }
894}
895
896fn read_tagged<'de, D>(
897 deserializer: D,
898 field: &'static str,
899) -> Result<(String, OrderedJsonObject), D::Error>
900where
901 D: Deserializer<'de>,
902{
903 let body = OrderedJsonObject::deserialize(deserializer)?;
904 let mut tag = None;
905 for (key, value) in body.as_entries() {
906 if key != field {
907 continue;
908 }
909 if tag.is_some() {
910 return Err(D::Error::custom(format!(
911 "tagged object has duplicate `{field}` field"
912 )));
913 }
914 let OrderedJsonValue::String(value) = value else {
915 return Err(D::Error::custom(format!(
916 "tagged object has no string `{field}` field"
917 )));
918 };
919 tag = Some(value);
920 }
921 let Some(tag) = tag else {
922 return Err(D::Error::custom(format!(
923 "tagged object has no string `{field}` field"
924 )));
925 };
926 Ok((tag.to_string(), body))
927}
928
929fn read_ordered_tagged(
930 value: OrderedJsonValue,
931 field: &'static str,
932) -> Result<(String, OrderedJsonObject), String> {
933 let OrderedJsonValue::Object(body) = value else {
934 return Err(format!("expected tagged object with `{field}` field"));
935 };
936 let mut tag = None;
937 for (key, value) in body.as_entries() {
938 if key != field {
939 continue;
940 }
941 if tag.is_some() {
942 return Err(format!("tagged object has duplicate `{field}` field"));
943 }
944 let OrderedJsonValue::String(value) = value else {
945 return Err(format!("tagged object has no string `{field}` field"));
946 };
947 tag = Some(value);
948 }
949 let Some(tag) = tag else {
950 return Err(format!("tagged object has no string `{field}` field"));
951 };
952 Ok((tag.to_string(), body))
953}
954
955fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
956 body.as_entries()
957 .iter()
958 .find_map(|(key, value)| (key == field).then_some(value))
959}
960
961fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
962 match ordered_field(body, field) {
963 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
964 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
965 None => Err(format!("tagged object has no `{field}` field")),
966 }
967}
968
969fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
970 let (tag, body) = read_ordered_tagged(value, "method")?;
971 match tag.as_str() {
972 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
973 digest: ordered_string(&body, "digest")?,
974 }),
975 "macos_spawn_inode" => {
976 let device = ordered_field(&body, "device")
977 .and_then(|value| match value {
978 OrderedJsonValue::Number(number) => number.as_u64(),
979 _ => None,
980 })
981 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
982 let inode = ordered_field(&body, "inode")
983 .and_then(|value| match value {
984 OrderedJsonValue::Number(number) => number.as_u64(),
985 _ => None,
986 })
987 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
988 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
989 }
990 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
991 }
992}
993
994impl Serialize for ModuleDeclaredProvenance {
995 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
996 where
997 S: Serializer,
998 {
999 match self {
1000 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1001 build: build.clone(),
1002 }
1003 .serialize(serializer),
1004 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1005 Self::Unknown { body, .. } => body.serialize(serializer),
1006 }
1007 }
1008}
1009
1010impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1011 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1012 where
1013 D: serde::Deserializer<'de>,
1014 {
1015 let (tag, value) = read_tagged(deserializer, "status")?;
1016 match tag.as_str() {
1017 "reported" => match serde_json::from_value(value.into_value())
1018 .map_err(D::Error::custom)?
1019 {
1020 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1021 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1022 },
1023 "unverifiable" => {
1024 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1025 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1026 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1027 }
1028 }
1029 _ => Ok(Self::Unknown { tag, body: value }),
1030 }
1031 }
1032}
1033
1034impl Serialize for RunningImageAgreement {
1035 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1036 where
1037 S: Serializer,
1038 {
1039 match self {
1040 Self::Match { evidence } => RunningImageAgreementWire::Match {
1041 evidence: evidence.clone(),
1042 }
1043 .serialize(serializer),
1044 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1045 running: running.clone(),
1046 disk: disk.clone(),
1047 }
1048 .serialize(serializer),
1049 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1050 reason: reason.clone(),
1051 }
1052 .serialize(serializer),
1053 Self::Unknown { body, .. } => body.serialize(serializer),
1054 }
1055 }
1056}
1057
1058impl<'de> Deserialize<'de> for RunningImageAgreement {
1059 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1060 where
1061 D: serde::Deserializer<'de>,
1062 {
1063 let (tag, value) = read_tagged(deserializer, "status")?;
1064 match tag.as_str() {
1065 "match" => Ok(Self::Match {
1066 evidence: decode_running_image_evidence(
1067 ordered_field(&value, "evidence")
1068 .cloned()
1069 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1070 )
1071 .map_err(D::Error::custom)?,
1072 }),
1073 "mismatch" => Ok(Self::Mismatch {
1074 running: decode_running_image_evidence(
1075 ordered_field(&value, "running")
1076 .cloned()
1077 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1078 )
1079 .map_err(D::Error::custom)?,
1080 disk: decode_running_image_evidence(
1081 ordered_field(&value, "disk")
1082 .cloned()
1083 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1084 )
1085 .map_err(D::Error::custom)?,
1086 }),
1087 "unavailable" => Ok(Self::Unavailable {
1088 reason: serde_json::from_value(
1089 ordered_field(&value, "reason")
1090 .cloned()
1091 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1092 .into_value(),
1093 )
1094 .map_err(D::Error::custom)?,
1095 }),
1096 _ => Ok(Self::Unknown { tag, body: value }),
1097 }
1098 }
1099}
1100
1101impl Serialize for RunningImageEvidence {
1102 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1103 where
1104 S: Serializer,
1105 {
1106 match self {
1107 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1108 digest: digest.clone(),
1109 }
1110 .serialize(serializer),
1111 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1112 device: *device,
1113 inode: *inode,
1114 }
1115 .serialize(serializer),
1116 Self::Unknown { body, .. } => body.serialize(serializer),
1117 }
1118 }
1119}
1120
1121impl<'de> Deserialize<'de> for RunningImageEvidence {
1122 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1123 where
1124 D: serde::Deserializer<'de>,
1125 {
1126 let (tag, value) = read_tagged(deserializer, "method")?;
1127 match tag.as_str() {
1128 "linux_proc_sha256" => {
1129 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1130 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1131 Ok(Self::LinuxProcSha256 { digest })
1132 }
1133 _ => unreachable!(),
1134 }
1135 }
1136 "macos_spawn_inode" => {
1137 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1138 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1139 Ok(Self::MacosSpawnInode { device, inode })
1140 }
1141 _ => unreachable!(),
1142 }
1143 }
1144 _ => Ok(Self::Unknown { tag, body: value }),
1145 }
1146 }
1147}
1148
1149impl Serialize for SupervisorRouteConsumer {
1150 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1151 where
1152 S: Serializer,
1153 {
1154 match self {
1155 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1156 module_id: module_id.clone(),
1157 }
1158 .serialize(serializer),
1159 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1160 connection_id: *connection_id,
1161 }
1162 .serialize(serializer),
1163 Self::Unknown { body, .. } => body.serialize(serializer),
1164 }
1165 }
1166}
1167
1168impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1169 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1170 where
1171 D: serde::Deserializer<'de>,
1172 {
1173 let (tag, value) = read_tagged(deserializer, "kind")?;
1174 match tag.as_str() {
1175 "reserved" => {
1176 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1177 SupervisorRouteConsumerWire::Reserved { module_id } => {
1178 Ok(Self::Reserved { module_id })
1179 }
1180 _ => unreachable!(),
1181 }
1182 }
1183 "direct" => {
1184 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1185 SupervisorRouteConsumerWire::Direct { connection_id } => {
1186 Ok(Self::Direct { connection_id })
1187 }
1188 _ => unreachable!(),
1189 }
1190 }
1191 _ => Ok(Self::Unknown { tag, body: value }),
1192 }
1193 }
1194}
1195
1196impl Serialize for StderrCaptureState {
1197 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1198 where
1199 S: Serializer,
1200 {
1201 match self {
1202 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1203 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1204 reason: reason.clone(),
1205 }
1206 .serialize(serializer),
1207 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1208 reason: reason.clone(),
1209 }
1210 .serialize(serializer),
1211 Self::Unknown { body, .. } => body.serialize(serializer),
1212 }
1213 }
1214}
1215
1216impl<'de> Deserialize<'de> for StderrCaptureState {
1217 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1218 where
1219 D: serde::Deserializer<'de>,
1220 {
1221 let (tag, value) = read_tagged(deserializer, "state")?;
1222 match tag.as_str() {
1223 "captured" => {
1224 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1225 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1226 _ => unreachable!(),
1227 }
1228 }
1229 "incomplete" => match serde_json::from_value(value.into_value())
1230 .map_err(D::Error::custom)?
1231 {
1232 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1233 _ => unreachable!(),
1234 },
1235 "not_captured" => match serde_json::from_value(value.into_value())
1236 .map_err(D::Error::custom)?
1237 {
1238 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1239 _ => unreachable!(),
1240 },
1241 _ => Ok(Self::Unknown { tag, body: value }),
1242 }
1243 }
1244}
1245
1246impl Serialize for StderrTailEntry {
1247 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1248 where
1249 S: Serializer,
1250 {
1251 match self {
1252 Self::Line { text, truncated } => StderrTailEntryWire::Line {
1253 text: text.clone(),
1254 truncated: *truncated,
1255 }
1256 .serialize(serializer),
1257 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1258 Self::Unknown { body, .. } => body.serialize(serializer),
1259 }
1260 }
1261}
1262
1263impl<'de> Deserialize<'de> for StderrTailEntry {
1264 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1265 where
1266 D: serde::Deserializer<'de>,
1267 {
1268 let (tag, value) = read_tagged(deserializer, "kind")?;
1269 match tag.as_str() {
1270 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1271 StderrTailEntryWire::Line { text, truncated } => Ok(Self::Line { text, truncated }),
1272 _ => unreachable!(),
1273 },
1274 "process_start" => {
1275 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1276 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1277 _ => unreachable!(),
1278 }
1279 }
1280 _ => Ok(Self::Unknown { tag, body: value }),
1281 }
1282 }
1283}
1284
1285fn is_zero_u64(value: &u64) -> bool {
1286 *value == 0
1287}
1288
1289fn default_true() -> bool {
1290 true
1291}
1292
1293#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1295pub struct TerminalHistory {
1296 pub daemon_started_at_ms: u64,
1298 pub entries: Vec<TerminalEntry>,
1299 #[serde(default, skip_serializing_if = "is_zero_u64")]
1302 pub dropped: u64,
1303 #[serde(default, skip_serializing_if = "is_zero_u64")]
1306 pub journal_skipped_lines: u64,
1307 #[serde(default, skip_serializing_if = "is_zero_u64")]
1309 pub journal_read_errors: u64,
1310 #[serde(default, skip_serializing_if = "is_zero_u64")]
1312 pub journal_write_failures: u64,
1313}
1314
1315#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1317pub struct TerminalEntry {
1318 #[serde(default, skip_serializing_if = "Option::is_none")]
1321 pub daemon_incarnation: Option<String>,
1322 #[serde(default, skip_serializing_if = "Option::is_none")]
1323 pub exit_code: Option<i32>,
1324 #[serde(default, skip_serializing_if = "Option::is_none")]
1325 pub exit_signal: Option<i32>,
1326 pub at_ms: u64,
1327 pub disposition: TerminalDisposition,
1328 #[serde(default, skip_serializing_if = "Option::is_none")]
1332 pub exit_kind: Option<TerminalExitKind>,
1333 #[serde(default, skip_serializing_if = "Option::is_none")]
1340 pub disposition_detail: Option<String>,
1341}
1342
1343#[derive(Debug, Clone, PartialEq, Eq)]
1348pub enum TerminalExitKind {
1349 Clean,
1350 Crash,
1351 DeliberateSeverance,
1352 Unknown(String),
1353}
1354
1355impl TerminalExitKind {
1356 fn wire_name(&self) -> &str {
1357 match self {
1358 Self::Clean => "clean",
1359 Self::Crash => "crash",
1360 Self::DeliberateSeverance => "deliberate_severance",
1361 Self::Unknown(value) => value,
1362 }
1363 }
1364}
1365
1366impl Serialize for TerminalExitKind {
1367 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1368 where
1369 S: serde::Serializer,
1370 {
1371 serializer.serialize_str(self.wire_name())
1372 }
1373}
1374
1375impl<'de> Deserialize<'de> for TerminalExitKind {
1376 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1377 where
1378 D: serde::Deserializer<'de>,
1379 {
1380 let value = String::deserialize(deserializer)?;
1381 Ok(match value.as_str() {
1382 "clean" => Self::Clean,
1383 "crash" => Self::Crash,
1384 "deliberate_severance" => Self::DeliberateSeverance,
1385 _ => Self::Unknown(value),
1386 })
1387 }
1388}
1389
1390open_string_enum! {
1391 TerminalDisposition {
1393 Stopped => "stopped",
1394 Disabled => "disabled",
1395 Failed => "failed",
1396 Restarting => "restarting",
1397 }
1398}
1399
1400#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1401#[serde(rename_all = "snake_case")]
1402pub enum PollKind {
1403 Status,
1404 Liveness,
1405}
1406
1407#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1408pub struct CatalogEntry {
1409 pub module_id: String,
1410 #[serde(default = "default_true")]
1414 pub ready: bool,
1415 #[serde(default, skip_serializing_if = "Option::is_none")]
1436 pub module_version: Option<String>,
1437 pub roles: Vec<ProviderRole>,
1438 pub control_ops: Vec<String>,
1439 #[serde(default, skip_serializing_if = "Option::is_none")]
1444 pub capabilities: Option<CapabilityDeclarations>,
1445 #[serde(default, skip_serializing_if = "Option::is_none")]
1448 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1449}
1450
1451#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1452pub struct CapabilityRequirementStatus {
1453 pub consumer: String,
1454 pub capability: String,
1455 pub need: String,
1456 pub verdict: String,
1457 pub episode_seq: u64,
1458 pub config_satisfiable: bool,
1459 pub runtime_available: bool,
1460 pub detail: String,
1461}
1462
1463#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1464pub struct SupervisorRescanResult {
1465 pub added: Vec<String>,
1466 pub removed: Vec<String>,
1467 pub changed_pending_reload: Vec<String>,
1468 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1481 pub enabled_changes: Vec<String>,
1482 pub unchanged: u32,
1483 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1491 pub preview: bool,
1492 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1510 pub restart_required: Vec<String>,
1511 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1515 pub capability_warnings: Vec<String>,
1516}
1517
1518#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1534#[serde(rename_all = "snake_case")]
1535pub enum ModuleProtocol {
1536 #[default]
1540 Subc,
1541 None,
1543}
1544
1545#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1546pub struct SupervisorEntry {
1547 pub module_id: String,
1548 pub state: String,
1549 pub enabled: bool,
1550 pub live: bool,
1560 #[serde(default)]
1564 pub protocol: ModuleProtocol,
1565 pub health: SupervisorHealthStatus,
1566 #[serde(default)]
1572 pub last_probe_ms: Option<u64>,
1573 #[serde(default, skip_serializing_if = "Option::is_none")]
1577 pub last_exit_code: Option<i32>,
1578 #[serde(default, skip_serializing_if = "Option::is_none")]
1582 pub last_exit_signal: Option<i32>,
1583 #[serde(default, skip_serializing_if = "Option::is_none")]
1587 pub last_exit_ms: Option<u64>,
1588 #[serde(default, skip_serializing_if = "Option::is_none")]
1591 pub last_exit_kind: Option<TerminalExitKind>,
1592 #[serde(default, skip_serializing_if = "Option::is_none")]
1609 pub restart_count: Option<u32>,
1610 #[serde(default, skip_serializing_if = "Option::is_none")]
1613 pub max_restarts: Option<u32>,
1614 #[serde(default, skip_serializing_if = "Option::is_none")]
1617 pub lifetime_restarts: Option<u32>,
1618 #[serde(default, skip_serializing_if = "Option::is_none")]
1628 pub restart_window_secs: Option<u64>,
1629 #[serde(default, skip_serializing_if = "Option::is_none")]
1633 pub drain_timeout_ms: Option<u64>,
1634 #[serde(default, skip_serializing_if = "Option::is_none")]
1637 pub restart_backoff_ms: Option<u64>,
1638 #[serde(default, skip_serializing_if = "Option::is_none")]
1641 pub restart_max_backoff_ms: Option<u64>,
1642}
1643
1644#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1645#[serde(rename_all = "snake_case")]
1646pub enum SupervisorHealthStatus {
1647 Ok,
1648 Degraded,
1649 Failing,
1650 Unresponsive,
1651 Unknown,
1652}
1653
1654#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1655pub struct SupervisorHealthEntry {
1656 pub module_id: String,
1657 pub status: SupervisorHealthStatus,
1658 #[serde(default, skip_serializing_if = "Option::is_none")]
1664 pub detail: Option<String>,
1665 #[serde(default, skip_serializing_if = "Option::is_none")]
1670 pub metrics: Option<serde_json::Value>,
1671 pub consecutive_failures: u32,
1672 #[serde(default)]
1675 pub late_answer_count: u64,
1676 #[serde(default, skip_serializing_if = "Option::is_none")]
1678 pub last_late_answer_latency_ms: Option<u64>,
1679 #[serde(default)]
1684 pub last_action: Option<String>,
1685 #[serde(default)]
1688 pub last_action_ms: Option<u64>,
1689 #[serde(default, skip_serializing_if = "Option::is_none")]
1702 pub last_probe_ms: Option<u64>,
1703}
1704
1705#[cfg(test)]
1706mod tests {
1707 use super::*;
1708 use subc_protocol::{BindIdentity, RouteTarget};
1709
1710 #[test]
1711 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
1712 let entry = TerminalEntry {
1713 daemon_incarnation: Some("daemon-before-restart".into()),
1714 exit_code: Some(1),
1715 exit_signal: None,
1716 at_ms: 1_700_000_000_123,
1717 disposition: TerminalDisposition::Restarting,
1718 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
1719 disposition_detail: None,
1720 };
1721 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
1722 assert_eq!(
1723 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
1724 ["exit_kind"],
1725 "deliberate_severance"
1726 );
1727
1728 #[derive(serde::Deserialize)]
1729 struct LegacyTerminalEntry {
1730 exit_code: Option<i32>,
1731 exit_signal: Option<i32>,
1732 at_ms: u64,
1733 disposition: TerminalDisposition,
1734 }
1735
1736 let decoded: LegacyTerminalEntry =
1737 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
1738 assert_eq!(decoded.exit_code, Some(1));
1739 assert_eq!(decoded.exit_signal, None);
1740 assert_eq!(decoded.at_ms, 1_700_000_000_123);
1741 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
1742
1743 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
1744 let future: TerminalEntry =
1745 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
1746 assert_eq!(
1747 future.exit_kind,
1748 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
1749 );
1750 }
1751
1752 #[test]
1753 fn terminal_incarnation_is_optional_for_older_daemons() {
1754 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
1755 "at_ms": 123,
1756 "disposition": "stopped"
1757 }))
1758 .unwrap();
1759 let encoded = serde_json::to_value(&entry).unwrap();
1760 assert_eq!(
1761 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
1762 (None, None)
1763 );
1764 }
1765
1766 #[test]
1767 fn route_poll_uses_kind_field() {
1768 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
1769 route_channel: 7,
1770 route_epoch: 11,
1771 kind: PollKind::Status,
1772 })
1773 .unwrap();
1774
1775 assert_eq!(body["op"], "route.poll");
1776 assert_eq!(body["route_epoch"], 11);
1777 assert_eq!(body["kind"], "status");
1778 assert!(body.get("op").is_some());
1779 }
1780
1781 #[test]
1782 fn route_open_is_internally_tagged() {
1783 let request = ClientControlRequest::RouteOpen {
1784 target: RouteTarget::ToolProvider {
1785 module_id: "aft".to_string(),
1786 },
1787 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
1788 consumer_identity: None,
1789 consumer_capabilities: None,
1790 admission_facts: None,
1791 };
1792
1793 let body = serde_json::to_value(request).unwrap();
1794 assert_eq!(body["op"], "route.open");
1795 assert_eq!(body["target"]["kind"], "tool_provider");
1796 assert!(body.get("consumer_identity").is_none());
1797 assert!(body.get("consumer_capabilities").is_none());
1798 }
1799
1800 #[test]
1801 fn route_open_without_optional_fields_still_decodes() {
1802 let body = serde_json::json!({
1803 "op": "route.open",
1804 "target": { "kind": "tool_provider", "module_id": "aft" },
1805 "identity": {
1806 "project_root": "/tmp/project",
1807 "harness": "opencode",
1808 "session": "session-1"
1809 }
1810 });
1811
1812 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
1813 let ClientControlRequest::RouteOpen {
1814 consumer_identity,
1815 consumer_capabilities,
1816 admission_facts,
1817 ..
1818 } = decoded
1819 else {
1820 panic!("decoded wrong request variant");
1821 };
1822 assert_eq!(consumer_identity, None);
1823 assert_eq!(consumer_capabilities, None);
1824 assert_eq!(admission_facts, None);
1825 }
1826
1827 #[test]
1828 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
1829 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
1830 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
1831 match decoded {
1832 ClientControlPush::RouteClosed {
1833 excluded_subscriptions,
1834 terminal,
1835 ..
1836 } => {
1837 assert_eq!(excluded_subscriptions, 0);
1838 assert_eq!(terminal, None);
1839 }
1840 other => panic!("unexpected push: {other:?}"),
1841 }
1842 assert!(!serde_json::to_string(&decoded)
1843 .unwrap()
1844 .contains("terminal"));
1845 }
1846
1847 #[test]
1848 fn old_route_closed_decoder_ignores_new_terminal_field() {
1849 #[derive(serde::Deserialize)]
1850 #[serde(tag = "op")]
1851 enum LegacyClientControlPush {
1852 #[serde(rename = "route.closed")]
1853 RouteClosed {
1854 module_id: String,
1855 reason: RouteCloseReason,
1856 drained: bool,
1857 abandoned: u32,
1858 },
1859 }
1860
1861 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
1862 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
1863 match decoded {
1864 LegacyClientControlPush::RouteClosed {
1865 module_id,
1866 reason,
1867 drained,
1868 abandoned,
1869 } => {
1870 assert_eq!(module_id, "aft-tools");
1871 assert_eq!(reason, RouteCloseReason::Crash);
1872 assert!(!drained);
1873 assert_eq!(abandoned, 0);
1874 }
1875 }
1876 }
1877
1878 #[test]
1879 fn supervisor_routes_is_a_control_plane_request() {
1880 let body = serde_json::json!({
1881 "op": "supervisor.routes",
1882 "module_id": "aft"
1883 });
1884
1885 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
1886 assert_eq!(serde_json::to_value(request).unwrap(), body);
1887 }
1888
1889 #[test]
1890 fn diagnostic_string_enums_retain_unknown_wire_values() {
1891 let reason: RunningImageUnavailableReason =
1892 serde_json::from_str("\"future_reason\"").unwrap();
1893 let disposition: TerminalDisposition =
1894 serde_json::from_str("\"future_disposition\"").unwrap();
1895
1896 assert_eq!(
1897 reason,
1898 RunningImageUnavailableReason::Unknown("future_reason".to_string())
1899 );
1900 assert_eq!(
1901 disposition,
1902 TerminalDisposition::Unknown("future_disposition".to_string())
1903 );
1904 }
1905
1906 #[test]
1907 fn diagnostic_string_enums_preserve_existing_wire_names() {
1908 let names = [
1909 (RunningImageUnavailableReason::NotRunning, "not_running"),
1910 (
1911 RunningImageUnavailableReason::UnsupportedPlatform,
1912 "unsupported_platform",
1913 ),
1914 (
1915 RunningImageUnavailableReason::RunningExecutableUnreadable,
1916 "running_executable_unreadable",
1917 ),
1918 (
1919 RunningImageUnavailableReason::SpawnedPathUnreadable,
1920 "spawned_path_unreadable",
1921 ),
1922 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
1923 (
1924 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
1925 "process_identity_unconfirmed",
1926 ),
1927 ];
1928 for (value, expected) in names {
1929 let wire = serde_json::to_string(&value).unwrap();
1930 assert_eq!(wire, format!("\"{expected}\""));
1931 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
1932 assert_eq!(decoded, value);
1933 }
1934
1935 for (value, expected) in [
1936 (TerminalDisposition::Stopped, "stopped"),
1937 (TerminalDisposition::Disabled, "disabled"),
1938 (TerminalDisposition::Failed, "failed"),
1939 (TerminalDisposition::Restarting, "restarting"),
1940 ] {
1941 let wire = serde_json::to_string(&value).unwrap();
1942 assert_eq!(wire, format!("\"{expected}\""));
1943 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
1944 assert_eq!(decoded, value);
1945 }
1946 }
1947
1948 #[test]
1949 fn diagnostic_string_enums_reject_non_string_bodies() {
1950 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
1951 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
1952 }
1953
1954 #[test]
1955 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
1956 let body = serde_json::json!({
1957 "op": "supervisor.provenance",
1958 "daemon": {
1959 "daemon_build": {},
1960 "daemon_observed": {
1961 "running_image": {
1962 "status": "unavailable",
1963 "reason": "not_running"
1964 }
1965 }
1966 },
1967 "modules": [
1968 {
1969 "module_id": "future",
1970 "module_declared": { "status": "unverifiable" },
1971 "daemon_observed": {
1972 "running_image": {
1973 "status": "unavailable",
1974 "reason": "future_reason"
1975 }
1976 }
1977 },
1978 {
1979 "module_id": "healthy-a",
1980 "module_declared": { "status": "unverifiable" },
1981 "daemon_observed": {
1982 "running_image": {
1983 "status": "match",
1984 "evidence": {
1985 "method": "linux_proc_sha256",
1986 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1987 }
1988 }
1989 }
1990 },
1991 {
1992 "module_id": "healthy-b",
1993 "module_declared": { "status": "unverifiable" },
1994 "daemon_observed": {
1995 "running_image": {
1996 "status": "unavailable",
1997 "reason": "unsupported_platform"
1998 }
1999 }
2000 }
2001 ]
2002 });
2003
2004 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2005 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2006 panic!("decoded wrong response variant");
2007 };
2008 assert_eq!(modules.len(), 3);
2009 assert_eq!(modules[0].module_id, "future");
2010 assert_eq!(
2011 modules[0].daemon_observed.running_image,
2012 RunningImageAgreement::Unavailable {
2013 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2014 }
2015 );
2016 assert_eq!(modules[1].module_id, "healthy-a");
2017 assert_eq!(modules[2].module_id, "healthy-b");
2018 }
2019
2020 #[test]
2021 fn tagged_unknown_values_retain_tag_and_body() {
2022 macro_rules! assert_unknown_round_trip {
2023 ($ty:ident, $field:literal, $value:expr) => {
2024 let value = $value;
2025 let wire = serde_json::to_string(&value).unwrap();
2026 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2027 match decoded {
2028 $ty::Unknown { tag, body } => {
2029 assert_eq!(tag, value[$field].as_str().unwrap());
2030 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2031 }
2032 _ => panic!("decoded known variant"),
2033 }
2034 };
2035 }
2036
2037 assert_unknown_round_trip!(
2038 ModuleDeclaredProvenance,
2039 "status",
2040 serde_json::json!({"status": "future", "build": {"version": 7}})
2041 );
2042 assert_unknown_round_trip!(
2043 RunningImageAgreement,
2044 "status",
2045 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2046 );
2047 assert_unknown_round_trip!(
2048 RunningImageEvidence,
2049 "method",
2050 serde_json::json!({"method": "future", "digest": "abc"})
2051 );
2052 assert_unknown_round_trip!(
2053 SupervisorRouteConsumer,
2054 "kind",
2055 serde_json::json!({"kind": "future", "module_id": "m"})
2056 );
2057 assert_unknown_round_trip!(
2058 StderrCaptureState,
2059 "state",
2060 serde_json::json!({"state": "future", "reason": "because"})
2061 );
2062 assert_unknown_round_trip!(
2063 StderrTailEntry,
2064 "kind",
2065 serde_json::json!({"kind": "future", "text": "line"})
2066 );
2067 }
2068
2069 #[test]
2070 fn tagged_unknown_values_round_trip_the_original_json() {
2071 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2072 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2073 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2074 }
2075
2076 #[test]
2077 fn tagged_unknown_values_round_trip_trailing_tag() {
2078 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2079 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2080 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2081
2082 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2083 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2084 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2085 }
2086
2087 #[test]
2088 fn tagged_unknown_values_round_trip_middle_tag() {
2089 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2090 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2091 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2092
2093 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2094 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2095 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2096 }
2097
2098 #[test]
2099 fn tagged_unknown_values_round_trip_deep_payload() {
2100 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2101 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2102 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2103
2104 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2105 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2106 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2107 }
2108
2109 #[test]
2110 fn tagged_unknown_values_reject_non_object_bodies() {
2111 for wire in ["42", r#""future""#, "[]"] {
2112 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2113 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2114 }
2115 }
2116
2117 #[test]
2118 fn duplicate_discriminators_reject_without_panicking() {
2119 assert_eq!(
2120 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2121 .unwrap(),
2122 ModuleDeclaredProvenance::Unverifiable
2123 );
2124 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2125 .unwrap()
2126 {
2127 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2128 _ => panic!("future discriminator decoded as a known variant"),
2129 }
2130
2131 let wires = [
2132 r#"{"status":"reported","status":"unverifiable"}"#,
2133 r#"{"status":"unverifiable","status":"reported"}"#,
2134 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2135 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2136 ];
2137
2138 for wire in wires {
2139 let result =
2140 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2141 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2142 assert!(
2143 result.unwrap().is_err(),
2144 "duplicate discriminator decoded: {wire}"
2145 );
2146 }
2147
2148 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2149 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2150 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2151 assert!(
2152 result.unwrap().is_err(),
2153 "duplicate discriminator decoded: {wire}"
2154 );
2155 }
2156
2157 #[test]
2158 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2159 let known_wire =
2160 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2161 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2162 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2163
2164 for wire in [
2165 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2166 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2167 ] {
2168 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2169 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2170 }
2171
2172 for wire in [
2173 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2174 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2175 ] {
2176 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2177 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2178 }
2179
2180 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2181 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2182 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2183
2184 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2185 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2186 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2187 }
2188
2189 #[test]
2190 fn tagged_unknown_member_does_not_discard_known_siblings() {
2191 let body = serde_json::json!({
2192 "modules": [{
2193 "module_id": "target",
2194 "routes": [
2195 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2196 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2197 ]
2198 }]
2199 });
2200 let decoded: ClientControlResponse = serde_json::from_value(
2201 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2202 )
2203 .unwrap();
2204 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2205 panic!("decoded wrong response variant");
2206 };
2207 assert_eq!(modules[0].routes.len(), 2);
2208 assert_eq!(
2209 modules[0].routes[1].consumer,
2210 SupervisorRouteConsumer::Direct { connection_id: 7 }
2211 );
2212 }
2213}