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(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")]
197 SupervisorSpawnSubscribe {
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 since: Option<SpawnCursor>,
200 },
201 #[serde(rename = "supervisor.restart")]
202 SupervisorRestart {
203 module_id: String,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
212 drain_timeout_ms: Option<u64>,
213 },
214 #[serde(rename = "supervisor.swap")]
229 SupervisorSwap {
230 module_id: String,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
234 ready_timeout_ms: Option<u64>,
235 },
236 #[serde(rename = "supervisor.reload")]
237 SupervisorReload { module_id: String },
238 #[serde(rename = "supervisor.rescan")]
239 SupervisorRescan {
240 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
260 preview: bool,
261 },
262 #[serde(rename = "supervisor.release_reserved")]
266 SupervisorReleaseReserved { module_id: String },
267 #[serde(rename = "supervisor.set_enabled")]
268 SupervisorSetEnabled { module_id: String, enabled: bool },
269 #[serde(rename = "supervisor.health_probe")]
270 SupervisorHealthProbe { module_id: String },
271 #[serde(rename = "supervisor.health")]
272 SupervisorHealth {},
273 #[serde(rename = "supervisor.routes")]
286 SupervisorRoutes {
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 module_id: Option<String>,
289 },
290 #[serde(rename = "supervisor.provenance")]
293 SupervisorProvenance {
294 #[serde(default, skip_serializing_if = "Option::is_none")]
295 module_id: Option<String>,
296 },
297 #[serde(rename = "supervisor.stderr_tail")]
305 SupervisorStderrTail {
306 module_id: String,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 max_lines: Option<u32>,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
310 max_bytes: Option<u32>,
311 },
312 #[serde(rename = "supervisor.terminals")]
325 SupervisorTerminals { module_id: String },
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
330#[serde(tag = "op")]
331pub enum ClientControlResponse {
332 #[serde(rename = "server.describe")]
333 ServerDescribe {
334 protocol_ver: u8,
335 subc_ops: Vec<String>,
336 capabilities: Vec<String>,
337 connected_clients: u64,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
339 counters: Option<serde_json::Value>,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
347 build_git_sha: Option<String>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
354 build_lock_digest: Option<String>,
355 #[serde(default, skip_serializing_if = "Vec::is_empty")]
359 capability_requirements: Vec<CapabilityRequirementStatus>,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
365 machine_id: Option<String>,
366 },
367 #[serde(rename = "catalog.list")]
368 CatalogList {
369 generation: u64,
370 modules: Vec<CatalogEntry>,
371 subc_ops: Vec<String>,
372 },
373 #[serde(rename = "route.open")]
374 RouteOpen {
375 route_channel: u16,
376 route_epoch: u32,
377 },
378 #[serde(rename = "route.poll")]
379 RoutePoll {
380 route_channel: u16,
381 route_epoch: u32,
382 status: Option<String>,
383 live: Option<bool>,
384 },
385 #[serde(rename = "supervisor.list")]
386 SupervisorList {
387 generation: u64,
388 modules: Vec<SupervisorEntry>,
389 },
390 #[serde(rename = "supervisor.spawn_snapshot")]
391 SupervisorSpawnSnapshot {
392 #[serde(flatten)]
393 snapshot: SpawnSnapshot,
394 },
395 #[serde(rename = "supervisor.ack")]
396 SupervisorAck { module_id: String, applied: bool },
397 #[serde(rename = "supervisor.rescan")]
398 SupervisorRescan {
399 #[serde(flatten)]
400 result: SupervisorRescanResult,
401 },
402 #[serde(rename = "supervisor.health_probe")]
403 SupervisorHealthProbe {
404 module_id: String,
405 status: HealthStatus,
406 #[serde(default, skip_serializing_if = "Option::is_none")]
407 detail: Option<String>,
408 #[serde(default, skip_serializing_if = "Option::is_none")]
409 metrics: Option<serde_json::Value>,
410 },
411 #[serde(rename = "supervisor.health")]
412 SupervisorHealth {
413 generation: u64,
414 modules: Vec<SupervisorHealthEntry>,
415 },
416 #[serde(rename = "supervisor.routes")]
417 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
418 #[serde(rename = "supervisor.provenance")]
419 SupervisorProvenance {
420 daemon: SupervisorDaemonProvenance,
421 modules: Vec<SupervisorModuleProvenance>,
422 },
423 #[serde(rename = "supervisor.stderr_tail")]
424 SupervisorStderrTail {
425 module_id: String,
426 #[serde(flatten)]
427 tail: StderrTail,
428 },
429 #[serde(rename = "supervisor.terminals")]
430 SupervisorTerminals {
431 module_id: String,
432 #[serde(flatten)]
433 terminals: TerminalHistory,
434 },
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
442#[serde(tag = "op")]
443pub enum ClientControlPush {
444 #[serde(rename = "route.closing")]
445 RouteClosing {
446 module_id: String,
447 reason: RouteCloseReason,
448 },
449 #[serde(rename = "route.closed")]
450 RouteClosed {
451 module_id: String,
452 reason: RouteCloseReason,
453 drained: bool,
455 abandoned: u32,
458 #[serde(default)]
460 excluded_subscriptions: u32,
461 #[serde(default, skip_serializing_if = "Option::is_none")]
467 terminal: Option<bool>,
468 },
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
473pub struct SpawnCursor {
474 pub daemon_incarnation: String,
475 pub seq: u64,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
480pub struct LiveSpawn {
481 pub module_id: String,
482 pub spawn_generation: u64,
483 pub pid: u32,
484 pub spawned_at_ms: u64,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
489pub struct SpawnSnapshot {
490 pub cursor: SpawnCursor,
491 pub ring_bound: u64,
493 pub live: Vec<LiveSpawn>,
494}
495
496#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
498#[serde(rename_all = "snake_case")]
499pub enum SpawnEventKind {
500 Spawned,
501 Exited,
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
510pub struct SpawnEvent {
511 pub cursor: SpawnCursor,
512 pub kind: SpawnEventKind,
513 pub module_id: String,
514 pub spawn_generation: u64,
515 pub pid: u32,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub exit_code: Option<i32>,
518 #[serde(default, skip_serializing_if = "Option::is_none")]
519 pub exit_signal: Option<i32>,
520}
521
522#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
524pub struct StderrTail {
525 pub capture: StderrCaptureState,
526 pub entries: Vec<StderrTailEntry>,
527 #[serde(default, skip_serializing_if = "is_zero_u64")]
536 pub dropped_lines: u64,
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
541pub struct SupervisorRouteModule {
542 pub module_id: String,
543 pub routes: Vec<SupervisorRoute>,
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
548pub struct SupervisorRoute {
549 pub consumer: SupervisorRouteConsumer,
550 pub age_ms: u64,
552 pub draining: bool,
555 #[serde(default, skip_serializing_if = "Option::is_none")]
561 pub drain_reason: Option<RouteCloseReason>,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
566pub struct SupervisorModuleProvenance {
567 pub module_id: String,
568 pub module_declared: ModuleDeclaredProvenance,
569 pub daemon_observed: SupervisorObservedProcess,
570}
571
572#[derive(Debug, Clone, PartialEq)]
574pub enum ModuleDeclaredProvenance {
575 Reported {
576 build: ManifestProvenance,
577 },
578 Unverifiable,
579 Unknown {
582 tag: String,
583 body: OrderedJsonObject,
584 },
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
592pub struct SupervisorObservedProcess {
593 #[serde(default, skip_serializing_if = "Option::is_none")]
594 pub pid: Option<u32>,
595 #[serde(default, skip_serializing_if = "Option::is_none")]
596 pub spawned_at_ms: Option<u64>,
597 #[serde(default, skip_serializing_if = "Option::is_none")]
598 pub spawned_from: Option<PathBuf>,
599 pub running_image: RunningImageAgreement,
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
604pub struct SupervisorDaemonProvenance {
605 pub daemon_build: DaemonBuildProvenance,
606 pub daemon_observed: DaemonObservedProcess,
607}
608
609#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
611pub struct DaemonBuildProvenance {
612 #[serde(default, skip_serializing_if = "Option::is_none")]
613 pub build_git_sha: Option<String>,
614 #[serde(default, skip_serializing_if = "Option::is_none")]
615 pub build_lock_digest: Option<String>,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
620pub struct DaemonObservedProcess {
621 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub pid: Option<u32>,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub started_at_ms: Option<u64>,
629 pub running_image: RunningImageAgreement,
630}
631
632#[derive(Debug, Clone, PartialEq)]
634pub enum RunningImageAgreement {
635 Match {
636 evidence: RunningImageEvidence,
637 },
638 Mismatch {
639 running: RunningImageEvidence,
640 disk: RunningImageEvidence,
641 },
642 Unavailable {
643 reason: RunningImageUnavailableReason,
644 },
645 Unknown {
648 tag: String,
649 body: OrderedJsonObject,
650 },
651}
652
653#[derive(Debug, Clone, PartialEq)]
655pub enum RunningImageEvidence {
656 LinuxProcSha256 {
657 digest: String,
658 },
659 MacosSpawnInode {
660 device: u64,
661 inode: u64,
662 },
663 Unknown {
666 tag: String,
667 body: OrderedJsonObject,
668 },
669}
670
671open_string_enum! {
672 RunningImageUnavailableReason {
674 NotRunning => "not_running",
675 UnsupportedPlatform => "unsupported_platform",
676 RunningExecutableUnreadable => "running_executable_unreadable",
677 SpawnedPathUnreadable => "spawned_path_unreadable",
678 HashFailed => "hash_failed",
679 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
680 }
681}
682
683#[derive(Debug, Clone, PartialEq)]
689pub enum SupervisorRouteConsumer {
690 Reserved {
691 module_id: String,
692 },
693 Direct {
694 connection_id: u64,
695 },
696 Unknown {
699 tag: String,
700 body: OrderedJsonObject,
701 },
702}
703
704#[derive(Debug, Clone, PartialEq)]
711pub enum StderrCaptureState {
712 Captured,
715 Incomplete { reason: String },
717 NotCaptured { reason: String },
719 Unknown {
722 tag: String,
723 body: OrderedJsonObject,
724 },
725}
726
727#[derive(Debug, Clone, PartialEq)]
728pub enum StderrTailEntry {
729 Line {
730 text: String,
731 truncated: bool,
736 },
737 ProcessStart,
742 Unknown {
745 tag: String,
746 body: OrderedJsonObject,
747 },
748}
749
750#[derive(Debug, Serialize, Deserialize)]
751#[serde(tag = "status", rename_all = "snake_case")]
752enum ModuleDeclaredProvenanceWire {
753 Reported { build: ManifestProvenance },
754 Unverifiable,
755}
756
757#[derive(Debug, Serialize, Deserialize)]
758#[serde(tag = "status", rename_all = "snake_case")]
759enum RunningImageAgreementWire {
760 Match {
761 evidence: RunningImageEvidence,
762 },
763 Mismatch {
764 running: RunningImageEvidence,
765 disk: RunningImageEvidence,
766 },
767 Unavailable {
768 reason: RunningImageUnavailableReason,
769 },
770}
771
772#[derive(Debug, Serialize, Deserialize)]
773#[serde(tag = "method", rename_all = "snake_case")]
774enum RunningImageEvidenceWire {
775 LinuxProcSha256 { digest: String },
776 MacosSpawnInode { device: u64, inode: u64 },
777}
778
779#[derive(Debug, Serialize, Deserialize)]
780#[serde(tag = "kind", rename_all = "snake_case")]
781enum SupervisorRouteConsumerWire {
782 Reserved { module_id: String },
783 Direct { connection_id: u64 },
784}
785
786#[derive(Debug, Serialize, Deserialize)]
787#[serde(tag = "state", rename_all = "snake_case")]
788enum StderrCaptureStateWire {
789 Captured,
790 Incomplete { reason: String },
791 NotCaptured { reason: String },
792}
793
794#[derive(Debug, Serialize, Deserialize)]
795#[serde(tag = "kind", rename_all = "snake_case")]
796enum StderrTailEntryWire {
797 Line {
798 text: String,
799 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
800 truncated: bool,
801 },
802 ProcessStart,
803}
804
805#[derive(Debug, Clone, PartialEq)]
807pub enum OrderedJsonValue {
808 Null,
809 Bool(bool),
810 Number(serde_json::Number),
811 String(String),
812 Array(Vec<Self>),
813 Object(OrderedJsonObject),
814}
815
816#[derive(Debug, Clone, PartialEq)]
818pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
819
820impl OrderedJsonObject {
821 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
823 &self.0
824 }
825
826 fn into_value(self) -> serde_json::Value {
827 serde_json::Value::Object(
828 self.0
829 .into_iter()
830 .map(|(key, value)| (key, value.into_value()))
831 .collect(),
832 )
833 }
834}
835
836impl OrderedJsonValue {
837 fn into_value(self) -> serde_json::Value {
838 match self {
839 Self::Null => serde_json::Value::Null,
840 Self::Bool(value) => serde_json::Value::Bool(value),
841 Self::Number(value) => serde_json::Value::Number(value),
842 Self::String(value) => serde_json::Value::String(value),
843 Self::Array(values) => {
844 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
845 }
846 Self::Object(value) => value.into_value(),
847 }
848 }
849}
850
851impl Serialize for OrderedJsonValue {
852 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
853 where
854 S: Serializer,
855 {
856 match self {
857 Self::Null => serializer.serialize_unit(),
858 Self::Bool(value) => serializer.serialize_bool(*value),
859 Self::Number(value) => value.serialize(serializer),
860 Self::String(value) => serializer.serialize_str(value),
861 Self::Array(values) => values.serialize(serializer),
862 Self::Object(value) => value.serialize(serializer),
863 }
864 }
865}
866
867impl<'de> Deserialize<'de> for OrderedJsonValue {
868 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
869 where
870 D: Deserializer<'de>,
871 {
872 struct OrderedValueVisitor;
873
874 impl<'de> Visitor<'de> for OrderedValueVisitor {
875 type Value = OrderedJsonValue;
876
877 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
878 formatter.write_str("a JSON value with ordered object members")
879 }
880
881 fn visit_unit<E>(self) -> Result<Self::Value, E>
882 where
883 E: serde::de::Error,
884 {
885 Ok(OrderedJsonValue::Null)
886 }
887
888 fn visit_none<E>(self) -> Result<Self::Value, E>
889 where
890 E: serde::de::Error,
891 {
892 Ok(OrderedJsonValue::Null)
893 }
894
895 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
896 where
897 D: Deserializer<'de>,
898 {
899 OrderedJsonValue::deserialize(deserializer)
900 }
901
902 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
903 where
904 E: serde::de::Error,
905 {
906 Ok(OrderedJsonValue::Bool(value))
907 }
908
909 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
910 where
911 E: serde::de::Error,
912 {
913 Ok(OrderedJsonValue::Number(value.into()))
914 }
915
916 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
917 where
918 E: serde::de::Error,
919 {
920 Ok(OrderedJsonValue::Number(value.into()))
921 }
922
923 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
924 where
925 E: serde::de::Error,
926 {
927 serde_json::Number::from_f64(value)
928 .map(OrderedJsonValue::Number)
929 .ok_or_else(|| E::custom("non-finite JSON number"))
930 }
931
932 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
933 where
934 E: serde::de::Error,
935 {
936 Ok(OrderedJsonValue::String(value.to_owned()))
937 }
938
939 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
940 where
941 E: serde::de::Error,
942 {
943 Ok(OrderedJsonValue::String(value))
944 }
945
946 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
947 where
948 A: SeqAccess<'de>,
949 {
950 let mut values = Vec::new();
951 while let Some(value) = sequence.next_element()? {
952 values.push(value);
953 }
954 Ok(OrderedJsonValue::Array(values))
955 }
956
957 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
958 where
959 A: MapAccess<'de>,
960 {
961 let mut entries = Vec::new();
962 while let Some((key, value)) = map.next_entry()? {
963 entries.push((key, value));
964 }
965 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
966 }
967 }
968
969 deserializer.deserialize_any(OrderedValueVisitor)
970 }
971}
972
973impl Serialize for OrderedJsonObject {
974 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
975 where
976 S: Serializer,
977 {
978 let mut map = serializer.serialize_map(Some(self.0.len()))?;
979 for (key, value) in &self.0 {
980 map.serialize_entry(key, value)?;
981 }
982 map.end()
983 }
984}
985
986impl<'de> Deserialize<'de> for OrderedJsonObject {
987 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
988 where
989 D: Deserializer<'de>,
990 {
991 struct OrderedObjectVisitor;
992
993 impl<'de> Visitor<'de> for OrderedObjectVisitor {
994 type Value = OrderedJsonObject;
995
996 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
997 formatter.write_str("an object with ordered JSON members")
998 }
999
1000 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1001 where
1002 A: MapAccess<'de>,
1003 {
1004 let mut entries = Vec::new();
1005 while let Some((key, value)) = map.next_entry()? {
1006 entries.push((key, value));
1007 }
1008 Ok(OrderedJsonObject(entries))
1009 }
1010 }
1011
1012 deserializer.deserialize_map(OrderedObjectVisitor)
1013 }
1014}
1015
1016fn read_tagged<'de, D>(
1017 deserializer: D,
1018 field: &'static str,
1019) -> Result<(String, OrderedJsonObject), D::Error>
1020where
1021 D: Deserializer<'de>,
1022{
1023 let body = OrderedJsonObject::deserialize(deserializer)?;
1024 let mut tag = None;
1025 for (key, value) in body.as_entries() {
1026 if key != field {
1027 continue;
1028 }
1029 if tag.is_some() {
1030 return Err(D::Error::custom(format!(
1031 "tagged object has duplicate `{field}` field"
1032 )));
1033 }
1034 let OrderedJsonValue::String(value) = value else {
1035 return Err(D::Error::custom(format!(
1036 "tagged object has no string `{field}` field"
1037 )));
1038 };
1039 tag = Some(value);
1040 }
1041 let Some(tag) = tag else {
1042 return Err(D::Error::custom(format!(
1043 "tagged object has no string `{field}` field"
1044 )));
1045 };
1046 Ok((tag.to_string(), body))
1047}
1048
1049fn read_ordered_tagged(
1050 value: OrderedJsonValue,
1051 field: &'static str,
1052) -> Result<(String, OrderedJsonObject), String> {
1053 let OrderedJsonValue::Object(body) = value else {
1054 return Err(format!("expected tagged object with `{field}` field"));
1055 };
1056 let mut tag = None;
1057 for (key, value) in body.as_entries() {
1058 if key != field {
1059 continue;
1060 }
1061 if tag.is_some() {
1062 return Err(format!("tagged object has duplicate `{field}` field"));
1063 }
1064 let OrderedJsonValue::String(value) = value else {
1065 return Err(format!("tagged object has no string `{field}` field"));
1066 };
1067 tag = Some(value);
1068 }
1069 let Some(tag) = tag else {
1070 return Err(format!("tagged object has no string `{field}` field"));
1071 };
1072 Ok((tag.to_string(), body))
1073}
1074
1075fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1076 body.as_entries()
1077 .iter()
1078 .find_map(|(key, value)| (key == field).then_some(value))
1079}
1080
1081fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1082 match ordered_field(body, field) {
1083 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1084 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1085 None => Err(format!("tagged object has no `{field}` field")),
1086 }
1087}
1088
1089fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1090 let (tag, body) = read_ordered_tagged(value, "method")?;
1091 match tag.as_str() {
1092 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1093 digest: ordered_string(&body, "digest")?,
1094 }),
1095 "macos_spawn_inode" => {
1096 let device = ordered_field(&body, "device")
1097 .and_then(|value| match value {
1098 OrderedJsonValue::Number(number) => number.as_u64(),
1099 _ => None,
1100 })
1101 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1102 let inode = ordered_field(&body, "inode")
1103 .and_then(|value| match value {
1104 OrderedJsonValue::Number(number) => number.as_u64(),
1105 _ => None,
1106 })
1107 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
1108 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1109 }
1110 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1111 }
1112}
1113
1114impl Serialize for ModuleDeclaredProvenance {
1115 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1116 where
1117 S: Serializer,
1118 {
1119 match self {
1120 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1121 build: build.clone(),
1122 }
1123 .serialize(serializer),
1124 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1125 Self::Unknown { body, .. } => body.serialize(serializer),
1126 }
1127 }
1128}
1129
1130impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1131 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1132 where
1133 D: serde::Deserializer<'de>,
1134 {
1135 let (tag, value) = read_tagged(deserializer, "status")?;
1136 match tag.as_str() {
1137 "reported" => match serde_json::from_value(value.into_value())
1138 .map_err(D::Error::custom)?
1139 {
1140 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1141 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1142 },
1143 "unverifiable" => {
1144 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1145 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1146 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1147 }
1148 }
1149 _ => Ok(Self::Unknown { tag, body: value }),
1150 }
1151 }
1152}
1153
1154impl Serialize for RunningImageAgreement {
1155 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1156 where
1157 S: Serializer,
1158 {
1159 match self {
1160 Self::Match { evidence } => RunningImageAgreementWire::Match {
1161 evidence: evidence.clone(),
1162 }
1163 .serialize(serializer),
1164 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1165 running: running.clone(),
1166 disk: disk.clone(),
1167 }
1168 .serialize(serializer),
1169 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1170 reason: reason.clone(),
1171 }
1172 .serialize(serializer),
1173 Self::Unknown { body, .. } => body.serialize(serializer),
1174 }
1175 }
1176}
1177
1178impl<'de> Deserialize<'de> for RunningImageAgreement {
1179 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1180 where
1181 D: serde::Deserializer<'de>,
1182 {
1183 let (tag, value) = read_tagged(deserializer, "status")?;
1184 match tag.as_str() {
1185 "match" => Ok(Self::Match {
1186 evidence: decode_running_image_evidence(
1187 ordered_field(&value, "evidence")
1188 .cloned()
1189 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1190 )
1191 .map_err(D::Error::custom)?,
1192 }),
1193 "mismatch" => Ok(Self::Mismatch {
1194 running: decode_running_image_evidence(
1195 ordered_field(&value, "running")
1196 .cloned()
1197 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1198 )
1199 .map_err(D::Error::custom)?,
1200 disk: decode_running_image_evidence(
1201 ordered_field(&value, "disk")
1202 .cloned()
1203 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1204 )
1205 .map_err(D::Error::custom)?,
1206 }),
1207 "unavailable" => Ok(Self::Unavailable {
1208 reason: serde_json::from_value(
1209 ordered_field(&value, "reason")
1210 .cloned()
1211 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1212 .into_value(),
1213 )
1214 .map_err(D::Error::custom)?,
1215 }),
1216 _ => Ok(Self::Unknown { tag, body: value }),
1217 }
1218 }
1219}
1220
1221impl Serialize for RunningImageEvidence {
1222 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1223 where
1224 S: Serializer,
1225 {
1226 match self {
1227 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1228 digest: digest.clone(),
1229 }
1230 .serialize(serializer),
1231 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1232 device: *device,
1233 inode: *inode,
1234 }
1235 .serialize(serializer),
1236 Self::Unknown { body, .. } => body.serialize(serializer),
1237 }
1238 }
1239}
1240
1241impl<'de> Deserialize<'de> for RunningImageEvidence {
1242 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1243 where
1244 D: serde::Deserializer<'de>,
1245 {
1246 let (tag, value) = read_tagged(deserializer, "method")?;
1247 match tag.as_str() {
1248 "linux_proc_sha256" => {
1249 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1250 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1251 Ok(Self::LinuxProcSha256 { digest })
1252 }
1253 _ => unreachable!(),
1254 }
1255 }
1256 "macos_spawn_inode" => {
1257 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1258 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1259 Ok(Self::MacosSpawnInode { device, inode })
1260 }
1261 _ => unreachable!(),
1262 }
1263 }
1264 _ => Ok(Self::Unknown { tag, body: value }),
1265 }
1266 }
1267}
1268
1269impl Serialize for SupervisorRouteConsumer {
1270 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1271 where
1272 S: Serializer,
1273 {
1274 match self {
1275 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1276 module_id: module_id.clone(),
1277 }
1278 .serialize(serializer),
1279 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1280 connection_id: *connection_id,
1281 }
1282 .serialize(serializer),
1283 Self::Unknown { body, .. } => body.serialize(serializer),
1284 }
1285 }
1286}
1287
1288impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1289 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1290 where
1291 D: serde::Deserializer<'de>,
1292 {
1293 let (tag, value) = read_tagged(deserializer, "kind")?;
1294 match tag.as_str() {
1295 "reserved" => {
1296 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1297 SupervisorRouteConsumerWire::Reserved { module_id } => {
1298 Ok(Self::Reserved { module_id })
1299 }
1300 _ => unreachable!(),
1301 }
1302 }
1303 "direct" => {
1304 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1305 SupervisorRouteConsumerWire::Direct { connection_id } => {
1306 Ok(Self::Direct { connection_id })
1307 }
1308 _ => unreachable!(),
1309 }
1310 }
1311 _ => Ok(Self::Unknown { tag, body: value }),
1312 }
1313 }
1314}
1315
1316impl Serialize for StderrCaptureState {
1317 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1318 where
1319 S: Serializer,
1320 {
1321 match self {
1322 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1323 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1324 reason: reason.clone(),
1325 }
1326 .serialize(serializer),
1327 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1328 reason: reason.clone(),
1329 }
1330 .serialize(serializer),
1331 Self::Unknown { body, .. } => body.serialize(serializer),
1332 }
1333 }
1334}
1335
1336impl<'de> Deserialize<'de> for StderrCaptureState {
1337 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1338 where
1339 D: serde::Deserializer<'de>,
1340 {
1341 let (tag, value) = read_tagged(deserializer, "state")?;
1342 match tag.as_str() {
1343 "captured" => {
1344 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1345 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1346 _ => unreachable!(),
1347 }
1348 }
1349 "incomplete" => match serde_json::from_value(value.into_value())
1350 .map_err(D::Error::custom)?
1351 {
1352 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1353 _ => unreachable!(),
1354 },
1355 "not_captured" => match serde_json::from_value(value.into_value())
1356 .map_err(D::Error::custom)?
1357 {
1358 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1359 _ => unreachable!(),
1360 },
1361 _ => Ok(Self::Unknown { tag, body: value }),
1362 }
1363 }
1364}
1365
1366impl Serialize for StderrTailEntry {
1367 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1368 where
1369 S: Serializer,
1370 {
1371 match self {
1372 Self::Line { text, truncated } => StderrTailEntryWire::Line {
1373 text: text.clone(),
1374 truncated: *truncated,
1375 }
1376 .serialize(serializer),
1377 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1378 Self::Unknown { body, .. } => body.serialize(serializer),
1379 }
1380 }
1381}
1382
1383impl<'de> Deserialize<'de> for StderrTailEntry {
1384 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1385 where
1386 D: serde::Deserializer<'de>,
1387 {
1388 let (tag, value) = read_tagged(deserializer, "kind")?;
1389 match tag.as_str() {
1390 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1391 StderrTailEntryWire::Line { text, truncated } => Ok(Self::Line { text, truncated }),
1392 _ => unreachable!(),
1393 },
1394 "process_start" => {
1395 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1396 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1397 _ => unreachable!(),
1398 }
1399 }
1400 _ => Ok(Self::Unknown { tag, body: value }),
1401 }
1402 }
1403}
1404
1405fn is_zero_u64(value: &u64) -> bool {
1406 *value == 0
1407}
1408
1409fn default_true() -> bool {
1410 true
1411}
1412
1413#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1415pub struct TerminalHistory {
1416 pub daemon_started_at_ms: u64,
1418 pub entries: Vec<TerminalEntry>,
1419 #[serde(default, skip_serializing_if = "is_zero_u64")]
1422 pub dropped: u64,
1423 #[serde(default, skip_serializing_if = "is_zero_u64")]
1426 pub journal_skipped_lines: u64,
1427 #[serde(default, skip_serializing_if = "is_zero_u64")]
1429 pub journal_read_errors: u64,
1430 #[serde(default, skip_serializing_if = "is_zero_u64")]
1432 pub journal_write_failures: u64,
1433}
1434
1435#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1437pub struct TerminalEntry {
1438 #[serde(default, skip_serializing_if = "Option::is_none")]
1441 pub daemon_incarnation: Option<String>,
1442 #[serde(default, skip_serializing_if = "Option::is_none")]
1443 pub exit_code: Option<i32>,
1444 #[serde(default, skip_serializing_if = "Option::is_none")]
1445 pub exit_signal: Option<i32>,
1446 pub at_ms: u64,
1447 pub disposition: TerminalDisposition,
1448 #[serde(default, skip_serializing_if = "Option::is_none")]
1452 pub exit_kind: Option<TerminalExitKind>,
1453 #[serde(default, skip_serializing_if = "Option::is_none")]
1460 pub disposition_detail: Option<String>,
1461}
1462
1463#[derive(Debug, Clone, PartialEq, Eq)]
1468pub enum TerminalExitKind {
1469 Clean,
1470 Crash,
1471 DeliberateSeverance,
1472 Unknown(String),
1473}
1474
1475impl TerminalExitKind {
1476 fn wire_name(&self) -> &str {
1477 match self {
1478 Self::Clean => "clean",
1479 Self::Crash => "crash",
1480 Self::DeliberateSeverance => "deliberate_severance",
1481 Self::Unknown(value) => value,
1482 }
1483 }
1484}
1485
1486impl Serialize for TerminalExitKind {
1487 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1488 where
1489 S: serde::Serializer,
1490 {
1491 serializer.serialize_str(self.wire_name())
1492 }
1493}
1494
1495impl<'de> Deserialize<'de> for TerminalExitKind {
1496 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1497 where
1498 D: serde::Deserializer<'de>,
1499 {
1500 let value = String::deserialize(deserializer)?;
1501 Ok(match value.as_str() {
1502 "clean" => Self::Clean,
1503 "crash" => Self::Crash,
1504 "deliberate_severance" => Self::DeliberateSeverance,
1505 _ => Self::Unknown(value),
1506 })
1507 }
1508}
1509
1510open_string_enum! {
1511 TerminalDisposition {
1513 Stopped => "stopped",
1514 Disabled => "disabled",
1515 Failed => "failed",
1516 Restarting => "restarting",
1517 }
1518}
1519
1520#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1521#[serde(rename_all = "snake_case")]
1522pub enum PollKind {
1523 Status,
1524 Liveness,
1525}
1526
1527#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1528pub struct CatalogEntry {
1529 pub module_id: String,
1530 #[serde(default = "default_true")]
1541 pub ready: bool,
1542 #[serde(default, skip_serializing_if = "Option::is_none")]
1546 pub not_ready: Option<NotReadyReason>,
1547 #[serde(default, skip_serializing_if = "Option::is_none")]
1568 pub module_version: Option<String>,
1569 pub roles: Vec<ProviderRole>,
1570 pub control_ops: Vec<String>,
1571 #[serde(default, skip_serializing_if = "Option::is_none")]
1576 pub capabilities: Option<CapabilityDeclarations>,
1577 #[serde(default, skip_serializing_if = "Option::is_none")]
1580 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1581}
1582
1583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1585pub struct NotReadyReason {
1586 pub reason: String,
1591 #[serde(default, skip_serializing_if = "Option::is_none")]
1594 pub capability: Option<String>,
1595}
1596
1597impl NotReadyReason {
1598 pub const DECLARED_NOT_READY: &'static str = "declared_not_ready";
1599 pub const REQUIRED_CAPABILITY_UNPROVIDED: &'static str = "required_capability_unprovided";
1600}
1601
1602#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1603pub struct CapabilityRequirementStatus {
1604 pub consumer: String,
1605 pub capability: String,
1606 pub need: String,
1607 pub verdict: String,
1608 pub episode_seq: u64,
1609 pub config_satisfiable: bool,
1610 pub runtime_available: bool,
1611 pub detail: String,
1612}
1613
1614#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1615pub struct SupervisorRescanResult {
1616 pub added: Vec<String>,
1617 pub removed: Vec<String>,
1618 pub changed_pending_reload: Vec<String>,
1619 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1632 pub enabled_changes: Vec<String>,
1633 pub unchanged: u32,
1634 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1642 pub preview: bool,
1643 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1661 pub restart_required: Vec<String>,
1662 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1666 pub capability_warnings: Vec<String>,
1667}
1668
1669#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1685#[serde(rename_all = "snake_case")]
1686pub enum ModuleProtocol {
1687 #[default]
1691 Subc,
1692 None,
1694}
1695
1696#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1697pub struct SupervisorEntry {
1698 pub module_id: String,
1699 pub state: String,
1700 pub enabled: bool,
1701 pub live: bool,
1711 #[serde(default)]
1715 pub protocol: ModuleProtocol,
1716 pub health: SupervisorHealthStatus,
1717 #[serde(default)]
1723 pub last_probe_ms: Option<u64>,
1724 #[serde(default, skip_serializing_if = "Option::is_none")]
1728 pub last_exit_code: Option<i32>,
1729 #[serde(default, skip_serializing_if = "Option::is_none")]
1733 pub last_exit_signal: Option<i32>,
1734 #[serde(default, skip_serializing_if = "Option::is_none")]
1738 pub last_exit_ms: Option<u64>,
1739 #[serde(default, skip_serializing_if = "Option::is_none")]
1742 pub last_exit_kind: Option<TerminalExitKind>,
1743 #[serde(default, skip_serializing_if = "Option::is_none")]
1760 pub restart_count: Option<u32>,
1761 #[serde(default, skip_serializing_if = "Option::is_none")]
1764 pub max_restarts: Option<u32>,
1765 #[serde(default, skip_serializing_if = "Option::is_none")]
1768 pub lifetime_restarts: Option<u32>,
1769 #[serde(default, skip_serializing_if = "Option::is_none")]
1773 pub spawn_generation: Option<u64>,
1774 #[serde(default, skip_serializing_if = "Option::is_none")]
1784 pub restart_window_secs: Option<u64>,
1785 #[serde(default, skip_serializing_if = "Option::is_none")]
1789 pub drain_timeout_ms: Option<u64>,
1790 #[serde(default, skip_serializing_if = "Option::is_none")]
1793 pub restart_backoff_ms: Option<u64>,
1794 #[serde(default, skip_serializing_if = "Option::is_none")]
1797 pub restart_max_backoff_ms: Option<u64>,
1798}
1799
1800#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1801#[serde(rename_all = "snake_case")]
1802pub enum SupervisorHealthStatus {
1803 Ok,
1804 Degraded,
1805 Failing,
1806 Unresponsive,
1807 Unknown,
1808}
1809
1810#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1811pub struct SupervisorHealthEntry {
1812 pub module_id: String,
1813 pub status: SupervisorHealthStatus,
1814 #[serde(default, skip_serializing_if = "Option::is_none")]
1820 pub detail: Option<String>,
1821 #[serde(default, skip_serializing_if = "Option::is_none")]
1826 pub metrics: Option<serde_json::Value>,
1827 pub consecutive_failures: u32,
1828 #[serde(default)]
1831 pub late_answer_count: u64,
1832 #[serde(default, skip_serializing_if = "Option::is_none")]
1834 pub last_late_answer_latency_ms: Option<u64>,
1835 #[serde(default)]
1840 pub last_action: Option<String>,
1841 #[serde(default)]
1844 pub last_action_ms: Option<u64>,
1845 #[serde(default, skip_serializing_if = "Option::is_none")]
1858 pub last_probe_ms: Option<u64>,
1859}
1860
1861#[cfg(test)]
1862mod tests {
1863 use super::*;
1864 use subc_protocol::{BindIdentity, RouteTarget};
1865
1866 #[test]
1867 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
1868 let entry = TerminalEntry {
1869 daemon_incarnation: Some("daemon-before-restart".into()),
1870 exit_code: Some(1),
1871 exit_signal: None,
1872 at_ms: 1_700_000_000_123,
1873 disposition: TerminalDisposition::Restarting,
1874 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
1875 disposition_detail: None,
1876 };
1877 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
1878 assert_eq!(
1879 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
1880 ["exit_kind"],
1881 "deliberate_severance"
1882 );
1883
1884 #[derive(serde::Deserialize)]
1885 struct LegacyTerminalEntry {
1886 exit_code: Option<i32>,
1887 exit_signal: Option<i32>,
1888 at_ms: u64,
1889 disposition: TerminalDisposition,
1890 }
1891
1892 let decoded: LegacyTerminalEntry =
1893 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
1894 assert_eq!(decoded.exit_code, Some(1));
1895 assert_eq!(decoded.exit_signal, None);
1896 assert_eq!(decoded.at_ms, 1_700_000_000_123);
1897 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
1898
1899 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
1900 let future: TerminalEntry =
1901 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
1902 assert_eq!(
1903 future.exit_kind,
1904 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
1905 );
1906 }
1907
1908 #[test]
1909 fn terminal_incarnation_is_optional_for_older_daemons() {
1910 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
1911 "at_ms": 123,
1912 "disposition": "stopped"
1913 }))
1914 .unwrap();
1915 let encoded = serde_json::to_value(&entry).unwrap();
1916 assert_eq!(
1917 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
1918 (None, None)
1919 );
1920 }
1921
1922 #[test]
1923 fn route_poll_uses_kind_field() {
1924 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
1925 route_channel: 7,
1926 route_epoch: 11,
1927 kind: PollKind::Status,
1928 })
1929 .unwrap();
1930
1931 assert_eq!(body["op"], "route.poll");
1932 assert_eq!(body["route_epoch"], 11);
1933 assert_eq!(body["kind"], "status");
1934 assert!(body.get("op").is_some());
1935 }
1936
1937 #[test]
1938 fn route_open_is_internally_tagged() {
1939 let request = ClientControlRequest::RouteOpen {
1940 target: RouteTarget::ToolProvider {
1941 module_id: "aft".to_string(),
1942 },
1943 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
1944 consumer_identity: None,
1945 consumer_capabilities: None,
1946 admission_facts: None,
1947 };
1948
1949 let body = serde_json::to_value(request).unwrap();
1950 assert_eq!(body["op"], "route.open");
1951 assert_eq!(body["target"]["kind"], "tool_provider");
1952 assert!(body.get("consumer_identity").is_none());
1953 assert!(body.get("consumer_capabilities").is_none());
1954 }
1955
1956 #[test]
1957 fn route_open_without_optional_fields_still_decodes() {
1958 let body = serde_json::json!({
1959 "op": "route.open",
1960 "target": { "kind": "tool_provider", "module_id": "aft" },
1961 "identity": {
1962 "project_root": "/tmp/project",
1963 "harness": "opencode",
1964 "session": "session-1"
1965 }
1966 });
1967
1968 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
1969 let ClientControlRequest::RouteOpen {
1970 consumer_identity,
1971 consumer_capabilities,
1972 admission_facts,
1973 ..
1974 } = decoded
1975 else {
1976 panic!("decoded wrong request variant");
1977 };
1978 assert_eq!(consumer_identity, None);
1979 assert_eq!(consumer_capabilities, None);
1980 assert_eq!(admission_facts, None);
1981 }
1982
1983 #[test]
1984 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
1985 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
1986 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
1987 match decoded {
1988 ClientControlPush::RouteClosed {
1989 excluded_subscriptions,
1990 terminal,
1991 ..
1992 } => {
1993 assert_eq!(excluded_subscriptions, 0);
1994 assert_eq!(terminal, None);
1995 }
1996 other => panic!("unexpected push: {other:?}"),
1997 }
1998 assert!(!serde_json::to_string(&decoded)
1999 .unwrap()
2000 .contains("terminal"));
2001 }
2002
2003 #[test]
2004 fn old_route_closed_decoder_ignores_new_terminal_field() {
2005 #[derive(serde::Deserialize)]
2006 #[serde(tag = "op")]
2007 enum LegacyClientControlPush {
2008 #[serde(rename = "route.closed")]
2009 RouteClosed {
2010 module_id: String,
2011 reason: RouteCloseReason,
2012 drained: bool,
2013 abandoned: u32,
2014 },
2015 }
2016
2017 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
2018 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
2019 match decoded {
2020 LegacyClientControlPush::RouteClosed {
2021 module_id,
2022 reason,
2023 drained,
2024 abandoned,
2025 } => {
2026 assert_eq!(module_id, "aft-tools");
2027 assert_eq!(reason, RouteCloseReason::Crash);
2028 assert!(!drained);
2029 assert_eq!(abandoned, 0);
2030 }
2031 }
2032 }
2033
2034 #[test]
2035 fn supervisor_routes_is_a_control_plane_request() {
2036 let body = serde_json::json!({
2037 "op": "supervisor.routes",
2038 "module_id": "aft"
2039 });
2040
2041 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
2042 assert_eq!(serde_json::to_value(request).unwrap(), body);
2043 }
2044
2045 #[test]
2046 fn diagnostic_string_enums_retain_unknown_wire_values() {
2047 let reason: RunningImageUnavailableReason =
2048 serde_json::from_str("\"future_reason\"").unwrap();
2049 let disposition: TerminalDisposition =
2050 serde_json::from_str("\"future_disposition\"").unwrap();
2051
2052 assert_eq!(
2053 reason,
2054 RunningImageUnavailableReason::Unknown("future_reason".to_string())
2055 );
2056 assert_eq!(
2057 disposition,
2058 TerminalDisposition::Unknown("future_disposition".to_string())
2059 );
2060 }
2061
2062 #[test]
2063 fn diagnostic_string_enums_preserve_existing_wire_names() {
2064 let names = [
2065 (RunningImageUnavailableReason::NotRunning, "not_running"),
2066 (
2067 RunningImageUnavailableReason::UnsupportedPlatform,
2068 "unsupported_platform",
2069 ),
2070 (
2071 RunningImageUnavailableReason::RunningExecutableUnreadable,
2072 "running_executable_unreadable",
2073 ),
2074 (
2075 RunningImageUnavailableReason::SpawnedPathUnreadable,
2076 "spawned_path_unreadable",
2077 ),
2078 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2079 (
2080 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2081 "process_identity_unconfirmed",
2082 ),
2083 ];
2084 for (value, expected) in names {
2085 let wire = serde_json::to_string(&value).unwrap();
2086 assert_eq!(wire, format!("\"{expected}\""));
2087 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2088 assert_eq!(decoded, value);
2089 }
2090
2091 for (value, expected) in [
2092 (TerminalDisposition::Stopped, "stopped"),
2093 (TerminalDisposition::Disabled, "disabled"),
2094 (TerminalDisposition::Failed, "failed"),
2095 (TerminalDisposition::Restarting, "restarting"),
2096 ] {
2097 let wire = serde_json::to_string(&value).unwrap();
2098 assert_eq!(wire, format!("\"{expected}\""));
2099 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2100 assert_eq!(decoded, value);
2101 }
2102 }
2103
2104 #[test]
2105 fn diagnostic_string_enums_reject_non_string_bodies() {
2106 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2107 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2108 }
2109
2110 #[test]
2111 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2112 let body = serde_json::json!({
2113 "op": "supervisor.provenance",
2114 "daemon": {
2115 "daemon_build": {},
2116 "daemon_observed": {
2117 "running_image": {
2118 "status": "unavailable",
2119 "reason": "not_running"
2120 }
2121 }
2122 },
2123 "modules": [
2124 {
2125 "module_id": "future",
2126 "module_declared": { "status": "unverifiable" },
2127 "daemon_observed": {
2128 "running_image": {
2129 "status": "unavailable",
2130 "reason": "future_reason"
2131 }
2132 }
2133 },
2134 {
2135 "module_id": "healthy-a",
2136 "module_declared": { "status": "unverifiable" },
2137 "daemon_observed": {
2138 "running_image": {
2139 "status": "match",
2140 "evidence": {
2141 "method": "linux_proc_sha256",
2142 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2143 }
2144 }
2145 }
2146 },
2147 {
2148 "module_id": "healthy-b",
2149 "module_declared": { "status": "unverifiable" },
2150 "daemon_observed": {
2151 "running_image": {
2152 "status": "unavailable",
2153 "reason": "unsupported_platform"
2154 }
2155 }
2156 }
2157 ]
2158 });
2159
2160 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2161 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2162 panic!("decoded wrong response variant");
2163 };
2164 assert_eq!(modules.len(), 3);
2165 assert_eq!(modules[0].module_id, "future");
2166 assert_eq!(
2167 modules[0].daemon_observed.running_image,
2168 RunningImageAgreement::Unavailable {
2169 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2170 }
2171 );
2172 assert_eq!(modules[1].module_id, "healthy-a");
2173 assert_eq!(modules[2].module_id, "healthy-b");
2174 }
2175
2176 #[test]
2177 fn tagged_unknown_values_retain_tag_and_body() {
2178 macro_rules! assert_unknown_round_trip {
2179 ($ty:ident, $field:literal, $value:expr) => {
2180 let value = $value;
2181 let wire = serde_json::to_string(&value).unwrap();
2182 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2183 match decoded {
2184 $ty::Unknown { tag, body } => {
2185 assert_eq!(tag, value[$field].as_str().unwrap());
2186 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2187 }
2188 _ => panic!("decoded known variant"),
2189 }
2190 };
2191 }
2192
2193 assert_unknown_round_trip!(
2194 ModuleDeclaredProvenance,
2195 "status",
2196 serde_json::json!({"status": "future", "build": {"version": 7}})
2197 );
2198 assert_unknown_round_trip!(
2199 RunningImageAgreement,
2200 "status",
2201 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2202 );
2203 assert_unknown_round_trip!(
2204 RunningImageEvidence,
2205 "method",
2206 serde_json::json!({"method": "future", "digest": "abc"})
2207 );
2208 assert_unknown_round_trip!(
2209 SupervisorRouteConsumer,
2210 "kind",
2211 serde_json::json!({"kind": "future", "module_id": "m"})
2212 );
2213 assert_unknown_round_trip!(
2214 StderrCaptureState,
2215 "state",
2216 serde_json::json!({"state": "future", "reason": "because"})
2217 );
2218 assert_unknown_round_trip!(
2219 StderrTailEntry,
2220 "kind",
2221 serde_json::json!({"kind": "future", "text": "line"})
2222 );
2223 }
2224
2225 #[test]
2226 fn tagged_unknown_values_round_trip_the_original_json() {
2227 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2228 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2229 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2230 }
2231
2232 #[test]
2233 fn tagged_unknown_values_round_trip_trailing_tag() {
2234 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2235 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2236 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2237
2238 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2239 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2240 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2241 }
2242
2243 #[test]
2244 fn tagged_unknown_values_round_trip_middle_tag() {
2245 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2246 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2247 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2248
2249 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2250 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2251 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2252 }
2253
2254 #[test]
2255 fn tagged_unknown_values_round_trip_deep_payload() {
2256 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2257 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2258 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2259
2260 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2261 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2262 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2263 }
2264
2265 #[test]
2266 fn tagged_unknown_values_reject_non_object_bodies() {
2267 for wire in ["42", r#""future""#, "[]"] {
2268 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2269 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2270 }
2271 }
2272
2273 #[test]
2274 fn duplicate_discriminators_reject_without_panicking() {
2275 assert_eq!(
2276 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2277 .unwrap(),
2278 ModuleDeclaredProvenance::Unverifiable
2279 );
2280 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2281 .unwrap()
2282 {
2283 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2284 _ => panic!("future discriminator decoded as a known variant"),
2285 }
2286
2287 let wires = [
2288 r#"{"status":"reported","status":"unverifiable"}"#,
2289 r#"{"status":"unverifiable","status":"reported"}"#,
2290 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2291 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2292 ];
2293
2294 for wire in wires {
2295 let result =
2296 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2297 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2298 assert!(
2299 result.unwrap().is_err(),
2300 "duplicate discriminator decoded: {wire}"
2301 );
2302 }
2303
2304 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2305 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2306 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2307 assert!(
2308 result.unwrap().is_err(),
2309 "duplicate discriminator decoded: {wire}"
2310 );
2311 }
2312
2313 #[test]
2314 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2315 let known_wire =
2316 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2317 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2318 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2319
2320 for wire in [
2321 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2322 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2323 ] {
2324 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2325 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2326 }
2327
2328 for wire in [
2329 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2330 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2331 ] {
2332 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2333 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2334 }
2335
2336 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2337 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2338 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2339
2340 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2341 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2342 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2343 }
2344
2345 #[test]
2346 fn tagged_unknown_member_does_not_discard_known_siblings() {
2347 let body = serde_json::json!({
2348 "modules": [{
2349 "module_id": "target",
2350 "routes": [
2351 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2352 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2353 ]
2354 }]
2355 });
2356 let decoded: ClientControlResponse = serde_json::from_value(
2357 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2358 )
2359 .unwrap();
2360 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2361 panic!("decoded wrong response variant");
2362 };
2363 assert_eq!(modules[0].routes.len(), 2);
2364 assert_eq!(
2365 modules[0].routes[1].consumer,
2366 SupervisorRouteConsumer::Direct { connection_id: 7 }
2367 );
2368 }
2369}
2370
2371#[cfg(test)]
2372mod launch_nonce_redaction_tests {
2373 use super::*;
2374
2375 const NONCE: &str = "nonce-f00dfeed1234abcd";
2376
2377 fn identity() -> ConsumerIdentity {
2378 ConsumerIdentity {
2379 module_id: "wernicke".to_string(),
2380 launch_nonce: NONCE.to_string(),
2381 }
2382 }
2383
2384 #[test]
2385 fn consumer_identity_debug_names_the_module_and_never_the_nonce() {
2386 let printed = format!("{:?}", identity());
2387 assert!(printed.contains("wernicke"), "{printed}");
2388 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
2389 }
2390
2391 #[test]
2392 fn route_open_request_debug_never_prints_the_nonce() {
2393 let request = ClientControlRequest::RouteOpen {
2394 target: subc_protocol::RouteTarget::ToolProvider {
2395 module_id: "broca".to_string(),
2396 },
2397 identity: subc_protocol::BindIdentity::new(
2398 PathBuf::from("/tmp/project"),
2399 "test".to_string(),
2400 "session".to_string(),
2401 ),
2402 consumer_identity: Some(identity()),
2403 consumer_capabilities: None,
2404 admission_facts: None,
2405 };
2406 let printed = format!("{request:?}");
2407 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
2408 }
2409}