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