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 },
345 #[serde(rename = "catalog.list")]
346 CatalogList {
347 generation: u64,
348 modules: Vec<CatalogEntry>,
349 subc_ops: Vec<String>,
350 },
351 #[serde(rename = "route.open")]
352 RouteOpen {
353 route_channel: u16,
354 route_epoch: u32,
355 },
356 #[serde(rename = "route.poll")]
357 RoutePoll {
358 route_channel: u16,
359 route_epoch: u32,
360 status: Option<String>,
361 live: Option<bool>,
362 },
363 #[serde(rename = "supervisor.list")]
364 SupervisorList {
365 generation: u64,
366 modules: Vec<SupervisorEntry>,
367 },
368 #[serde(rename = "supervisor.spawn_snapshot")]
369 SupervisorSpawnSnapshot {
370 #[serde(flatten)]
371 snapshot: SpawnSnapshot,
372 },
373 #[serde(rename = "supervisor.ack")]
374 SupervisorAck { module_id: String, applied: bool },
375 #[serde(rename = "supervisor.rescan")]
376 SupervisorRescan {
377 #[serde(flatten)]
378 result: SupervisorRescanResult,
379 },
380 #[serde(rename = "supervisor.health_probe")]
381 SupervisorHealthProbe {
382 module_id: String,
383 status: HealthStatus,
384 #[serde(default, skip_serializing_if = "Option::is_none")]
385 detail: Option<String>,
386 #[serde(default, skip_serializing_if = "Option::is_none")]
387 metrics: Option<serde_json::Value>,
388 },
389 #[serde(rename = "supervisor.health")]
390 SupervisorHealth {
391 generation: u64,
392 modules: Vec<SupervisorHealthEntry>,
393 },
394 #[serde(rename = "supervisor.routes")]
395 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
396 #[serde(rename = "supervisor.provenance")]
397 SupervisorProvenance {
398 daemon: SupervisorDaemonProvenance,
399 modules: Vec<SupervisorModuleProvenance>,
400 },
401 #[serde(rename = "supervisor.stderr_tail")]
402 SupervisorStderrTail {
403 module_id: String,
404 #[serde(flatten)]
405 tail: StderrTail,
406 },
407 #[serde(rename = "supervisor.terminals")]
408 SupervisorTerminals {
409 module_id: String,
410 #[serde(flatten)]
411 terminals: TerminalHistory,
412 },
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
420#[serde(tag = "op")]
421pub enum ClientControlPush {
422 #[serde(rename = "route.closing")]
423 RouteClosing {
424 module_id: String,
425 reason: RouteCloseReason,
426 },
427 #[serde(rename = "route.closed")]
428 RouteClosed {
429 module_id: String,
430 reason: RouteCloseReason,
431 drained: bool,
433 abandoned: u32,
436 #[serde(default)]
438 excluded_subscriptions: u32,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
445 terminal: Option<bool>,
446 },
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
451pub struct SpawnCursor {
452 pub daemon_incarnation: String,
453 pub seq: u64,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
458pub struct LiveSpawn {
459 pub module_id: String,
460 pub spawn_generation: u64,
461 pub pid: u32,
462 pub spawned_at_ms: u64,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
467pub struct SpawnSnapshot {
468 pub cursor: SpawnCursor,
469 pub ring_bound: u64,
471 pub live: Vec<LiveSpawn>,
472}
473
474#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
476#[serde(rename_all = "snake_case")]
477pub enum SpawnEventKind {
478 Spawned,
479 Exited,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
488pub struct SpawnEvent {
489 pub cursor: SpawnCursor,
490 pub kind: SpawnEventKind,
491 pub module_id: String,
492 pub spawn_generation: u64,
493 pub pid: u32,
494 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub exit_code: Option<i32>,
496 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub exit_signal: Option<i32>,
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
502pub struct StderrTail {
503 pub capture: StderrCaptureState,
504 pub entries: Vec<StderrTailEntry>,
505 #[serde(default, skip_serializing_if = "is_zero_u64")]
514 pub dropped_lines: u64,
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
519pub struct SupervisorRouteModule {
520 pub module_id: String,
521 pub routes: Vec<SupervisorRoute>,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
526pub struct SupervisorRoute {
527 pub consumer: SupervisorRouteConsumer,
528 pub age_ms: u64,
530 pub draining: bool,
533 #[serde(default, skip_serializing_if = "Option::is_none")]
539 pub drain_reason: Option<RouteCloseReason>,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
544pub struct SupervisorModuleProvenance {
545 pub module_id: String,
546 pub module_declared: ModuleDeclaredProvenance,
547 pub daemon_observed: SupervisorObservedProcess,
548}
549
550#[derive(Debug, Clone, PartialEq)]
552pub enum ModuleDeclaredProvenance {
553 Reported {
554 build: ManifestProvenance,
555 },
556 Unverifiable,
557 Unknown {
560 tag: String,
561 body: OrderedJsonObject,
562 },
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
570pub struct SupervisorObservedProcess {
571 #[serde(default, skip_serializing_if = "Option::is_none")]
572 pub pid: Option<u32>,
573 #[serde(default, skip_serializing_if = "Option::is_none")]
574 pub spawned_at_ms: Option<u64>,
575 #[serde(default, skip_serializing_if = "Option::is_none")]
576 pub spawned_from: Option<PathBuf>,
577 pub running_image: RunningImageAgreement,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
582pub struct SupervisorDaemonProvenance {
583 pub daemon_build: DaemonBuildProvenance,
584 pub daemon_observed: DaemonObservedProcess,
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
589pub struct DaemonBuildProvenance {
590 #[serde(default, skip_serializing_if = "Option::is_none")]
591 pub build_git_sha: Option<String>,
592 #[serde(default, skip_serializing_if = "Option::is_none")]
593 pub build_lock_digest: Option<String>,
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
598pub struct DaemonObservedProcess {
599 #[serde(default, skip_serializing_if = "Option::is_none")]
600 pub pid: Option<u32>,
601 #[serde(default, skip_serializing_if = "Option::is_none")]
606 pub started_at_ms: Option<u64>,
607 pub running_image: RunningImageAgreement,
608}
609
610#[derive(Debug, Clone, PartialEq)]
612pub enum RunningImageAgreement {
613 Match {
614 evidence: RunningImageEvidence,
615 },
616 Mismatch {
617 running: RunningImageEvidence,
618 disk: RunningImageEvidence,
619 },
620 Unavailable {
621 reason: RunningImageUnavailableReason,
622 },
623 Unknown {
626 tag: String,
627 body: OrderedJsonObject,
628 },
629}
630
631#[derive(Debug, Clone, PartialEq)]
633pub enum RunningImageEvidence {
634 LinuxProcSha256 {
635 digest: String,
636 },
637 MacosSpawnInode {
638 device: u64,
639 inode: u64,
640 },
641 Unknown {
644 tag: String,
645 body: OrderedJsonObject,
646 },
647}
648
649open_string_enum! {
650 RunningImageUnavailableReason {
652 NotRunning => "not_running",
653 UnsupportedPlatform => "unsupported_platform",
654 RunningExecutableUnreadable => "running_executable_unreadable",
655 SpawnedPathUnreadable => "spawned_path_unreadable",
656 HashFailed => "hash_failed",
657 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
658 }
659}
660
661#[derive(Debug, Clone, PartialEq)]
667pub enum SupervisorRouteConsumer {
668 Reserved {
669 module_id: String,
670 },
671 Direct {
672 connection_id: u64,
673 },
674 Unknown {
677 tag: String,
678 body: OrderedJsonObject,
679 },
680}
681
682#[derive(Debug, Clone, PartialEq)]
689pub enum StderrCaptureState {
690 Captured,
693 Incomplete { reason: String },
695 NotCaptured { reason: String },
697 Unknown {
700 tag: String,
701 body: OrderedJsonObject,
702 },
703}
704
705#[derive(Debug, Clone, PartialEq)]
706pub enum StderrTailEntry {
707 Line {
708 text: String,
709 truncated: bool,
714 },
715 ProcessStart,
720 Unknown {
723 tag: String,
724 body: OrderedJsonObject,
725 },
726}
727
728#[derive(Debug, Serialize, Deserialize)]
729#[serde(tag = "status", rename_all = "snake_case")]
730enum ModuleDeclaredProvenanceWire {
731 Reported { build: ManifestProvenance },
732 Unverifiable,
733}
734
735#[derive(Debug, Serialize, Deserialize)]
736#[serde(tag = "status", rename_all = "snake_case")]
737enum RunningImageAgreementWire {
738 Match {
739 evidence: RunningImageEvidence,
740 },
741 Mismatch {
742 running: RunningImageEvidence,
743 disk: RunningImageEvidence,
744 },
745 Unavailable {
746 reason: RunningImageUnavailableReason,
747 },
748}
749
750#[derive(Debug, Serialize, Deserialize)]
751#[serde(tag = "method", rename_all = "snake_case")]
752enum RunningImageEvidenceWire {
753 LinuxProcSha256 { digest: String },
754 MacosSpawnInode { device: u64, inode: u64 },
755}
756
757#[derive(Debug, Serialize, Deserialize)]
758#[serde(tag = "kind", rename_all = "snake_case")]
759enum SupervisorRouteConsumerWire {
760 Reserved { module_id: String },
761 Direct { connection_id: u64 },
762}
763
764#[derive(Debug, Serialize, Deserialize)]
765#[serde(tag = "state", rename_all = "snake_case")]
766enum StderrCaptureStateWire {
767 Captured,
768 Incomplete { reason: String },
769 NotCaptured { reason: String },
770}
771
772#[derive(Debug, Serialize, Deserialize)]
773#[serde(tag = "kind", rename_all = "snake_case")]
774enum StderrTailEntryWire {
775 Line {
776 text: String,
777 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
778 truncated: bool,
779 },
780 ProcessStart,
781}
782
783#[derive(Debug, Clone, PartialEq)]
785pub enum OrderedJsonValue {
786 Null,
787 Bool(bool),
788 Number(serde_json::Number),
789 String(String),
790 Array(Vec<Self>),
791 Object(OrderedJsonObject),
792}
793
794#[derive(Debug, Clone, PartialEq)]
796pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
797
798impl OrderedJsonObject {
799 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
801 &self.0
802 }
803
804 fn into_value(self) -> serde_json::Value {
805 serde_json::Value::Object(
806 self.0
807 .into_iter()
808 .map(|(key, value)| (key, value.into_value()))
809 .collect(),
810 )
811 }
812}
813
814impl OrderedJsonValue {
815 fn into_value(self) -> serde_json::Value {
816 match self {
817 Self::Null => serde_json::Value::Null,
818 Self::Bool(value) => serde_json::Value::Bool(value),
819 Self::Number(value) => serde_json::Value::Number(value),
820 Self::String(value) => serde_json::Value::String(value),
821 Self::Array(values) => {
822 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
823 }
824 Self::Object(value) => value.into_value(),
825 }
826 }
827}
828
829impl Serialize for OrderedJsonValue {
830 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
831 where
832 S: Serializer,
833 {
834 match self {
835 Self::Null => serializer.serialize_unit(),
836 Self::Bool(value) => serializer.serialize_bool(*value),
837 Self::Number(value) => value.serialize(serializer),
838 Self::String(value) => serializer.serialize_str(value),
839 Self::Array(values) => values.serialize(serializer),
840 Self::Object(value) => value.serialize(serializer),
841 }
842 }
843}
844
845impl<'de> Deserialize<'de> for OrderedJsonValue {
846 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
847 where
848 D: Deserializer<'de>,
849 {
850 struct OrderedValueVisitor;
851
852 impl<'de> Visitor<'de> for OrderedValueVisitor {
853 type Value = OrderedJsonValue;
854
855 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
856 formatter.write_str("a JSON value with ordered object members")
857 }
858
859 fn visit_unit<E>(self) -> Result<Self::Value, E>
860 where
861 E: serde::de::Error,
862 {
863 Ok(OrderedJsonValue::Null)
864 }
865
866 fn visit_none<E>(self) -> Result<Self::Value, E>
867 where
868 E: serde::de::Error,
869 {
870 Ok(OrderedJsonValue::Null)
871 }
872
873 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
874 where
875 D: Deserializer<'de>,
876 {
877 OrderedJsonValue::deserialize(deserializer)
878 }
879
880 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
881 where
882 E: serde::de::Error,
883 {
884 Ok(OrderedJsonValue::Bool(value))
885 }
886
887 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
888 where
889 E: serde::de::Error,
890 {
891 Ok(OrderedJsonValue::Number(value.into()))
892 }
893
894 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
895 where
896 E: serde::de::Error,
897 {
898 Ok(OrderedJsonValue::Number(value.into()))
899 }
900
901 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
902 where
903 E: serde::de::Error,
904 {
905 serde_json::Number::from_f64(value)
906 .map(OrderedJsonValue::Number)
907 .ok_or_else(|| E::custom("non-finite JSON number"))
908 }
909
910 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
911 where
912 E: serde::de::Error,
913 {
914 Ok(OrderedJsonValue::String(value.to_owned()))
915 }
916
917 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
918 where
919 E: serde::de::Error,
920 {
921 Ok(OrderedJsonValue::String(value))
922 }
923
924 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
925 where
926 A: SeqAccess<'de>,
927 {
928 let mut values = Vec::new();
929 while let Some(value) = sequence.next_element()? {
930 values.push(value);
931 }
932 Ok(OrderedJsonValue::Array(values))
933 }
934
935 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
936 where
937 A: MapAccess<'de>,
938 {
939 let mut entries = Vec::new();
940 while let Some((key, value)) = map.next_entry()? {
941 entries.push((key, value));
942 }
943 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
944 }
945 }
946
947 deserializer.deserialize_any(OrderedValueVisitor)
948 }
949}
950
951impl Serialize for OrderedJsonObject {
952 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
953 where
954 S: Serializer,
955 {
956 let mut map = serializer.serialize_map(Some(self.0.len()))?;
957 for (key, value) in &self.0 {
958 map.serialize_entry(key, value)?;
959 }
960 map.end()
961 }
962}
963
964impl<'de> Deserialize<'de> for OrderedJsonObject {
965 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
966 where
967 D: Deserializer<'de>,
968 {
969 struct OrderedObjectVisitor;
970
971 impl<'de> Visitor<'de> for OrderedObjectVisitor {
972 type Value = OrderedJsonObject;
973
974 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
975 formatter.write_str("an object with ordered JSON members")
976 }
977
978 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
979 where
980 A: MapAccess<'de>,
981 {
982 let mut entries = Vec::new();
983 while let Some((key, value)) = map.next_entry()? {
984 entries.push((key, value));
985 }
986 Ok(OrderedJsonObject(entries))
987 }
988 }
989
990 deserializer.deserialize_map(OrderedObjectVisitor)
991 }
992}
993
994fn read_tagged<'de, D>(
995 deserializer: D,
996 field: &'static str,
997) -> Result<(String, OrderedJsonObject), D::Error>
998where
999 D: Deserializer<'de>,
1000{
1001 let body = OrderedJsonObject::deserialize(deserializer)?;
1002 let mut tag = None;
1003 for (key, value) in body.as_entries() {
1004 if key != field {
1005 continue;
1006 }
1007 if tag.is_some() {
1008 return Err(D::Error::custom(format!(
1009 "tagged object has duplicate `{field}` field"
1010 )));
1011 }
1012 let OrderedJsonValue::String(value) = value else {
1013 return Err(D::Error::custom(format!(
1014 "tagged object has no string `{field}` field"
1015 )));
1016 };
1017 tag = Some(value);
1018 }
1019 let Some(tag) = tag else {
1020 return Err(D::Error::custom(format!(
1021 "tagged object has no string `{field}` field"
1022 )));
1023 };
1024 Ok((tag.to_string(), body))
1025}
1026
1027fn read_ordered_tagged(
1028 value: OrderedJsonValue,
1029 field: &'static str,
1030) -> Result<(String, OrderedJsonObject), String> {
1031 let OrderedJsonValue::Object(body) = value else {
1032 return Err(format!("expected tagged object with `{field}` field"));
1033 };
1034 let mut tag = None;
1035 for (key, value) in body.as_entries() {
1036 if key != field {
1037 continue;
1038 }
1039 if tag.is_some() {
1040 return Err(format!("tagged object has duplicate `{field}` field"));
1041 }
1042 let OrderedJsonValue::String(value) = value else {
1043 return Err(format!("tagged object has no string `{field}` field"));
1044 };
1045 tag = Some(value);
1046 }
1047 let Some(tag) = tag else {
1048 return Err(format!("tagged object has no string `{field}` field"));
1049 };
1050 Ok((tag.to_string(), body))
1051}
1052
1053fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1054 body.as_entries()
1055 .iter()
1056 .find_map(|(key, value)| (key == field).then_some(value))
1057}
1058
1059fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1060 match ordered_field(body, field) {
1061 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1062 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1063 None => Err(format!("tagged object has no `{field}` field")),
1064 }
1065}
1066
1067fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1068 let (tag, body) = read_ordered_tagged(value, "method")?;
1069 match tag.as_str() {
1070 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1071 digest: ordered_string(&body, "digest")?,
1072 }),
1073 "macos_spawn_inode" => {
1074 let device = ordered_field(&body, "device")
1075 .and_then(|value| match value {
1076 OrderedJsonValue::Number(number) => number.as_u64(),
1077 _ => None,
1078 })
1079 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1080 let inode = ordered_field(&body, "inode")
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 `inode` field".to_string())?;
1086 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1087 }
1088 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1089 }
1090}
1091
1092impl Serialize for ModuleDeclaredProvenance {
1093 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1094 where
1095 S: Serializer,
1096 {
1097 match self {
1098 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1099 build: build.clone(),
1100 }
1101 .serialize(serializer),
1102 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1103 Self::Unknown { body, .. } => body.serialize(serializer),
1104 }
1105 }
1106}
1107
1108impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1109 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1110 where
1111 D: serde::Deserializer<'de>,
1112 {
1113 let (tag, value) = read_tagged(deserializer, "status")?;
1114 match tag.as_str() {
1115 "reported" => match serde_json::from_value(value.into_value())
1116 .map_err(D::Error::custom)?
1117 {
1118 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1119 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1120 },
1121 "unverifiable" => {
1122 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1123 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1124 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1125 }
1126 }
1127 _ => Ok(Self::Unknown { tag, body: value }),
1128 }
1129 }
1130}
1131
1132impl Serialize for RunningImageAgreement {
1133 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1134 where
1135 S: Serializer,
1136 {
1137 match self {
1138 Self::Match { evidence } => RunningImageAgreementWire::Match {
1139 evidence: evidence.clone(),
1140 }
1141 .serialize(serializer),
1142 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1143 running: running.clone(),
1144 disk: disk.clone(),
1145 }
1146 .serialize(serializer),
1147 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1148 reason: reason.clone(),
1149 }
1150 .serialize(serializer),
1151 Self::Unknown { body, .. } => body.serialize(serializer),
1152 }
1153 }
1154}
1155
1156impl<'de> Deserialize<'de> for RunningImageAgreement {
1157 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1158 where
1159 D: serde::Deserializer<'de>,
1160 {
1161 let (tag, value) = read_tagged(deserializer, "status")?;
1162 match tag.as_str() {
1163 "match" => Ok(Self::Match {
1164 evidence: decode_running_image_evidence(
1165 ordered_field(&value, "evidence")
1166 .cloned()
1167 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1168 )
1169 .map_err(D::Error::custom)?,
1170 }),
1171 "mismatch" => Ok(Self::Mismatch {
1172 running: decode_running_image_evidence(
1173 ordered_field(&value, "running")
1174 .cloned()
1175 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1176 )
1177 .map_err(D::Error::custom)?,
1178 disk: decode_running_image_evidence(
1179 ordered_field(&value, "disk")
1180 .cloned()
1181 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1182 )
1183 .map_err(D::Error::custom)?,
1184 }),
1185 "unavailable" => Ok(Self::Unavailable {
1186 reason: serde_json::from_value(
1187 ordered_field(&value, "reason")
1188 .cloned()
1189 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1190 .into_value(),
1191 )
1192 .map_err(D::Error::custom)?,
1193 }),
1194 _ => Ok(Self::Unknown { tag, body: value }),
1195 }
1196 }
1197}
1198
1199impl Serialize for RunningImageEvidence {
1200 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1201 where
1202 S: Serializer,
1203 {
1204 match self {
1205 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1206 digest: digest.clone(),
1207 }
1208 .serialize(serializer),
1209 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1210 device: *device,
1211 inode: *inode,
1212 }
1213 .serialize(serializer),
1214 Self::Unknown { body, .. } => body.serialize(serializer),
1215 }
1216 }
1217}
1218
1219impl<'de> Deserialize<'de> for RunningImageEvidence {
1220 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1221 where
1222 D: serde::Deserializer<'de>,
1223 {
1224 let (tag, value) = read_tagged(deserializer, "method")?;
1225 match tag.as_str() {
1226 "linux_proc_sha256" => {
1227 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1228 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1229 Ok(Self::LinuxProcSha256 { digest })
1230 }
1231 _ => unreachable!(),
1232 }
1233 }
1234 "macos_spawn_inode" => {
1235 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1236 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1237 Ok(Self::MacosSpawnInode { device, inode })
1238 }
1239 _ => unreachable!(),
1240 }
1241 }
1242 _ => Ok(Self::Unknown { tag, body: value }),
1243 }
1244 }
1245}
1246
1247impl Serialize for SupervisorRouteConsumer {
1248 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1249 where
1250 S: Serializer,
1251 {
1252 match self {
1253 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1254 module_id: module_id.clone(),
1255 }
1256 .serialize(serializer),
1257 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1258 connection_id: *connection_id,
1259 }
1260 .serialize(serializer),
1261 Self::Unknown { body, .. } => body.serialize(serializer),
1262 }
1263 }
1264}
1265
1266impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1267 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1268 where
1269 D: serde::Deserializer<'de>,
1270 {
1271 let (tag, value) = read_tagged(deserializer, "kind")?;
1272 match tag.as_str() {
1273 "reserved" => {
1274 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1275 SupervisorRouteConsumerWire::Reserved { module_id } => {
1276 Ok(Self::Reserved { module_id })
1277 }
1278 _ => unreachable!(),
1279 }
1280 }
1281 "direct" => {
1282 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1283 SupervisorRouteConsumerWire::Direct { connection_id } => {
1284 Ok(Self::Direct { connection_id })
1285 }
1286 _ => unreachable!(),
1287 }
1288 }
1289 _ => Ok(Self::Unknown { tag, body: value }),
1290 }
1291 }
1292}
1293
1294impl Serialize for StderrCaptureState {
1295 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1296 where
1297 S: Serializer,
1298 {
1299 match self {
1300 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1301 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1302 reason: reason.clone(),
1303 }
1304 .serialize(serializer),
1305 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1306 reason: reason.clone(),
1307 }
1308 .serialize(serializer),
1309 Self::Unknown { body, .. } => body.serialize(serializer),
1310 }
1311 }
1312}
1313
1314impl<'de> Deserialize<'de> for StderrCaptureState {
1315 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1316 where
1317 D: serde::Deserializer<'de>,
1318 {
1319 let (tag, value) = read_tagged(deserializer, "state")?;
1320 match tag.as_str() {
1321 "captured" => {
1322 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1323 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1324 _ => unreachable!(),
1325 }
1326 }
1327 "incomplete" => match serde_json::from_value(value.into_value())
1328 .map_err(D::Error::custom)?
1329 {
1330 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1331 _ => unreachable!(),
1332 },
1333 "not_captured" => match serde_json::from_value(value.into_value())
1334 .map_err(D::Error::custom)?
1335 {
1336 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1337 _ => unreachable!(),
1338 },
1339 _ => Ok(Self::Unknown { tag, body: value }),
1340 }
1341 }
1342}
1343
1344impl Serialize for StderrTailEntry {
1345 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1346 where
1347 S: Serializer,
1348 {
1349 match self {
1350 Self::Line { text, truncated } => StderrTailEntryWire::Line {
1351 text: text.clone(),
1352 truncated: *truncated,
1353 }
1354 .serialize(serializer),
1355 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1356 Self::Unknown { body, .. } => body.serialize(serializer),
1357 }
1358 }
1359}
1360
1361impl<'de> Deserialize<'de> for StderrTailEntry {
1362 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1363 where
1364 D: serde::Deserializer<'de>,
1365 {
1366 let (tag, value) = read_tagged(deserializer, "kind")?;
1367 match tag.as_str() {
1368 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1369 StderrTailEntryWire::Line { text, truncated } => Ok(Self::Line { text, truncated }),
1370 _ => unreachable!(),
1371 },
1372 "process_start" => {
1373 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1374 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1375 _ => unreachable!(),
1376 }
1377 }
1378 _ => Ok(Self::Unknown { tag, body: value }),
1379 }
1380 }
1381}
1382
1383fn is_zero_u64(value: &u64) -> bool {
1384 *value == 0
1385}
1386
1387fn default_true() -> bool {
1388 true
1389}
1390
1391#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1393pub struct TerminalHistory {
1394 pub daemon_started_at_ms: u64,
1396 pub entries: Vec<TerminalEntry>,
1397 #[serde(default, skip_serializing_if = "is_zero_u64")]
1400 pub dropped: u64,
1401 #[serde(default, skip_serializing_if = "is_zero_u64")]
1404 pub journal_skipped_lines: u64,
1405 #[serde(default, skip_serializing_if = "is_zero_u64")]
1407 pub journal_read_errors: u64,
1408 #[serde(default, skip_serializing_if = "is_zero_u64")]
1410 pub journal_write_failures: u64,
1411}
1412
1413#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1415pub struct TerminalEntry {
1416 #[serde(default, skip_serializing_if = "Option::is_none")]
1419 pub daemon_incarnation: Option<String>,
1420 #[serde(default, skip_serializing_if = "Option::is_none")]
1421 pub exit_code: Option<i32>,
1422 #[serde(default, skip_serializing_if = "Option::is_none")]
1423 pub exit_signal: Option<i32>,
1424 pub at_ms: u64,
1425 pub disposition: TerminalDisposition,
1426 #[serde(default, skip_serializing_if = "Option::is_none")]
1430 pub exit_kind: Option<TerminalExitKind>,
1431 #[serde(default, skip_serializing_if = "Option::is_none")]
1438 pub disposition_detail: Option<String>,
1439}
1440
1441#[derive(Debug, Clone, PartialEq, Eq)]
1446pub enum TerminalExitKind {
1447 Clean,
1448 Crash,
1449 DeliberateSeverance,
1450 Unknown(String),
1451}
1452
1453impl TerminalExitKind {
1454 fn wire_name(&self) -> &str {
1455 match self {
1456 Self::Clean => "clean",
1457 Self::Crash => "crash",
1458 Self::DeliberateSeverance => "deliberate_severance",
1459 Self::Unknown(value) => value,
1460 }
1461 }
1462}
1463
1464impl Serialize for TerminalExitKind {
1465 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1466 where
1467 S: serde::Serializer,
1468 {
1469 serializer.serialize_str(self.wire_name())
1470 }
1471}
1472
1473impl<'de> Deserialize<'de> for TerminalExitKind {
1474 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1475 where
1476 D: serde::Deserializer<'de>,
1477 {
1478 let value = String::deserialize(deserializer)?;
1479 Ok(match value.as_str() {
1480 "clean" => Self::Clean,
1481 "crash" => Self::Crash,
1482 "deliberate_severance" => Self::DeliberateSeverance,
1483 _ => Self::Unknown(value),
1484 })
1485 }
1486}
1487
1488open_string_enum! {
1489 TerminalDisposition {
1491 Stopped => "stopped",
1492 Disabled => "disabled",
1493 Failed => "failed",
1494 Restarting => "restarting",
1495 }
1496}
1497
1498#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1499#[serde(rename_all = "snake_case")]
1500pub enum PollKind {
1501 Status,
1502 Liveness,
1503}
1504
1505#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1506pub struct CatalogEntry {
1507 pub module_id: String,
1508 #[serde(default = "default_true")]
1512 pub ready: bool,
1513 #[serde(default, skip_serializing_if = "Option::is_none")]
1534 pub module_version: Option<String>,
1535 pub roles: Vec<ProviderRole>,
1536 pub control_ops: Vec<String>,
1537 #[serde(default, skip_serializing_if = "Option::is_none")]
1542 pub capabilities: Option<CapabilityDeclarations>,
1543 #[serde(default, skip_serializing_if = "Option::is_none")]
1546 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1547}
1548
1549#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1550pub struct CapabilityRequirementStatus {
1551 pub consumer: String,
1552 pub capability: String,
1553 pub need: String,
1554 pub verdict: String,
1555 pub episode_seq: u64,
1556 pub config_satisfiable: bool,
1557 pub runtime_available: bool,
1558 pub detail: String,
1559}
1560
1561#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1562pub struct SupervisorRescanResult {
1563 pub added: Vec<String>,
1564 pub removed: Vec<String>,
1565 pub changed_pending_reload: Vec<String>,
1566 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1579 pub enabled_changes: Vec<String>,
1580 pub unchanged: u32,
1581 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1589 pub preview: bool,
1590 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1608 pub restart_required: Vec<String>,
1609 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1613 pub capability_warnings: Vec<String>,
1614}
1615
1616#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1632#[serde(rename_all = "snake_case")]
1633pub enum ModuleProtocol {
1634 #[default]
1638 Subc,
1639 None,
1641}
1642
1643#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1644pub struct SupervisorEntry {
1645 pub module_id: String,
1646 pub state: String,
1647 pub enabled: bool,
1648 pub live: bool,
1658 #[serde(default)]
1662 pub protocol: ModuleProtocol,
1663 pub health: SupervisorHealthStatus,
1664 #[serde(default)]
1670 pub last_probe_ms: Option<u64>,
1671 #[serde(default, skip_serializing_if = "Option::is_none")]
1675 pub last_exit_code: Option<i32>,
1676 #[serde(default, skip_serializing_if = "Option::is_none")]
1680 pub last_exit_signal: Option<i32>,
1681 #[serde(default, skip_serializing_if = "Option::is_none")]
1685 pub last_exit_ms: Option<u64>,
1686 #[serde(default, skip_serializing_if = "Option::is_none")]
1689 pub last_exit_kind: Option<TerminalExitKind>,
1690 #[serde(default, skip_serializing_if = "Option::is_none")]
1707 pub restart_count: Option<u32>,
1708 #[serde(default, skip_serializing_if = "Option::is_none")]
1711 pub max_restarts: Option<u32>,
1712 #[serde(default, skip_serializing_if = "Option::is_none")]
1715 pub lifetime_restarts: Option<u32>,
1716 #[serde(default, skip_serializing_if = "Option::is_none")]
1720 pub spawn_generation: Option<u64>,
1721 #[serde(default, skip_serializing_if = "Option::is_none")]
1731 pub restart_window_secs: Option<u64>,
1732 #[serde(default, skip_serializing_if = "Option::is_none")]
1736 pub drain_timeout_ms: Option<u64>,
1737 #[serde(default, skip_serializing_if = "Option::is_none")]
1740 pub restart_backoff_ms: Option<u64>,
1741 #[serde(default, skip_serializing_if = "Option::is_none")]
1744 pub restart_max_backoff_ms: Option<u64>,
1745}
1746
1747#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1748#[serde(rename_all = "snake_case")]
1749pub enum SupervisorHealthStatus {
1750 Ok,
1751 Degraded,
1752 Failing,
1753 Unresponsive,
1754 Unknown,
1755}
1756
1757#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1758pub struct SupervisorHealthEntry {
1759 pub module_id: String,
1760 pub status: SupervisorHealthStatus,
1761 #[serde(default, skip_serializing_if = "Option::is_none")]
1767 pub detail: Option<String>,
1768 #[serde(default, skip_serializing_if = "Option::is_none")]
1773 pub metrics: Option<serde_json::Value>,
1774 pub consecutive_failures: u32,
1775 #[serde(default)]
1778 pub late_answer_count: u64,
1779 #[serde(default, skip_serializing_if = "Option::is_none")]
1781 pub last_late_answer_latency_ms: Option<u64>,
1782 #[serde(default)]
1787 pub last_action: Option<String>,
1788 #[serde(default)]
1791 pub last_action_ms: Option<u64>,
1792 #[serde(default, skip_serializing_if = "Option::is_none")]
1805 pub last_probe_ms: Option<u64>,
1806}
1807
1808#[cfg(test)]
1809mod tests {
1810 use super::*;
1811 use subc_protocol::{BindIdentity, RouteTarget};
1812
1813 #[test]
1814 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
1815 let entry = TerminalEntry {
1816 daemon_incarnation: Some("daemon-before-restart".into()),
1817 exit_code: Some(1),
1818 exit_signal: None,
1819 at_ms: 1_700_000_000_123,
1820 disposition: TerminalDisposition::Restarting,
1821 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
1822 disposition_detail: None,
1823 };
1824 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
1825 assert_eq!(
1826 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
1827 ["exit_kind"],
1828 "deliberate_severance"
1829 );
1830
1831 #[derive(serde::Deserialize)]
1832 struct LegacyTerminalEntry {
1833 exit_code: Option<i32>,
1834 exit_signal: Option<i32>,
1835 at_ms: u64,
1836 disposition: TerminalDisposition,
1837 }
1838
1839 let decoded: LegacyTerminalEntry =
1840 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
1841 assert_eq!(decoded.exit_code, Some(1));
1842 assert_eq!(decoded.exit_signal, None);
1843 assert_eq!(decoded.at_ms, 1_700_000_000_123);
1844 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
1845
1846 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
1847 let future: TerminalEntry =
1848 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
1849 assert_eq!(
1850 future.exit_kind,
1851 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
1852 );
1853 }
1854
1855 #[test]
1856 fn terminal_incarnation_is_optional_for_older_daemons() {
1857 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
1858 "at_ms": 123,
1859 "disposition": "stopped"
1860 }))
1861 .unwrap();
1862 let encoded = serde_json::to_value(&entry).unwrap();
1863 assert_eq!(
1864 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
1865 (None, None)
1866 );
1867 }
1868
1869 #[test]
1870 fn route_poll_uses_kind_field() {
1871 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
1872 route_channel: 7,
1873 route_epoch: 11,
1874 kind: PollKind::Status,
1875 })
1876 .unwrap();
1877
1878 assert_eq!(body["op"], "route.poll");
1879 assert_eq!(body["route_epoch"], 11);
1880 assert_eq!(body["kind"], "status");
1881 assert!(body.get("op").is_some());
1882 }
1883
1884 #[test]
1885 fn route_open_is_internally_tagged() {
1886 let request = ClientControlRequest::RouteOpen {
1887 target: RouteTarget::ToolProvider {
1888 module_id: "aft".to_string(),
1889 },
1890 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
1891 consumer_identity: None,
1892 consumer_capabilities: None,
1893 admission_facts: None,
1894 };
1895
1896 let body = serde_json::to_value(request).unwrap();
1897 assert_eq!(body["op"], "route.open");
1898 assert_eq!(body["target"]["kind"], "tool_provider");
1899 assert!(body.get("consumer_identity").is_none());
1900 assert!(body.get("consumer_capabilities").is_none());
1901 }
1902
1903 #[test]
1904 fn route_open_without_optional_fields_still_decodes() {
1905 let body = serde_json::json!({
1906 "op": "route.open",
1907 "target": { "kind": "tool_provider", "module_id": "aft" },
1908 "identity": {
1909 "project_root": "/tmp/project",
1910 "harness": "opencode",
1911 "session": "session-1"
1912 }
1913 });
1914
1915 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
1916 let ClientControlRequest::RouteOpen {
1917 consumer_identity,
1918 consumer_capabilities,
1919 admission_facts,
1920 ..
1921 } = decoded
1922 else {
1923 panic!("decoded wrong request variant");
1924 };
1925 assert_eq!(consumer_identity, None);
1926 assert_eq!(consumer_capabilities, None);
1927 assert_eq!(admission_facts, None);
1928 }
1929
1930 #[test]
1931 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
1932 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
1933 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
1934 match decoded {
1935 ClientControlPush::RouteClosed {
1936 excluded_subscriptions,
1937 terminal,
1938 ..
1939 } => {
1940 assert_eq!(excluded_subscriptions, 0);
1941 assert_eq!(terminal, None);
1942 }
1943 other => panic!("unexpected push: {other:?}"),
1944 }
1945 assert!(!serde_json::to_string(&decoded)
1946 .unwrap()
1947 .contains("terminal"));
1948 }
1949
1950 #[test]
1951 fn old_route_closed_decoder_ignores_new_terminal_field() {
1952 #[derive(serde::Deserialize)]
1953 #[serde(tag = "op")]
1954 enum LegacyClientControlPush {
1955 #[serde(rename = "route.closed")]
1956 RouteClosed {
1957 module_id: String,
1958 reason: RouteCloseReason,
1959 drained: bool,
1960 abandoned: u32,
1961 },
1962 }
1963
1964 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
1965 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
1966 match decoded {
1967 LegacyClientControlPush::RouteClosed {
1968 module_id,
1969 reason,
1970 drained,
1971 abandoned,
1972 } => {
1973 assert_eq!(module_id, "aft-tools");
1974 assert_eq!(reason, RouteCloseReason::Crash);
1975 assert!(!drained);
1976 assert_eq!(abandoned, 0);
1977 }
1978 }
1979 }
1980
1981 #[test]
1982 fn supervisor_routes_is_a_control_plane_request() {
1983 let body = serde_json::json!({
1984 "op": "supervisor.routes",
1985 "module_id": "aft"
1986 });
1987
1988 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
1989 assert_eq!(serde_json::to_value(request).unwrap(), body);
1990 }
1991
1992 #[test]
1993 fn diagnostic_string_enums_retain_unknown_wire_values() {
1994 let reason: RunningImageUnavailableReason =
1995 serde_json::from_str("\"future_reason\"").unwrap();
1996 let disposition: TerminalDisposition =
1997 serde_json::from_str("\"future_disposition\"").unwrap();
1998
1999 assert_eq!(
2000 reason,
2001 RunningImageUnavailableReason::Unknown("future_reason".to_string())
2002 );
2003 assert_eq!(
2004 disposition,
2005 TerminalDisposition::Unknown("future_disposition".to_string())
2006 );
2007 }
2008
2009 #[test]
2010 fn diagnostic_string_enums_preserve_existing_wire_names() {
2011 let names = [
2012 (RunningImageUnavailableReason::NotRunning, "not_running"),
2013 (
2014 RunningImageUnavailableReason::UnsupportedPlatform,
2015 "unsupported_platform",
2016 ),
2017 (
2018 RunningImageUnavailableReason::RunningExecutableUnreadable,
2019 "running_executable_unreadable",
2020 ),
2021 (
2022 RunningImageUnavailableReason::SpawnedPathUnreadable,
2023 "spawned_path_unreadable",
2024 ),
2025 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2026 (
2027 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2028 "process_identity_unconfirmed",
2029 ),
2030 ];
2031 for (value, expected) in names {
2032 let wire = serde_json::to_string(&value).unwrap();
2033 assert_eq!(wire, format!("\"{expected}\""));
2034 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2035 assert_eq!(decoded, value);
2036 }
2037
2038 for (value, expected) in [
2039 (TerminalDisposition::Stopped, "stopped"),
2040 (TerminalDisposition::Disabled, "disabled"),
2041 (TerminalDisposition::Failed, "failed"),
2042 (TerminalDisposition::Restarting, "restarting"),
2043 ] {
2044 let wire = serde_json::to_string(&value).unwrap();
2045 assert_eq!(wire, format!("\"{expected}\""));
2046 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2047 assert_eq!(decoded, value);
2048 }
2049 }
2050
2051 #[test]
2052 fn diagnostic_string_enums_reject_non_string_bodies() {
2053 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2054 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2055 }
2056
2057 #[test]
2058 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2059 let body = serde_json::json!({
2060 "op": "supervisor.provenance",
2061 "daemon": {
2062 "daemon_build": {},
2063 "daemon_observed": {
2064 "running_image": {
2065 "status": "unavailable",
2066 "reason": "not_running"
2067 }
2068 }
2069 },
2070 "modules": [
2071 {
2072 "module_id": "future",
2073 "module_declared": { "status": "unverifiable" },
2074 "daemon_observed": {
2075 "running_image": {
2076 "status": "unavailable",
2077 "reason": "future_reason"
2078 }
2079 }
2080 },
2081 {
2082 "module_id": "healthy-a",
2083 "module_declared": { "status": "unverifiable" },
2084 "daemon_observed": {
2085 "running_image": {
2086 "status": "match",
2087 "evidence": {
2088 "method": "linux_proc_sha256",
2089 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2090 }
2091 }
2092 }
2093 },
2094 {
2095 "module_id": "healthy-b",
2096 "module_declared": { "status": "unverifiable" },
2097 "daemon_observed": {
2098 "running_image": {
2099 "status": "unavailable",
2100 "reason": "unsupported_platform"
2101 }
2102 }
2103 }
2104 ]
2105 });
2106
2107 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2108 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2109 panic!("decoded wrong response variant");
2110 };
2111 assert_eq!(modules.len(), 3);
2112 assert_eq!(modules[0].module_id, "future");
2113 assert_eq!(
2114 modules[0].daemon_observed.running_image,
2115 RunningImageAgreement::Unavailable {
2116 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2117 }
2118 );
2119 assert_eq!(modules[1].module_id, "healthy-a");
2120 assert_eq!(modules[2].module_id, "healthy-b");
2121 }
2122
2123 #[test]
2124 fn tagged_unknown_values_retain_tag_and_body() {
2125 macro_rules! assert_unknown_round_trip {
2126 ($ty:ident, $field:literal, $value:expr) => {
2127 let value = $value;
2128 let wire = serde_json::to_string(&value).unwrap();
2129 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2130 match decoded {
2131 $ty::Unknown { tag, body } => {
2132 assert_eq!(tag, value[$field].as_str().unwrap());
2133 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2134 }
2135 _ => panic!("decoded known variant"),
2136 }
2137 };
2138 }
2139
2140 assert_unknown_round_trip!(
2141 ModuleDeclaredProvenance,
2142 "status",
2143 serde_json::json!({"status": "future", "build": {"version": 7}})
2144 );
2145 assert_unknown_round_trip!(
2146 RunningImageAgreement,
2147 "status",
2148 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2149 );
2150 assert_unknown_round_trip!(
2151 RunningImageEvidence,
2152 "method",
2153 serde_json::json!({"method": "future", "digest": "abc"})
2154 );
2155 assert_unknown_round_trip!(
2156 SupervisorRouteConsumer,
2157 "kind",
2158 serde_json::json!({"kind": "future", "module_id": "m"})
2159 );
2160 assert_unknown_round_trip!(
2161 StderrCaptureState,
2162 "state",
2163 serde_json::json!({"state": "future", "reason": "because"})
2164 );
2165 assert_unknown_round_trip!(
2166 StderrTailEntry,
2167 "kind",
2168 serde_json::json!({"kind": "future", "text": "line"})
2169 );
2170 }
2171
2172 #[test]
2173 fn tagged_unknown_values_round_trip_the_original_json() {
2174 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2175 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2176 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2177 }
2178
2179 #[test]
2180 fn tagged_unknown_values_round_trip_trailing_tag() {
2181 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2182 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2183 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2184
2185 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2186 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2187 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2188 }
2189
2190 #[test]
2191 fn tagged_unknown_values_round_trip_middle_tag() {
2192 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2193 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2194 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2195
2196 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2197 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2198 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2199 }
2200
2201 #[test]
2202 fn tagged_unknown_values_round_trip_deep_payload() {
2203 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2204 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2205 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2206
2207 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2208 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2209 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2210 }
2211
2212 #[test]
2213 fn tagged_unknown_values_reject_non_object_bodies() {
2214 for wire in ["42", r#""future""#, "[]"] {
2215 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2216 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2217 }
2218 }
2219
2220 #[test]
2221 fn duplicate_discriminators_reject_without_panicking() {
2222 assert_eq!(
2223 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2224 .unwrap(),
2225 ModuleDeclaredProvenance::Unverifiable
2226 );
2227 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2228 .unwrap()
2229 {
2230 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2231 _ => panic!("future discriminator decoded as a known variant"),
2232 }
2233
2234 let wires = [
2235 r#"{"status":"reported","status":"unverifiable"}"#,
2236 r#"{"status":"unverifiable","status":"reported"}"#,
2237 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2238 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2239 ];
2240
2241 for wire in wires {
2242 let result =
2243 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2244 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2245 assert!(
2246 result.unwrap().is_err(),
2247 "duplicate discriminator decoded: {wire}"
2248 );
2249 }
2250
2251 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2252 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2253 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2254 assert!(
2255 result.unwrap().is_err(),
2256 "duplicate discriminator decoded: {wire}"
2257 );
2258 }
2259
2260 #[test]
2261 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2262 let known_wire =
2263 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2264 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2265 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2266
2267 for wire in [
2268 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2269 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2270 ] {
2271 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2272 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2273 }
2274
2275 for wire in [
2276 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2277 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2278 ] {
2279 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2280 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2281 }
2282
2283 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2284 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2285 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2286
2287 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2288 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2289 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2290 }
2291
2292 #[test]
2293 fn tagged_unknown_member_does_not_discard_known_siblings() {
2294 let body = serde_json::json!({
2295 "modules": [{
2296 "module_id": "target",
2297 "routes": [
2298 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2299 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2300 ]
2301 }]
2302 });
2303 let decoded: ClientControlResponse = serde_json::from_value(
2304 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2305 )
2306 .unwrap();
2307 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2308 panic!("decoded wrong response variant");
2309 };
2310 assert_eq!(modules[0].routes.len(), 2);
2311 assert_eq!(
2312 modules[0].routes[1].consumer,
2313 SupervisorRouteConsumer::Direct { connection_id: 7 }
2314 );
2315 }
2316}