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_SWAP: &str = "supervisor.swap";
105 pub const SUPERVISOR_RELOAD: &str = "supervisor.reload";
106 pub const SUPERVISOR_RESCAN: &str = "supervisor.rescan";
107 pub const SUPERVISOR_RELEASE_RESERVED: &str = "supervisor.release_reserved";
108 pub const SUPERVISOR_SET_ENABLED: &str = "supervisor.set_enabled";
109 pub const SUPERVISOR_HEALTH_PROBE: &str = "supervisor.health_probe";
110 pub const SUPERVISOR_HEALTH: &str = "supervisor.health";
111 pub const SUPERVISOR_STDERR_TAIL: &str = "supervisor.stderr_tail";
112 pub const SUPERVISOR_TERMINALS: &str = "supervisor.terminals";
113 pub const SUPERVISOR_ROUTES: &str = "supervisor.routes";
114 pub const SUPERVISOR_PROVENANCE: &str = "supervisor.provenance";
115 pub const SUPERVISOR_SPAWN_SNAPSHOT: &str = "supervisor.spawn_snapshot";
116 pub const SUPERVISOR_SPAWN_SUBSCRIBE: &str = "supervisor.spawn_subscribe";
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121#[serde(tag = "op")]
122#[allow(clippy::large_enum_variant)]
125pub enum ClientControlRequest {
126 #[serde(rename = "server.describe")]
127 ServerDescribe {},
128 #[serde(rename = "catalog.list")]
129 CatalogList {
130 #[serde(default)]
135 module_id: Option<String>,
136 },
137 #[serde(rename = "route.open")]
138 RouteOpen {
139 target: RouteTarget,
140 identity: BindIdentity,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
150 consumer_identity: Option<ConsumerIdentity>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
159 consumer_capabilities: Option<Vec<String>>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 admission_facts: Option<serde_json::Value>,
163 },
164 #[serde(rename = "route.poll")]
165 RoutePoll {
166 route_channel: u16,
167 route_epoch: u32,
168 kind: PollKind,
169 },
170 #[serde(rename = "supervisor.list")]
171 SupervisorList {},
172 #[serde(rename = "supervisor.spawn_snapshot")]
174 SupervisorSpawnSnapshot {},
175 #[serde(rename = "supervisor.spawn_subscribe")]
181 SupervisorSpawnSubscribe {
182 #[serde(default, skip_serializing_if = "Option::is_none")]
183 since: Option<SpawnCursor>,
184 },
185 #[serde(rename = "supervisor.restart")]
186 SupervisorRestart {
187 module_id: String,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
196 drain_timeout_ms: Option<u64>,
197 },
198 #[serde(rename = "supervisor.swap")]
213 SupervisorSwap {
214 module_id: String,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
218 ready_timeout_ms: Option<u64>,
219 },
220 #[serde(rename = "supervisor.reload")]
221 SupervisorReload { module_id: String },
222 #[serde(rename = "supervisor.rescan")]
223 SupervisorRescan {
224 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
244 preview: bool,
245 },
246 #[serde(rename = "supervisor.release_reserved")]
250 SupervisorReleaseReserved { module_id: String },
251 #[serde(rename = "supervisor.set_enabled")]
252 SupervisorSetEnabled { module_id: String, enabled: bool },
253 #[serde(rename = "supervisor.health_probe")]
254 SupervisorHealthProbe { module_id: String },
255 #[serde(rename = "supervisor.health")]
256 SupervisorHealth {},
257 #[serde(rename = "supervisor.routes")]
270 SupervisorRoutes {
271 #[serde(default, skip_serializing_if = "Option::is_none")]
272 module_id: Option<String>,
273 },
274 #[serde(rename = "supervisor.provenance")]
277 SupervisorProvenance {
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 module_id: Option<String>,
280 },
281 #[serde(rename = "supervisor.stderr_tail")]
289 SupervisorStderrTail {
290 module_id: String,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 max_lines: Option<u32>,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 max_bytes: Option<u32>,
295 },
296 #[serde(rename = "supervisor.terminals")]
309 SupervisorTerminals { module_id: String },
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
314#[serde(tag = "op")]
315pub enum ClientControlResponse {
316 #[serde(rename = "server.describe")]
317 ServerDescribe {
318 protocol_ver: u8,
319 subc_ops: Vec<String>,
320 capabilities: Vec<String>,
321 connected_clients: u64,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
323 counters: Option<serde_json::Value>,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
331 build_git_sha: Option<String>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
338 build_lock_digest: Option<String>,
339 #[serde(default, skip_serializing_if = "Vec::is_empty")]
343 capability_requirements: Vec<CapabilityRequirementStatus>,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
349 machine_id: Option<String>,
350 },
351 #[serde(rename = "catalog.list")]
352 CatalogList {
353 generation: u64,
354 modules: Vec<CatalogEntry>,
355 subc_ops: Vec<String>,
356 },
357 #[serde(rename = "route.open")]
358 RouteOpen {
359 route_channel: u16,
360 route_epoch: u32,
361 },
362 #[serde(rename = "route.poll")]
363 RoutePoll {
364 route_channel: u16,
365 route_epoch: u32,
366 status: Option<String>,
367 live: Option<bool>,
368 },
369 #[serde(rename = "supervisor.list")]
370 SupervisorList {
371 generation: u64,
372 modules: Vec<SupervisorEntry>,
373 },
374 #[serde(rename = "supervisor.spawn_snapshot")]
375 SupervisorSpawnSnapshot {
376 #[serde(flatten)]
377 snapshot: SpawnSnapshot,
378 },
379 #[serde(rename = "supervisor.ack")]
380 SupervisorAck { module_id: String, applied: bool },
381 #[serde(rename = "supervisor.rescan")]
382 SupervisorRescan {
383 #[serde(flatten)]
384 result: SupervisorRescanResult,
385 },
386 #[serde(rename = "supervisor.health_probe")]
387 SupervisorHealthProbe {
388 module_id: String,
389 status: HealthStatus,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
391 detail: Option<String>,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
393 metrics: Option<serde_json::Value>,
394 },
395 #[serde(rename = "supervisor.health")]
396 SupervisorHealth {
397 generation: u64,
398 modules: Vec<SupervisorHealthEntry>,
399 },
400 #[serde(rename = "supervisor.routes")]
401 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
402 #[serde(rename = "supervisor.provenance")]
403 SupervisorProvenance {
404 daemon: SupervisorDaemonProvenance,
405 modules: Vec<SupervisorModuleProvenance>,
406 },
407 #[serde(rename = "supervisor.stderr_tail")]
408 SupervisorStderrTail {
409 module_id: String,
410 #[serde(flatten)]
411 tail: StderrTail,
412 },
413 #[serde(rename = "supervisor.terminals")]
414 SupervisorTerminals {
415 module_id: String,
416 #[serde(flatten)]
417 terminals: TerminalHistory,
418 },
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
426#[serde(tag = "op")]
427pub enum ClientControlPush {
428 #[serde(rename = "route.closing")]
429 RouteClosing {
430 module_id: String,
431 reason: RouteCloseReason,
432 },
433 #[serde(rename = "route.closed")]
434 RouteClosed {
435 module_id: String,
436 reason: RouteCloseReason,
437 drained: bool,
439 abandoned: u32,
442 #[serde(default)]
444 excluded_subscriptions: u32,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
451 terminal: Option<bool>,
452 },
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
457pub struct SpawnCursor {
458 pub daemon_incarnation: String,
459 pub seq: u64,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
464pub struct LiveSpawn {
465 pub module_id: String,
466 pub spawn_generation: u64,
467 pub pid: u32,
468 pub spawned_at_ms: u64,
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
473pub struct SpawnSnapshot {
474 pub cursor: SpawnCursor,
475 pub ring_bound: u64,
477 pub live: Vec<LiveSpawn>,
478}
479
480#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
482#[serde(rename_all = "snake_case")]
483pub enum SpawnEventKind {
484 Spawned,
485 Exited,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
494pub struct SpawnEvent {
495 pub cursor: SpawnCursor,
496 pub kind: SpawnEventKind,
497 pub module_id: String,
498 pub spawn_generation: u64,
499 pub pid: u32,
500 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub exit_code: Option<i32>,
502 #[serde(default, skip_serializing_if = "Option::is_none")]
503 pub exit_signal: Option<i32>,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
508pub struct StderrTail {
509 pub capture: StderrCaptureState,
510 pub entries: Vec<StderrTailEntry>,
511 #[serde(default, skip_serializing_if = "is_zero_u64")]
520 pub dropped_lines: u64,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
525pub struct SupervisorRouteModule {
526 pub module_id: String,
527 pub routes: Vec<SupervisorRoute>,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
532pub struct SupervisorRoute {
533 pub consumer: SupervisorRouteConsumer,
534 pub age_ms: u64,
536 pub draining: bool,
539 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub drain_reason: Option<RouteCloseReason>,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
550pub struct SupervisorModuleProvenance {
551 pub module_id: String,
552 pub module_declared: ModuleDeclaredProvenance,
553 pub daemon_observed: SupervisorObservedProcess,
554}
555
556#[derive(Debug, Clone, PartialEq)]
558pub enum ModuleDeclaredProvenance {
559 Reported {
560 build: ManifestProvenance,
561 },
562 Unverifiable,
563 Unknown {
566 tag: String,
567 body: OrderedJsonObject,
568 },
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
576pub struct SupervisorObservedProcess {
577 #[serde(default, skip_serializing_if = "Option::is_none")]
578 pub pid: Option<u32>,
579 #[serde(default, skip_serializing_if = "Option::is_none")]
580 pub spawned_at_ms: Option<u64>,
581 #[serde(default, skip_serializing_if = "Option::is_none")]
582 pub spawned_from: Option<PathBuf>,
583 pub running_image: RunningImageAgreement,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
588pub struct SupervisorDaemonProvenance {
589 pub daemon_build: DaemonBuildProvenance,
590 pub daemon_observed: DaemonObservedProcess,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
595pub struct DaemonBuildProvenance {
596 #[serde(default, skip_serializing_if = "Option::is_none")]
597 pub build_git_sha: Option<String>,
598 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub build_lock_digest: Option<String>,
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
604pub struct DaemonObservedProcess {
605 #[serde(default, skip_serializing_if = "Option::is_none")]
606 pub pid: Option<u32>,
607 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub started_at_ms: Option<u64>,
613 pub running_image: RunningImageAgreement,
614}
615
616#[derive(Debug, Clone, PartialEq)]
618pub enum RunningImageAgreement {
619 Match {
620 evidence: RunningImageEvidence,
621 },
622 Mismatch {
623 running: RunningImageEvidence,
624 disk: RunningImageEvidence,
625 },
626 Unavailable {
627 reason: RunningImageUnavailableReason,
628 },
629 Unknown {
632 tag: String,
633 body: OrderedJsonObject,
634 },
635}
636
637#[derive(Debug, Clone, PartialEq)]
639pub enum RunningImageEvidence {
640 LinuxProcSha256 {
641 digest: String,
642 },
643 MacosSpawnInode {
644 device: u64,
645 inode: u64,
646 },
647 Unknown {
650 tag: String,
651 body: OrderedJsonObject,
652 },
653}
654
655open_string_enum! {
656 RunningImageUnavailableReason {
658 NotRunning => "not_running",
659 UnsupportedPlatform => "unsupported_platform",
660 RunningExecutableUnreadable => "running_executable_unreadable",
661 SpawnedPathUnreadable => "spawned_path_unreadable",
662 HashFailed => "hash_failed",
663 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
664 }
665}
666
667#[derive(Debug, Clone, PartialEq)]
673pub enum SupervisorRouteConsumer {
674 Reserved {
675 module_id: String,
676 },
677 Direct {
678 connection_id: u64,
679 },
680 Unknown {
683 tag: String,
684 body: OrderedJsonObject,
685 },
686}
687
688#[derive(Debug, Clone, PartialEq)]
695pub enum StderrCaptureState {
696 Captured,
699 Incomplete { reason: String },
701 NotCaptured { reason: String },
703 Unknown {
706 tag: String,
707 body: OrderedJsonObject,
708 },
709}
710
711#[derive(Debug, Clone, PartialEq)]
712pub enum StderrTailEntry {
713 Line {
714 text: String,
715 truncated: bool,
720 },
721 ProcessStart,
726 Unknown {
729 tag: String,
730 body: OrderedJsonObject,
731 },
732}
733
734#[derive(Debug, Serialize, Deserialize)]
735#[serde(tag = "status", rename_all = "snake_case")]
736enum ModuleDeclaredProvenanceWire {
737 Reported { build: ManifestProvenance },
738 Unverifiable,
739}
740
741#[derive(Debug, Serialize, Deserialize)]
742#[serde(tag = "status", rename_all = "snake_case")]
743enum RunningImageAgreementWire {
744 Match {
745 evidence: RunningImageEvidence,
746 },
747 Mismatch {
748 running: RunningImageEvidence,
749 disk: RunningImageEvidence,
750 },
751 Unavailable {
752 reason: RunningImageUnavailableReason,
753 },
754}
755
756#[derive(Debug, Serialize, Deserialize)]
757#[serde(tag = "method", rename_all = "snake_case")]
758enum RunningImageEvidenceWire {
759 LinuxProcSha256 { digest: String },
760 MacosSpawnInode { device: u64, inode: u64 },
761}
762
763#[derive(Debug, Serialize, Deserialize)]
764#[serde(tag = "kind", rename_all = "snake_case")]
765enum SupervisorRouteConsumerWire {
766 Reserved { module_id: String },
767 Direct { connection_id: u64 },
768}
769
770#[derive(Debug, Serialize, Deserialize)]
771#[serde(tag = "state", rename_all = "snake_case")]
772enum StderrCaptureStateWire {
773 Captured,
774 Incomplete { reason: String },
775 NotCaptured { reason: String },
776}
777
778#[derive(Debug, Serialize, Deserialize)]
779#[serde(tag = "kind", rename_all = "snake_case")]
780enum StderrTailEntryWire {
781 Line {
782 text: String,
783 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
784 truncated: bool,
785 },
786 ProcessStart,
787}
788
789#[derive(Debug, Clone, PartialEq)]
791pub enum OrderedJsonValue {
792 Null,
793 Bool(bool),
794 Number(serde_json::Number),
795 String(String),
796 Array(Vec<Self>),
797 Object(OrderedJsonObject),
798}
799
800#[derive(Debug, Clone, PartialEq)]
802pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
803
804impl OrderedJsonObject {
805 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
807 &self.0
808 }
809
810 fn into_value(self) -> serde_json::Value {
811 serde_json::Value::Object(
812 self.0
813 .into_iter()
814 .map(|(key, value)| (key, value.into_value()))
815 .collect(),
816 )
817 }
818}
819
820impl OrderedJsonValue {
821 fn into_value(self) -> serde_json::Value {
822 match self {
823 Self::Null => serde_json::Value::Null,
824 Self::Bool(value) => serde_json::Value::Bool(value),
825 Self::Number(value) => serde_json::Value::Number(value),
826 Self::String(value) => serde_json::Value::String(value),
827 Self::Array(values) => {
828 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
829 }
830 Self::Object(value) => value.into_value(),
831 }
832 }
833}
834
835impl Serialize for OrderedJsonValue {
836 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
837 where
838 S: Serializer,
839 {
840 match self {
841 Self::Null => serializer.serialize_unit(),
842 Self::Bool(value) => serializer.serialize_bool(*value),
843 Self::Number(value) => value.serialize(serializer),
844 Self::String(value) => serializer.serialize_str(value),
845 Self::Array(values) => values.serialize(serializer),
846 Self::Object(value) => value.serialize(serializer),
847 }
848 }
849}
850
851impl<'de> Deserialize<'de> for OrderedJsonValue {
852 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
853 where
854 D: Deserializer<'de>,
855 {
856 struct OrderedValueVisitor;
857
858 impl<'de> Visitor<'de> for OrderedValueVisitor {
859 type Value = OrderedJsonValue;
860
861 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
862 formatter.write_str("a JSON value with ordered object members")
863 }
864
865 fn visit_unit<E>(self) -> Result<Self::Value, E>
866 where
867 E: serde::de::Error,
868 {
869 Ok(OrderedJsonValue::Null)
870 }
871
872 fn visit_none<E>(self) -> Result<Self::Value, E>
873 where
874 E: serde::de::Error,
875 {
876 Ok(OrderedJsonValue::Null)
877 }
878
879 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
880 where
881 D: Deserializer<'de>,
882 {
883 OrderedJsonValue::deserialize(deserializer)
884 }
885
886 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
887 where
888 E: serde::de::Error,
889 {
890 Ok(OrderedJsonValue::Bool(value))
891 }
892
893 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
894 where
895 E: serde::de::Error,
896 {
897 Ok(OrderedJsonValue::Number(value.into()))
898 }
899
900 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
901 where
902 E: serde::de::Error,
903 {
904 Ok(OrderedJsonValue::Number(value.into()))
905 }
906
907 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
908 where
909 E: serde::de::Error,
910 {
911 serde_json::Number::from_f64(value)
912 .map(OrderedJsonValue::Number)
913 .ok_or_else(|| E::custom("non-finite JSON number"))
914 }
915
916 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
917 where
918 E: serde::de::Error,
919 {
920 Ok(OrderedJsonValue::String(value.to_owned()))
921 }
922
923 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
924 where
925 E: serde::de::Error,
926 {
927 Ok(OrderedJsonValue::String(value))
928 }
929
930 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
931 where
932 A: SeqAccess<'de>,
933 {
934 let mut values = Vec::new();
935 while let Some(value) = sequence.next_element()? {
936 values.push(value);
937 }
938 Ok(OrderedJsonValue::Array(values))
939 }
940
941 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
942 where
943 A: MapAccess<'de>,
944 {
945 let mut entries = Vec::new();
946 while let Some((key, value)) = map.next_entry()? {
947 entries.push((key, value));
948 }
949 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
950 }
951 }
952
953 deserializer.deserialize_any(OrderedValueVisitor)
954 }
955}
956
957impl Serialize for OrderedJsonObject {
958 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
959 where
960 S: Serializer,
961 {
962 let mut map = serializer.serialize_map(Some(self.0.len()))?;
963 for (key, value) in &self.0 {
964 map.serialize_entry(key, value)?;
965 }
966 map.end()
967 }
968}
969
970impl<'de> Deserialize<'de> for OrderedJsonObject {
971 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
972 where
973 D: Deserializer<'de>,
974 {
975 struct OrderedObjectVisitor;
976
977 impl<'de> Visitor<'de> for OrderedObjectVisitor {
978 type Value = OrderedJsonObject;
979
980 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
981 formatter.write_str("an object with ordered JSON members")
982 }
983
984 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
985 where
986 A: MapAccess<'de>,
987 {
988 let mut entries = Vec::new();
989 while let Some((key, value)) = map.next_entry()? {
990 entries.push((key, value));
991 }
992 Ok(OrderedJsonObject(entries))
993 }
994 }
995
996 deserializer.deserialize_map(OrderedObjectVisitor)
997 }
998}
999
1000fn read_tagged<'de, D>(
1001 deserializer: D,
1002 field: &'static str,
1003) -> Result<(String, OrderedJsonObject), D::Error>
1004where
1005 D: Deserializer<'de>,
1006{
1007 let body = OrderedJsonObject::deserialize(deserializer)?;
1008 let mut tag = None;
1009 for (key, value) in body.as_entries() {
1010 if key != field {
1011 continue;
1012 }
1013 if tag.is_some() {
1014 return Err(D::Error::custom(format!(
1015 "tagged object has duplicate `{field}` field"
1016 )));
1017 }
1018 let OrderedJsonValue::String(value) = value else {
1019 return Err(D::Error::custom(format!(
1020 "tagged object has no string `{field}` field"
1021 )));
1022 };
1023 tag = Some(value);
1024 }
1025 let Some(tag) = tag else {
1026 return Err(D::Error::custom(format!(
1027 "tagged object has no string `{field}` field"
1028 )));
1029 };
1030 Ok((tag.to_string(), body))
1031}
1032
1033fn read_ordered_tagged(
1034 value: OrderedJsonValue,
1035 field: &'static str,
1036) -> Result<(String, OrderedJsonObject), String> {
1037 let OrderedJsonValue::Object(body) = value else {
1038 return Err(format!("expected tagged object with `{field}` field"));
1039 };
1040 let mut tag = None;
1041 for (key, value) in body.as_entries() {
1042 if key != field {
1043 continue;
1044 }
1045 if tag.is_some() {
1046 return Err(format!("tagged object has duplicate `{field}` field"));
1047 }
1048 let OrderedJsonValue::String(value) = value else {
1049 return Err(format!("tagged object has no string `{field}` field"));
1050 };
1051 tag = Some(value);
1052 }
1053 let Some(tag) = tag else {
1054 return Err(format!("tagged object has no string `{field}` field"));
1055 };
1056 Ok((tag.to_string(), body))
1057}
1058
1059fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1060 body.as_entries()
1061 .iter()
1062 .find_map(|(key, value)| (key == field).then_some(value))
1063}
1064
1065fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1066 match ordered_field(body, field) {
1067 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1068 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1069 None => Err(format!("tagged object has no `{field}` field")),
1070 }
1071}
1072
1073fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1074 let (tag, body) = read_ordered_tagged(value, "method")?;
1075 match tag.as_str() {
1076 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1077 digest: ordered_string(&body, "digest")?,
1078 }),
1079 "macos_spawn_inode" => {
1080 let device = ordered_field(&body, "device")
1081 .and_then(|value| match value {
1082 OrderedJsonValue::Number(number) => number.as_u64(),
1083 _ => None,
1084 })
1085 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1086 let inode = ordered_field(&body, "inode")
1087 .and_then(|value| match value {
1088 OrderedJsonValue::Number(number) => number.as_u64(),
1089 _ => None,
1090 })
1091 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
1092 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1093 }
1094 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1095 }
1096}
1097
1098impl Serialize for ModuleDeclaredProvenance {
1099 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1100 where
1101 S: Serializer,
1102 {
1103 match self {
1104 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1105 build: build.clone(),
1106 }
1107 .serialize(serializer),
1108 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1109 Self::Unknown { body, .. } => body.serialize(serializer),
1110 }
1111 }
1112}
1113
1114impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1115 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1116 where
1117 D: serde::Deserializer<'de>,
1118 {
1119 let (tag, value) = read_tagged(deserializer, "status")?;
1120 match tag.as_str() {
1121 "reported" => match serde_json::from_value(value.into_value())
1122 .map_err(D::Error::custom)?
1123 {
1124 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1125 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1126 },
1127 "unverifiable" => {
1128 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1129 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1130 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1131 }
1132 }
1133 _ => Ok(Self::Unknown { tag, body: value }),
1134 }
1135 }
1136}
1137
1138impl Serialize for RunningImageAgreement {
1139 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1140 where
1141 S: Serializer,
1142 {
1143 match self {
1144 Self::Match { evidence } => RunningImageAgreementWire::Match {
1145 evidence: evidence.clone(),
1146 }
1147 .serialize(serializer),
1148 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1149 running: running.clone(),
1150 disk: disk.clone(),
1151 }
1152 .serialize(serializer),
1153 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1154 reason: reason.clone(),
1155 }
1156 .serialize(serializer),
1157 Self::Unknown { body, .. } => body.serialize(serializer),
1158 }
1159 }
1160}
1161
1162impl<'de> Deserialize<'de> for RunningImageAgreement {
1163 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1164 where
1165 D: serde::Deserializer<'de>,
1166 {
1167 let (tag, value) = read_tagged(deserializer, "status")?;
1168 match tag.as_str() {
1169 "match" => Ok(Self::Match {
1170 evidence: decode_running_image_evidence(
1171 ordered_field(&value, "evidence")
1172 .cloned()
1173 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1174 )
1175 .map_err(D::Error::custom)?,
1176 }),
1177 "mismatch" => Ok(Self::Mismatch {
1178 running: decode_running_image_evidence(
1179 ordered_field(&value, "running")
1180 .cloned()
1181 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1182 )
1183 .map_err(D::Error::custom)?,
1184 disk: decode_running_image_evidence(
1185 ordered_field(&value, "disk")
1186 .cloned()
1187 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1188 )
1189 .map_err(D::Error::custom)?,
1190 }),
1191 "unavailable" => Ok(Self::Unavailable {
1192 reason: serde_json::from_value(
1193 ordered_field(&value, "reason")
1194 .cloned()
1195 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1196 .into_value(),
1197 )
1198 .map_err(D::Error::custom)?,
1199 }),
1200 _ => Ok(Self::Unknown { tag, body: value }),
1201 }
1202 }
1203}
1204
1205impl Serialize for RunningImageEvidence {
1206 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1207 where
1208 S: Serializer,
1209 {
1210 match self {
1211 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1212 digest: digest.clone(),
1213 }
1214 .serialize(serializer),
1215 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1216 device: *device,
1217 inode: *inode,
1218 }
1219 .serialize(serializer),
1220 Self::Unknown { body, .. } => body.serialize(serializer),
1221 }
1222 }
1223}
1224
1225impl<'de> Deserialize<'de> for RunningImageEvidence {
1226 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1227 where
1228 D: serde::Deserializer<'de>,
1229 {
1230 let (tag, value) = read_tagged(deserializer, "method")?;
1231 match tag.as_str() {
1232 "linux_proc_sha256" => {
1233 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1234 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1235 Ok(Self::LinuxProcSha256 { digest })
1236 }
1237 _ => unreachable!(),
1238 }
1239 }
1240 "macos_spawn_inode" => {
1241 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1242 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1243 Ok(Self::MacosSpawnInode { device, inode })
1244 }
1245 _ => unreachable!(),
1246 }
1247 }
1248 _ => Ok(Self::Unknown { tag, body: value }),
1249 }
1250 }
1251}
1252
1253impl Serialize for SupervisorRouteConsumer {
1254 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1255 where
1256 S: Serializer,
1257 {
1258 match self {
1259 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1260 module_id: module_id.clone(),
1261 }
1262 .serialize(serializer),
1263 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1264 connection_id: *connection_id,
1265 }
1266 .serialize(serializer),
1267 Self::Unknown { body, .. } => body.serialize(serializer),
1268 }
1269 }
1270}
1271
1272impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1273 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1274 where
1275 D: serde::Deserializer<'de>,
1276 {
1277 let (tag, value) = read_tagged(deserializer, "kind")?;
1278 match tag.as_str() {
1279 "reserved" => {
1280 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1281 SupervisorRouteConsumerWire::Reserved { module_id } => {
1282 Ok(Self::Reserved { module_id })
1283 }
1284 _ => unreachable!(),
1285 }
1286 }
1287 "direct" => {
1288 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1289 SupervisorRouteConsumerWire::Direct { connection_id } => {
1290 Ok(Self::Direct { connection_id })
1291 }
1292 _ => unreachable!(),
1293 }
1294 }
1295 _ => Ok(Self::Unknown { tag, body: value }),
1296 }
1297 }
1298}
1299
1300impl Serialize for StderrCaptureState {
1301 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1302 where
1303 S: Serializer,
1304 {
1305 match self {
1306 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1307 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1308 reason: reason.clone(),
1309 }
1310 .serialize(serializer),
1311 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1312 reason: reason.clone(),
1313 }
1314 .serialize(serializer),
1315 Self::Unknown { body, .. } => body.serialize(serializer),
1316 }
1317 }
1318}
1319
1320impl<'de> Deserialize<'de> for StderrCaptureState {
1321 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1322 where
1323 D: serde::Deserializer<'de>,
1324 {
1325 let (tag, value) = read_tagged(deserializer, "state")?;
1326 match tag.as_str() {
1327 "captured" => {
1328 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1329 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1330 _ => unreachable!(),
1331 }
1332 }
1333 "incomplete" => match serde_json::from_value(value.into_value())
1334 .map_err(D::Error::custom)?
1335 {
1336 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1337 _ => unreachable!(),
1338 },
1339 "not_captured" => match serde_json::from_value(value.into_value())
1340 .map_err(D::Error::custom)?
1341 {
1342 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1343 _ => unreachable!(),
1344 },
1345 _ => Ok(Self::Unknown { tag, body: value }),
1346 }
1347 }
1348}
1349
1350impl Serialize for StderrTailEntry {
1351 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1352 where
1353 S: Serializer,
1354 {
1355 match self {
1356 Self::Line { text, truncated } => StderrTailEntryWire::Line {
1357 text: text.clone(),
1358 truncated: *truncated,
1359 }
1360 .serialize(serializer),
1361 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1362 Self::Unknown { body, .. } => body.serialize(serializer),
1363 }
1364 }
1365}
1366
1367impl<'de> Deserialize<'de> for StderrTailEntry {
1368 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1369 where
1370 D: serde::Deserializer<'de>,
1371 {
1372 let (tag, value) = read_tagged(deserializer, "kind")?;
1373 match tag.as_str() {
1374 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1375 StderrTailEntryWire::Line { text, truncated } => Ok(Self::Line { text, truncated }),
1376 _ => unreachable!(),
1377 },
1378 "process_start" => {
1379 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1380 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1381 _ => unreachable!(),
1382 }
1383 }
1384 _ => Ok(Self::Unknown { tag, body: value }),
1385 }
1386 }
1387}
1388
1389fn is_zero_u64(value: &u64) -> bool {
1390 *value == 0
1391}
1392
1393fn default_true() -> bool {
1394 true
1395}
1396
1397#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1399pub struct TerminalHistory {
1400 pub daemon_started_at_ms: u64,
1402 pub entries: Vec<TerminalEntry>,
1403 #[serde(default, skip_serializing_if = "is_zero_u64")]
1406 pub dropped: u64,
1407 #[serde(default, skip_serializing_if = "is_zero_u64")]
1410 pub journal_skipped_lines: u64,
1411 #[serde(default, skip_serializing_if = "is_zero_u64")]
1413 pub journal_read_errors: u64,
1414 #[serde(default, skip_serializing_if = "is_zero_u64")]
1416 pub journal_write_failures: u64,
1417}
1418
1419#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1421pub struct TerminalEntry {
1422 #[serde(default, skip_serializing_if = "Option::is_none")]
1425 pub daemon_incarnation: Option<String>,
1426 #[serde(default, skip_serializing_if = "Option::is_none")]
1427 pub exit_code: Option<i32>,
1428 #[serde(default, skip_serializing_if = "Option::is_none")]
1429 pub exit_signal: Option<i32>,
1430 pub at_ms: u64,
1431 pub disposition: TerminalDisposition,
1432 #[serde(default, skip_serializing_if = "Option::is_none")]
1436 pub exit_kind: Option<TerminalExitKind>,
1437 #[serde(default, skip_serializing_if = "Option::is_none")]
1444 pub disposition_detail: Option<String>,
1445}
1446
1447#[derive(Debug, Clone, PartialEq, Eq)]
1452pub enum TerminalExitKind {
1453 Clean,
1454 Crash,
1455 DeliberateSeverance,
1456 Unknown(String),
1457}
1458
1459impl TerminalExitKind {
1460 fn wire_name(&self) -> &str {
1461 match self {
1462 Self::Clean => "clean",
1463 Self::Crash => "crash",
1464 Self::DeliberateSeverance => "deliberate_severance",
1465 Self::Unknown(value) => value,
1466 }
1467 }
1468}
1469
1470impl Serialize for TerminalExitKind {
1471 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1472 where
1473 S: serde::Serializer,
1474 {
1475 serializer.serialize_str(self.wire_name())
1476 }
1477}
1478
1479impl<'de> Deserialize<'de> for TerminalExitKind {
1480 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1481 where
1482 D: serde::Deserializer<'de>,
1483 {
1484 let value = String::deserialize(deserializer)?;
1485 Ok(match value.as_str() {
1486 "clean" => Self::Clean,
1487 "crash" => Self::Crash,
1488 "deliberate_severance" => Self::DeliberateSeverance,
1489 _ => Self::Unknown(value),
1490 })
1491 }
1492}
1493
1494open_string_enum! {
1495 TerminalDisposition {
1497 Stopped => "stopped",
1498 Disabled => "disabled",
1499 Failed => "failed",
1500 Restarting => "restarting",
1501 }
1502}
1503
1504#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1505#[serde(rename_all = "snake_case")]
1506pub enum PollKind {
1507 Status,
1508 Liveness,
1509}
1510
1511#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1512pub struct CatalogEntry {
1513 pub module_id: String,
1514 #[serde(default = "default_true")]
1525 pub ready: bool,
1526 #[serde(default, skip_serializing_if = "Option::is_none")]
1530 pub not_ready: Option<NotReadyReason>,
1531 #[serde(default, skip_serializing_if = "Option::is_none")]
1552 pub module_version: Option<String>,
1553 pub roles: Vec<ProviderRole>,
1554 pub control_ops: Vec<String>,
1555 #[serde(default, skip_serializing_if = "Option::is_none")]
1560 pub capabilities: Option<CapabilityDeclarations>,
1561 #[serde(default, skip_serializing_if = "Option::is_none")]
1564 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1565}
1566
1567#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1569pub struct NotReadyReason {
1570 pub reason: String,
1575 #[serde(default, skip_serializing_if = "Option::is_none")]
1578 pub capability: Option<String>,
1579}
1580
1581impl NotReadyReason {
1582 pub const DECLARED_NOT_READY: &'static str = "declared_not_ready";
1583 pub const REQUIRED_CAPABILITY_UNPROVIDED: &'static str = "required_capability_unprovided";
1584}
1585
1586#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1587pub struct CapabilityRequirementStatus {
1588 pub consumer: String,
1589 pub capability: String,
1590 pub need: String,
1591 pub verdict: String,
1592 pub episode_seq: u64,
1593 pub config_satisfiable: bool,
1594 pub runtime_available: bool,
1595 pub detail: String,
1596}
1597
1598#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1599pub struct SupervisorRescanResult {
1600 pub added: Vec<String>,
1601 pub removed: Vec<String>,
1602 pub changed_pending_reload: Vec<String>,
1603 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1616 pub enabled_changes: Vec<String>,
1617 pub unchanged: u32,
1618 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1626 pub preview: bool,
1627 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1645 pub restart_required: Vec<String>,
1646 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1650 pub capability_warnings: Vec<String>,
1651}
1652
1653#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1669#[serde(rename_all = "snake_case")]
1670pub enum ModuleProtocol {
1671 #[default]
1675 Subc,
1676 None,
1678}
1679
1680#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1681pub struct SupervisorEntry {
1682 pub module_id: String,
1683 pub state: String,
1684 pub enabled: bool,
1685 pub live: bool,
1695 #[serde(default)]
1699 pub protocol: ModuleProtocol,
1700 pub health: SupervisorHealthStatus,
1701 #[serde(default)]
1707 pub last_probe_ms: Option<u64>,
1708 #[serde(default, skip_serializing_if = "Option::is_none")]
1712 pub last_exit_code: Option<i32>,
1713 #[serde(default, skip_serializing_if = "Option::is_none")]
1717 pub last_exit_signal: Option<i32>,
1718 #[serde(default, skip_serializing_if = "Option::is_none")]
1722 pub last_exit_ms: Option<u64>,
1723 #[serde(default, skip_serializing_if = "Option::is_none")]
1726 pub last_exit_kind: Option<TerminalExitKind>,
1727 #[serde(default, skip_serializing_if = "Option::is_none")]
1744 pub restart_count: Option<u32>,
1745 #[serde(default, skip_serializing_if = "Option::is_none")]
1748 pub max_restarts: Option<u32>,
1749 #[serde(default, skip_serializing_if = "Option::is_none")]
1752 pub lifetime_restarts: Option<u32>,
1753 #[serde(default, skip_serializing_if = "Option::is_none")]
1757 pub spawn_generation: Option<u64>,
1758 #[serde(default, skip_serializing_if = "Option::is_none")]
1768 pub restart_window_secs: Option<u64>,
1769 #[serde(default, skip_serializing_if = "Option::is_none")]
1773 pub drain_timeout_ms: Option<u64>,
1774 #[serde(default, skip_serializing_if = "Option::is_none")]
1777 pub restart_backoff_ms: Option<u64>,
1778 #[serde(default, skip_serializing_if = "Option::is_none")]
1781 pub restart_max_backoff_ms: Option<u64>,
1782}
1783
1784#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1785#[serde(rename_all = "snake_case")]
1786pub enum SupervisorHealthStatus {
1787 Ok,
1788 Degraded,
1789 Failing,
1790 Unresponsive,
1791 Unknown,
1792}
1793
1794#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1795pub struct SupervisorHealthEntry {
1796 pub module_id: String,
1797 pub status: SupervisorHealthStatus,
1798 #[serde(default, skip_serializing_if = "Option::is_none")]
1804 pub detail: Option<String>,
1805 #[serde(default, skip_serializing_if = "Option::is_none")]
1810 pub metrics: Option<serde_json::Value>,
1811 pub consecutive_failures: u32,
1812 #[serde(default)]
1815 pub late_answer_count: u64,
1816 #[serde(default, skip_serializing_if = "Option::is_none")]
1818 pub last_late_answer_latency_ms: Option<u64>,
1819 #[serde(default)]
1824 pub last_action: Option<String>,
1825 #[serde(default)]
1828 pub last_action_ms: Option<u64>,
1829 #[serde(default, skip_serializing_if = "Option::is_none")]
1842 pub last_probe_ms: Option<u64>,
1843}
1844
1845#[cfg(test)]
1846mod tests {
1847 use super::*;
1848 use subc_protocol::{BindIdentity, RouteTarget};
1849
1850 #[test]
1851 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
1852 let entry = TerminalEntry {
1853 daemon_incarnation: Some("daemon-before-restart".into()),
1854 exit_code: Some(1),
1855 exit_signal: None,
1856 at_ms: 1_700_000_000_123,
1857 disposition: TerminalDisposition::Restarting,
1858 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
1859 disposition_detail: None,
1860 };
1861 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
1862 assert_eq!(
1863 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
1864 ["exit_kind"],
1865 "deliberate_severance"
1866 );
1867
1868 #[derive(serde::Deserialize)]
1869 struct LegacyTerminalEntry {
1870 exit_code: Option<i32>,
1871 exit_signal: Option<i32>,
1872 at_ms: u64,
1873 disposition: TerminalDisposition,
1874 }
1875
1876 let decoded: LegacyTerminalEntry =
1877 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
1878 assert_eq!(decoded.exit_code, Some(1));
1879 assert_eq!(decoded.exit_signal, None);
1880 assert_eq!(decoded.at_ms, 1_700_000_000_123);
1881 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
1882
1883 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
1884 let future: TerminalEntry =
1885 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
1886 assert_eq!(
1887 future.exit_kind,
1888 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
1889 );
1890 }
1891
1892 #[test]
1893 fn terminal_incarnation_is_optional_for_older_daemons() {
1894 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
1895 "at_ms": 123,
1896 "disposition": "stopped"
1897 }))
1898 .unwrap();
1899 let encoded = serde_json::to_value(&entry).unwrap();
1900 assert_eq!(
1901 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
1902 (None, None)
1903 );
1904 }
1905
1906 #[test]
1907 fn route_poll_uses_kind_field() {
1908 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
1909 route_channel: 7,
1910 route_epoch: 11,
1911 kind: PollKind::Status,
1912 })
1913 .unwrap();
1914
1915 assert_eq!(body["op"], "route.poll");
1916 assert_eq!(body["route_epoch"], 11);
1917 assert_eq!(body["kind"], "status");
1918 assert!(body.get("op").is_some());
1919 }
1920
1921 #[test]
1922 fn route_open_is_internally_tagged() {
1923 let request = ClientControlRequest::RouteOpen {
1924 target: RouteTarget::ToolProvider {
1925 module_id: "aft".to_string(),
1926 },
1927 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
1928 consumer_identity: None,
1929 consumer_capabilities: None,
1930 admission_facts: None,
1931 };
1932
1933 let body = serde_json::to_value(request).unwrap();
1934 assert_eq!(body["op"], "route.open");
1935 assert_eq!(body["target"]["kind"], "tool_provider");
1936 assert!(body.get("consumer_identity").is_none());
1937 assert!(body.get("consumer_capabilities").is_none());
1938 }
1939
1940 #[test]
1941 fn route_open_without_optional_fields_still_decodes() {
1942 let body = serde_json::json!({
1943 "op": "route.open",
1944 "target": { "kind": "tool_provider", "module_id": "aft" },
1945 "identity": {
1946 "project_root": "/tmp/project",
1947 "harness": "opencode",
1948 "session": "session-1"
1949 }
1950 });
1951
1952 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
1953 let ClientControlRequest::RouteOpen {
1954 consumer_identity,
1955 consumer_capabilities,
1956 admission_facts,
1957 ..
1958 } = decoded
1959 else {
1960 panic!("decoded wrong request variant");
1961 };
1962 assert_eq!(consumer_identity, None);
1963 assert_eq!(consumer_capabilities, None);
1964 assert_eq!(admission_facts, None);
1965 }
1966
1967 #[test]
1968 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
1969 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
1970 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
1971 match decoded {
1972 ClientControlPush::RouteClosed {
1973 excluded_subscriptions,
1974 terminal,
1975 ..
1976 } => {
1977 assert_eq!(excluded_subscriptions, 0);
1978 assert_eq!(terminal, None);
1979 }
1980 other => panic!("unexpected push: {other:?}"),
1981 }
1982 assert!(!serde_json::to_string(&decoded)
1983 .unwrap()
1984 .contains("terminal"));
1985 }
1986
1987 #[test]
1988 fn old_route_closed_decoder_ignores_new_terminal_field() {
1989 #[derive(serde::Deserialize)]
1990 #[serde(tag = "op")]
1991 enum LegacyClientControlPush {
1992 #[serde(rename = "route.closed")]
1993 RouteClosed {
1994 module_id: String,
1995 reason: RouteCloseReason,
1996 drained: bool,
1997 abandoned: u32,
1998 },
1999 }
2000
2001 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
2002 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
2003 match decoded {
2004 LegacyClientControlPush::RouteClosed {
2005 module_id,
2006 reason,
2007 drained,
2008 abandoned,
2009 } => {
2010 assert_eq!(module_id, "aft-tools");
2011 assert_eq!(reason, RouteCloseReason::Crash);
2012 assert!(!drained);
2013 assert_eq!(abandoned, 0);
2014 }
2015 }
2016 }
2017
2018 #[test]
2019 fn supervisor_routes_is_a_control_plane_request() {
2020 let body = serde_json::json!({
2021 "op": "supervisor.routes",
2022 "module_id": "aft"
2023 });
2024
2025 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
2026 assert_eq!(serde_json::to_value(request).unwrap(), body);
2027 }
2028
2029 #[test]
2030 fn diagnostic_string_enums_retain_unknown_wire_values() {
2031 let reason: RunningImageUnavailableReason =
2032 serde_json::from_str("\"future_reason\"").unwrap();
2033 let disposition: TerminalDisposition =
2034 serde_json::from_str("\"future_disposition\"").unwrap();
2035
2036 assert_eq!(
2037 reason,
2038 RunningImageUnavailableReason::Unknown("future_reason".to_string())
2039 );
2040 assert_eq!(
2041 disposition,
2042 TerminalDisposition::Unknown("future_disposition".to_string())
2043 );
2044 }
2045
2046 #[test]
2047 fn diagnostic_string_enums_preserve_existing_wire_names() {
2048 let names = [
2049 (RunningImageUnavailableReason::NotRunning, "not_running"),
2050 (
2051 RunningImageUnavailableReason::UnsupportedPlatform,
2052 "unsupported_platform",
2053 ),
2054 (
2055 RunningImageUnavailableReason::RunningExecutableUnreadable,
2056 "running_executable_unreadable",
2057 ),
2058 (
2059 RunningImageUnavailableReason::SpawnedPathUnreadable,
2060 "spawned_path_unreadable",
2061 ),
2062 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2063 (
2064 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2065 "process_identity_unconfirmed",
2066 ),
2067 ];
2068 for (value, expected) in names {
2069 let wire = serde_json::to_string(&value).unwrap();
2070 assert_eq!(wire, format!("\"{expected}\""));
2071 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2072 assert_eq!(decoded, value);
2073 }
2074
2075 for (value, expected) in [
2076 (TerminalDisposition::Stopped, "stopped"),
2077 (TerminalDisposition::Disabled, "disabled"),
2078 (TerminalDisposition::Failed, "failed"),
2079 (TerminalDisposition::Restarting, "restarting"),
2080 ] {
2081 let wire = serde_json::to_string(&value).unwrap();
2082 assert_eq!(wire, format!("\"{expected}\""));
2083 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2084 assert_eq!(decoded, value);
2085 }
2086 }
2087
2088 #[test]
2089 fn diagnostic_string_enums_reject_non_string_bodies() {
2090 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2091 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2092 }
2093
2094 #[test]
2095 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2096 let body = serde_json::json!({
2097 "op": "supervisor.provenance",
2098 "daemon": {
2099 "daemon_build": {},
2100 "daemon_observed": {
2101 "running_image": {
2102 "status": "unavailable",
2103 "reason": "not_running"
2104 }
2105 }
2106 },
2107 "modules": [
2108 {
2109 "module_id": "future",
2110 "module_declared": { "status": "unverifiable" },
2111 "daemon_observed": {
2112 "running_image": {
2113 "status": "unavailable",
2114 "reason": "future_reason"
2115 }
2116 }
2117 },
2118 {
2119 "module_id": "healthy-a",
2120 "module_declared": { "status": "unverifiable" },
2121 "daemon_observed": {
2122 "running_image": {
2123 "status": "match",
2124 "evidence": {
2125 "method": "linux_proc_sha256",
2126 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2127 }
2128 }
2129 }
2130 },
2131 {
2132 "module_id": "healthy-b",
2133 "module_declared": { "status": "unverifiable" },
2134 "daemon_observed": {
2135 "running_image": {
2136 "status": "unavailable",
2137 "reason": "unsupported_platform"
2138 }
2139 }
2140 }
2141 ]
2142 });
2143
2144 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2145 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2146 panic!("decoded wrong response variant");
2147 };
2148 assert_eq!(modules.len(), 3);
2149 assert_eq!(modules[0].module_id, "future");
2150 assert_eq!(
2151 modules[0].daemon_observed.running_image,
2152 RunningImageAgreement::Unavailable {
2153 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2154 }
2155 );
2156 assert_eq!(modules[1].module_id, "healthy-a");
2157 assert_eq!(modules[2].module_id, "healthy-b");
2158 }
2159
2160 #[test]
2161 fn tagged_unknown_values_retain_tag_and_body() {
2162 macro_rules! assert_unknown_round_trip {
2163 ($ty:ident, $field:literal, $value:expr) => {
2164 let value = $value;
2165 let wire = serde_json::to_string(&value).unwrap();
2166 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2167 match decoded {
2168 $ty::Unknown { tag, body } => {
2169 assert_eq!(tag, value[$field].as_str().unwrap());
2170 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2171 }
2172 _ => panic!("decoded known variant"),
2173 }
2174 };
2175 }
2176
2177 assert_unknown_round_trip!(
2178 ModuleDeclaredProvenance,
2179 "status",
2180 serde_json::json!({"status": "future", "build": {"version": 7}})
2181 );
2182 assert_unknown_round_trip!(
2183 RunningImageAgreement,
2184 "status",
2185 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2186 );
2187 assert_unknown_round_trip!(
2188 RunningImageEvidence,
2189 "method",
2190 serde_json::json!({"method": "future", "digest": "abc"})
2191 );
2192 assert_unknown_round_trip!(
2193 SupervisorRouteConsumer,
2194 "kind",
2195 serde_json::json!({"kind": "future", "module_id": "m"})
2196 );
2197 assert_unknown_round_trip!(
2198 StderrCaptureState,
2199 "state",
2200 serde_json::json!({"state": "future", "reason": "because"})
2201 );
2202 assert_unknown_round_trip!(
2203 StderrTailEntry,
2204 "kind",
2205 serde_json::json!({"kind": "future", "text": "line"})
2206 );
2207 }
2208
2209 #[test]
2210 fn tagged_unknown_values_round_trip_the_original_json() {
2211 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2212 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2213 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2214 }
2215
2216 #[test]
2217 fn tagged_unknown_values_round_trip_trailing_tag() {
2218 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2219 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2220 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2221
2222 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2223 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2224 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2225 }
2226
2227 #[test]
2228 fn tagged_unknown_values_round_trip_middle_tag() {
2229 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2230 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2231 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2232
2233 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2234 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2235 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2236 }
2237
2238 #[test]
2239 fn tagged_unknown_values_round_trip_deep_payload() {
2240 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2241 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2242 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2243
2244 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2245 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2246 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2247 }
2248
2249 #[test]
2250 fn tagged_unknown_values_reject_non_object_bodies() {
2251 for wire in ["42", r#""future""#, "[]"] {
2252 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2253 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2254 }
2255 }
2256
2257 #[test]
2258 fn duplicate_discriminators_reject_without_panicking() {
2259 assert_eq!(
2260 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2261 .unwrap(),
2262 ModuleDeclaredProvenance::Unverifiable
2263 );
2264 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2265 .unwrap()
2266 {
2267 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2268 _ => panic!("future discriminator decoded as a known variant"),
2269 }
2270
2271 let wires = [
2272 r#"{"status":"reported","status":"unverifiable"}"#,
2273 r#"{"status":"unverifiable","status":"reported"}"#,
2274 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2275 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2276 ];
2277
2278 for wire in wires {
2279 let result =
2280 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2281 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2282 assert!(
2283 result.unwrap().is_err(),
2284 "duplicate discriminator decoded: {wire}"
2285 );
2286 }
2287
2288 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2289 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2290 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2291 assert!(
2292 result.unwrap().is_err(),
2293 "duplicate discriminator decoded: {wire}"
2294 );
2295 }
2296
2297 #[test]
2298 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2299 let known_wire =
2300 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2301 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2302 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2303
2304 for wire in [
2305 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2306 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2307 ] {
2308 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2309 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2310 }
2311
2312 for wire in [
2313 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2314 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2315 ] {
2316 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2317 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2318 }
2319
2320 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2321 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2322 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2323
2324 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2325 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2326 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2327 }
2328
2329 #[test]
2330 fn tagged_unknown_member_does_not_discard_known_siblings() {
2331 let body = serde_json::json!({
2332 "modules": [{
2333 "module_id": "target",
2334 "routes": [
2335 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2336 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2337 ]
2338 }]
2339 });
2340 let decoded: ClientControlResponse = serde_json::from_value(
2341 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2342 )
2343 .unwrap();
2344 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2345 panic!("decoded wrong response variant");
2346 };
2347 assert_eq!(modules[0].routes.len(), 2);
2348 assert_eq!(
2349 modules[0].routes[1].consumer,
2350 SupervisorRouteConsumer::Direct { connection_id: 7 }
2351 );
2352 }
2353}