1#![forbid(unsafe_code)]
9
10use std::path::PathBuf;
11
12use serde::{
13 de::{Error as _, MapAccess, SeqAccess, Visitor},
14 ser::SerializeMap,
15 Deserialize, Deserializer, Serialize, Serializer,
16};
17use subc_protocol::{
18 manifest::{CapabilityDeclarations, ManifestProvenance, ProviderRole, SelfSignalDeclaration},
19 session::HealthStatus,
20 BindIdentity, RouteTarget,
21};
22
23pub use subc_protocol::RouteCloseReason;
24
25macro_rules! open_string_enum {
26 (
27 $(#[$meta:meta])*
28 $name:ident {
29 $( $variant:ident => $wire_name:literal ),+ $(,)?
30 }
31 ) => {
32 $(#[$meta])*
33 #[derive(Debug, Clone, PartialEq, Eq)]
34 pub enum $name {
35 $( $variant, )+
36 Unknown(String),
37 }
38
39 impl $name {
40 fn wire_name(&self) -> &str {
41 match self {
42 $( Self::$variant => $wire_name, )+
43 Self::Unknown(value) => value,
44 }
45 }
46 }
47
48 impl Serialize for $name {
49 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
50 where
51 S: serde::Serializer,
52 {
53 serializer.serialize_str(self.wire_name())
54 }
55 }
56
57 impl<'de> Deserialize<'de> for $name {
58 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59 where
60 D: serde::Deserializer<'de>,
61 {
62 let value = String::deserialize(deserializer)?;
63 Ok(match value.as_str() {
64 $( $wire_name => Self::$variant, )+
65 _ => Self::Unknown(value),
66 })
67 }
68 }
69 };
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
74pub struct ConsumerIdentity {
75 pub module_id: String,
76 pub launch_nonce: String,
77}
78
79pub mod ops {
90 pub const SERVER: &str = "server.";
91 pub const CATALOG: &str = "catalog.";
92 pub const ROUTE: &str = "route.";
93 pub const SUPERVISOR: &str = "supervisor.";
94 pub const CONFIG: &str = "config.";
95
96 pub const SERVER_DESCRIBE: &str = "server.describe";
97 pub const CATALOG_LIST: &str = "catalog.list";
98 pub const ROUTE_OPEN: &str = "route.open";
99 pub const ROUTE_POLL: &str = "route.poll";
100 pub const ROUTE_CLOSING: &str = "route.closing";
101 pub const ROUTE_CLOSED: &str = "route.closed";
102 pub const SUPERVISOR_LIST: &str = "supervisor.list";
103 pub const SUPERVISOR_RESTART: &str = "supervisor.restart";
104 pub const SUPERVISOR_RELOAD: &str = "supervisor.reload";
105 pub const SUPERVISOR_RESCAN: &str = "supervisor.rescan";
106 pub const SUPERVISOR_RELEASE_RESERVED: &str = "supervisor.release_reserved";
107 pub const SUPERVISOR_SET_ENABLED: &str = "supervisor.set_enabled";
108 pub const SUPERVISOR_HEALTH_PROBE: &str = "supervisor.health_probe";
109 pub const SUPERVISOR_HEALTH: &str = "supervisor.health";
110 pub const SUPERVISOR_STDERR_TAIL: &str = "supervisor.stderr_tail";
111 pub const SUPERVISOR_TERMINALS: &str = "supervisor.terminals";
112 pub const SUPERVISOR_ROUTES: &str = "supervisor.routes";
113 pub const SUPERVISOR_PROVENANCE: &str = "supervisor.provenance";
114 pub const SUPERVISOR_SPAWN_SNAPSHOT: &str = "supervisor.spawn_snapshot";
115 pub const SUPERVISOR_SPAWN_SUBSCRIBE: &str = "supervisor.spawn_subscribe";
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
120#[serde(tag = "op")]
121#[allow(clippy::large_enum_variant)]
124pub enum ClientControlRequest {
125 #[serde(rename = "server.describe")]
126 ServerDescribe {},
127 #[serde(rename = "catalog.list")]
128 CatalogList {
129 #[serde(default)]
134 module_id: Option<String>,
135 },
136 #[serde(rename = "route.open")]
137 RouteOpen {
138 target: RouteTarget,
139 identity: BindIdentity,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
149 consumer_identity: Option<ConsumerIdentity>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
158 consumer_capabilities: Option<Vec<String>>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
161 admission_facts: Option<serde_json::Value>,
162 },
163 #[serde(rename = "route.poll")]
164 RoutePoll {
165 route_channel: u16,
166 route_epoch: u32,
167 kind: PollKind,
168 },
169 #[serde(rename = "supervisor.list")]
170 SupervisorList {},
171 #[serde(rename = "supervisor.spawn_snapshot")]
173 SupervisorSpawnSnapshot {},
174 #[serde(rename = "supervisor.spawn_subscribe")]
180 SupervisorSpawnSubscribe {
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 since: Option<SpawnCursor>,
183 },
184 #[serde(rename = "supervisor.restart")]
185 SupervisorRestart {
186 module_id: String,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
195 drain_timeout_ms: Option<u64>,
196 },
197 #[serde(rename = "supervisor.reload")]
198 SupervisorReload { module_id: String },
199 #[serde(rename = "supervisor.rescan")]
200 SupervisorRescan {
201 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
221 preview: bool,
222 },
223 #[serde(rename = "supervisor.release_reserved")]
227 SupervisorReleaseReserved { module_id: String },
228 #[serde(rename = "supervisor.set_enabled")]
229 SupervisorSetEnabled { module_id: String, enabled: bool },
230 #[serde(rename = "supervisor.health_probe")]
231 SupervisorHealthProbe { module_id: String },
232 #[serde(rename = "supervisor.health")]
233 SupervisorHealth {},
234 #[serde(rename = "supervisor.routes")]
247 SupervisorRoutes {
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 module_id: Option<String>,
250 },
251 #[serde(rename = "supervisor.provenance")]
254 SupervisorProvenance {
255 #[serde(default, skip_serializing_if = "Option::is_none")]
256 module_id: Option<String>,
257 },
258 #[serde(rename = "supervisor.stderr_tail")]
266 SupervisorStderrTail {
267 module_id: String,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
269 max_lines: Option<u32>,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
271 max_bytes: Option<u32>,
272 },
273 #[serde(rename = "supervisor.terminals")]
286 SupervisorTerminals { module_id: String },
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
291#[serde(tag = "op")]
292pub enum ClientControlResponse {
293 #[serde(rename = "server.describe")]
294 ServerDescribe {
295 protocol_ver: u8,
296 subc_ops: Vec<String>,
297 capabilities: Vec<String>,
298 connected_clients: u64,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 counters: Option<serde_json::Value>,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
308 build_git_sha: Option<String>,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
315 build_lock_digest: Option<String>,
316 #[serde(default, skip_serializing_if = "Vec::is_empty")]
320 capability_requirements: Vec<CapabilityRequirementStatus>,
321 },
322 #[serde(rename = "catalog.list")]
323 CatalogList {
324 generation: u64,
325 modules: Vec<CatalogEntry>,
326 subc_ops: Vec<String>,
327 },
328 #[serde(rename = "route.open")]
329 RouteOpen {
330 route_channel: u16,
331 route_epoch: u32,
332 },
333 #[serde(rename = "route.poll")]
334 RoutePoll {
335 route_channel: u16,
336 route_epoch: u32,
337 status: Option<String>,
338 live: Option<bool>,
339 },
340 #[serde(rename = "supervisor.list")]
341 SupervisorList {
342 generation: u64,
343 modules: Vec<SupervisorEntry>,
344 },
345 #[serde(rename = "supervisor.spawn_snapshot")]
346 SupervisorSpawnSnapshot {
347 #[serde(flatten)]
348 snapshot: SpawnSnapshot,
349 },
350 #[serde(rename = "supervisor.ack")]
351 SupervisorAck { module_id: String, applied: bool },
352 #[serde(rename = "supervisor.rescan")]
353 SupervisorRescan {
354 #[serde(flatten)]
355 result: SupervisorRescanResult,
356 },
357 #[serde(rename = "supervisor.health_probe")]
358 SupervisorHealthProbe {
359 module_id: String,
360 status: HealthStatus,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 detail: Option<String>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
364 metrics: Option<serde_json::Value>,
365 },
366 #[serde(rename = "supervisor.health")]
367 SupervisorHealth {
368 generation: u64,
369 modules: Vec<SupervisorHealthEntry>,
370 },
371 #[serde(rename = "supervisor.routes")]
372 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
373 #[serde(rename = "supervisor.provenance")]
374 SupervisorProvenance {
375 daemon: SupervisorDaemonProvenance,
376 modules: Vec<SupervisorModuleProvenance>,
377 },
378 #[serde(rename = "supervisor.stderr_tail")]
379 SupervisorStderrTail {
380 module_id: String,
381 #[serde(flatten)]
382 tail: StderrTail,
383 },
384 #[serde(rename = "supervisor.terminals")]
385 SupervisorTerminals {
386 module_id: String,
387 #[serde(flatten)]
388 terminals: TerminalHistory,
389 },
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
397#[serde(tag = "op")]
398pub enum ClientControlPush {
399 #[serde(rename = "route.closing")]
400 RouteClosing {
401 module_id: String,
402 reason: RouteCloseReason,
403 },
404 #[serde(rename = "route.closed")]
405 RouteClosed {
406 module_id: String,
407 reason: RouteCloseReason,
408 drained: bool,
410 abandoned: u32,
413 #[serde(default)]
415 excluded_subscriptions: u32,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
422 terminal: Option<bool>,
423 },
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
428pub struct SpawnCursor {
429 pub daemon_incarnation: String,
430 pub seq: u64,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
435pub struct LiveSpawn {
436 pub module_id: String,
437 pub spawn_generation: u64,
438 pub pid: u32,
439 pub spawned_at_ms: u64,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
444pub struct SpawnSnapshot {
445 pub cursor: SpawnCursor,
446 pub ring_bound: u64,
448 pub live: Vec<LiveSpawn>,
449}
450
451#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
453#[serde(rename_all = "snake_case")]
454pub enum SpawnEventKind {
455 Spawned,
456 Exited,
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
465pub struct SpawnEvent {
466 pub cursor: SpawnCursor,
467 pub kind: SpawnEventKind,
468 pub module_id: String,
469 pub spawn_generation: u64,
470 pub pid: u32,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
472 pub exit_code: Option<i32>,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub exit_signal: Option<i32>,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
479pub struct StderrTail {
480 pub capture: StderrCaptureState,
481 pub entries: Vec<StderrTailEntry>,
482 #[serde(default, skip_serializing_if = "is_zero_u64")]
491 pub dropped_lines: u64,
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
496pub struct SupervisorRouteModule {
497 pub module_id: String,
498 pub routes: Vec<SupervisorRoute>,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
503pub struct SupervisorRoute {
504 pub consumer: SupervisorRouteConsumer,
505 pub age_ms: u64,
507 pub draining: bool,
510 #[serde(default, skip_serializing_if = "Option::is_none")]
516 pub drain_reason: Option<RouteCloseReason>,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
521pub struct SupervisorModuleProvenance {
522 pub module_id: String,
523 pub module_declared: ModuleDeclaredProvenance,
524 pub daemon_observed: SupervisorObservedProcess,
525}
526
527#[derive(Debug, Clone, PartialEq)]
529pub enum ModuleDeclaredProvenance {
530 Reported {
531 build: ManifestProvenance,
532 },
533 Unverifiable,
534 Unknown {
537 tag: String,
538 body: OrderedJsonObject,
539 },
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
547pub struct SupervisorObservedProcess {
548 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub pid: Option<u32>,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
551 pub spawned_at_ms: Option<u64>,
552 #[serde(default, skip_serializing_if = "Option::is_none")]
553 pub spawned_from: Option<PathBuf>,
554 pub running_image: RunningImageAgreement,
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
559pub struct SupervisorDaemonProvenance {
560 pub daemon_build: DaemonBuildProvenance,
561 pub daemon_observed: DaemonObservedProcess,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
566pub struct DaemonBuildProvenance {
567 #[serde(default, skip_serializing_if = "Option::is_none")]
568 pub build_git_sha: Option<String>,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub build_lock_digest: Option<String>,
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
575pub struct DaemonObservedProcess {
576 #[serde(default, skip_serializing_if = "Option::is_none")]
577 pub pid: Option<u32>,
578 #[serde(default, skip_serializing_if = "Option::is_none")]
579 pub started_at_ms: Option<u64>,
580 pub running_image: RunningImageAgreement,
581}
582
583#[derive(Debug, Clone, PartialEq)]
585pub enum RunningImageAgreement {
586 Match {
587 evidence: RunningImageEvidence,
588 },
589 Mismatch {
590 running: RunningImageEvidence,
591 disk: RunningImageEvidence,
592 },
593 Unavailable {
594 reason: RunningImageUnavailableReason,
595 },
596 Unknown {
599 tag: String,
600 body: OrderedJsonObject,
601 },
602}
603
604#[derive(Debug, Clone, PartialEq)]
606pub enum RunningImageEvidence {
607 LinuxProcSha256 {
608 digest: String,
609 },
610 MacosSpawnInode {
611 device: u64,
612 inode: u64,
613 },
614 Unknown {
617 tag: String,
618 body: OrderedJsonObject,
619 },
620}
621
622open_string_enum! {
623 RunningImageUnavailableReason {
625 NotRunning => "not_running",
626 UnsupportedPlatform => "unsupported_platform",
627 RunningExecutableUnreadable => "running_executable_unreadable",
628 SpawnedPathUnreadable => "spawned_path_unreadable",
629 HashFailed => "hash_failed",
630 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
631 }
632}
633
634#[derive(Debug, Clone, PartialEq)]
640pub enum SupervisorRouteConsumer {
641 Reserved {
642 module_id: String,
643 },
644 Direct {
645 connection_id: u64,
646 },
647 Unknown {
650 tag: String,
651 body: OrderedJsonObject,
652 },
653}
654
655#[derive(Debug, Clone, PartialEq)]
662pub enum StderrCaptureState {
663 Captured,
666 Incomplete { reason: String },
668 NotCaptured { reason: String },
670 Unknown {
673 tag: String,
674 body: OrderedJsonObject,
675 },
676}
677
678#[derive(Debug, Clone, PartialEq)]
679pub enum StderrTailEntry {
680 Line {
681 text: String,
682 truncated: bool,
687 },
688 ProcessStart,
693 Unknown {
696 tag: String,
697 body: OrderedJsonObject,
698 },
699}
700
701#[derive(Debug, Serialize, Deserialize)]
702#[serde(tag = "status", rename_all = "snake_case")]
703enum ModuleDeclaredProvenanceWire {
704 Reported { build: ManifestProvenance },
705 Unverifiable,
706}
707
708#[derive(Debug, Serialize, Deserialize)]
709#[serde(tag = "status", rename_all = "snake_case")]
710enum RunningImageAgreementWire {
711 Match {
712 evidence: RunningImageEvidence,
713 },
714 Mismatch {
715 running: RunningImageEvidence,
716 disk: RunningImageEvidence,
717 },
718 Unavailable {
719 reason: RunningImageUnavailableReason,
720 },
721}
722
723#[derive(Debug, Serialize, Deserialize)]
724#[serde(tag = "method", rename_all = "snake_case")]
725enum RunningImageEvidenceWire {
726 LinuxProcSha256 { digest: String },
727 MacosSpawnInode { device: u64, inode: u64 },
728}
729
730#[derive(Debug, Serialize, Deserialize)]
731#[serde(tag = "kind", rename_all = "snake_case")]
732enum SupervisorRouteConsumerWire {
733 Reserved { module_id: String },
734 Direct { connection_id: u64 },
735}
736
737#[derive(Debug, Serialize, Deserialize)]
738#[serde(tag = "state", rename_all = "snake_case")]
739enum StderrCaptureStateWire {
740 Captured,
741 Incomplete { reason: String },
742 NotCaptured { reason: String },
743}
744
745#[derive(Debug, Serialize, Deserialize)]
746#[serde(tag = "kind", rename_all = "snake_case")]
747enum StderrTailEntryWire {
748 Line {
749 text: String,
750 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
751 truncated: bool,
752 },
753 ProcessStart,
754}
755
756#[derive(Debug, Clone, PartialEq)]
758pub enum OrderedJsonValue {
759 Null,
760 Bool(bool),
761 Number(serde_json::Number),
762 String(String),
763 Array(Vec<Self>),
764 Object(OrderedJsonObject),
765}
766
767#[derive(Debug, Clone, PartialEq)]
769pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
770
771impl OrderedJsonObject {
772 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
774 &self.0
775 }
776
777 fn into_value(self) -> serde_json::Value {
778 serde_json::Value::Object(
779 self.0
780 .into_iter()
781 .map(|(key, value)| (key, value.into_value()))
782 .collect(),
783 )
784 }
785}
786
787impl OrderedJsonValue {
788 fn into_value(self) -> serde_json::Value {
789 match self {
790 Self::Null => serde_json::Value::Null,
791 Self::Bool(value) => serde_json::Value::Bool(value),
792 Self::Number(value) => serde_json::Value::Number(value),
793 Self::String(value) => serde_json::Value::String(value),
794 Self::Array(values) => {
795 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
796 }
797 Self::Object(value) => value.into_value(),
798 }
799 }
800}
801
802impl Serialize for OrderedJsonValue {
803 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
804 where
805 S: Serializer,
806 {
807 match self {
808 Self::Null => serializer.serialize_unit(),
809 Self::Bool(value) => serializer.serialize_bool(*value),
810 Self::Number(value) => value.serialize(serializer),
811 Self::String(value) => serializer.serialize_str(value),
812 Self::Array(values) => values.serialize(serializer),
813 Self::Object(value) => value.serialize(serializer),
814 }
815 }
816}
817
818impl<'de> Deserialize<'de> for OrderedJsonValue {
819 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
820 where
821 D: Deserializer<'de>,
822 {
823 struct OrderedValueVisitor;
824
825 impl<'de> Visitor<'de> for OrderedValueVisitor {
826 type Value = OrderedJsonValue;
827
828 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
829 formatter.write_str("a JSON value with ordered object members")
830 }
831
832 fn visit_unit<E>(self) -> Result<Self::Value, E>
833 where
834 E: serde::de::Error,
835 {
836 Ok(OrderedJsonValue::Null)
837 }
838
839 fn visit_none<E>(self) -> Result<Self::Value, E>
840 where
841 E: serde::de::Error,
842 {
843 Ok(OrderedJsonValue::Null)
844 }
845
846 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
847 where
848 D: Deserializer<'de>,
849 {
850 OrderedJsonValue::deserialize(deserializer)
851 }
852
853 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
854 where
855 E: serde::de::Error,
856 {
857 Ok(OrderedJsonValue::Bool(value))
858 }
859
860 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
861 where
862 E: serde::de::Error,
863 {
864 Ok(OrderedJsonValue::Number(value.into()))
865 }
866
867 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
868 where
869 E: serde::de::Error,
870 {
871 Ok(OrderedJsonValue::Number(value.into()))
872 }
873
874 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
875 where
876 E: serde::de::Error,
877 {
878 serde_json::Number::from_f64(value)
879 .map(OrderedJsonValue::Number)
880 .ok_or_else(|| E::custom("non-finite JSON number"))
881 }
882
883 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
884 where
885 E: serde::de::Error,
886 {
887 Ok(OrderedJsonValue::String(value.to_owned()))
888 }
889
890 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
891 where
892 E: serde::de::Error,
893 {
894 Ok(OrderedJsonValue::String(value))
895 }
896
897 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
898 where
899 A: SeqAccess<'de>,
900 {
901 let mut values = Vec::new();
902 while let Some(value) = sequence.next_element()? {
903 values.push(value);
904 }
905 Ok(OrderedJsonValue::Array(values))
906 }
907
908 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
909 where
910 A: MapAccess<'de>,
911 {
912 let mut entries = Vec::new();
913 while let Some((key, value)) = map.next_entry()? {
914 entries.push((key, value));
915 }
916 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
917 }
918 }
919
920 deserializer.deserialize_any(OrderedValueVisitor)
921 }
922}
923
924impl Serialize for OrderedJsonObject {
925 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
926 where
927 S: Serializer,
928 {
929 let mut map = serializer.serialize_map(Some(self.0.len()))?;
930 for (key, value) in &self.0 {
931 map.serialize_entry(key, value)?;
932 }
933 map.end()
934 }
935}
936
937impl<'de> Deserialize<'de> for OrderedJsonObject {
938 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
939 where
940 D: Deserializer<'de>,
941 {
942 struct OrderedObjectVisitor;
943
944 impl<'de> Visitor<'de> for OrderedObjectVisitor {
945 type Value = OrderedJsonObject;
946
947 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
948 formatter.write_str("an object with ordered JSON members")
949 }
950
951 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
952 where
953 A: MapAccess<'de>,
954 {
955 let mut entries = Vec::new();
956 while let Some((key, value)) = map.next_entry()? {
957 entries.push((key, value));
958 }
959 Ok(OrderedJsonObject(entries))
960 }
961 }
962
963 deserializer.deserialize_map(OrderedObjectVisitor)
964 }
965}
966
967fn read_tagged<'de, D>(
968 deserializer: D,
969 field: &'static str,
970) -> Result<(String, OrderedJsonObject), D::Error>
971where
972 D: Deserializer<'de>,
973{
974 let body = OrderedJsonObject::deserialize(deserializer)?;
975 let mut tag = None;
976 for (key, value) in body.as_entries() {
977 if key != field {
978 continue;
979 }
980 if tag.is_some() {
981 return Err(D::Error::custom(format!(
982 "tagged object has duplicate `{field}` field"
983 )));
984 }
985 let OrderedJsonValue::String(value) = value else {
986 return Err(D::Error::custom(format!(
987 "tagged object has no string `{field}` field"
988 )));
989 };
990 tag = Some(value);
991 }
992 let Some(tag) = tag else {
993 return Err(D::Error::custom(format!(
994 "tagged object has no string `{field}` field"
995 )));
996 };
997 Ok((tag.to_string(), body))
998}
999
1000fn read_ordered_tagged(
1001 value: OrderedJsonValue,
1002 field: &'static str,
1003) -> Result<(String, OrderedJsonObject), String> {
1004 let OrderedJsonValue::Object(body) = value else {
1005 return Err(format!("expected tagged object with `{field}` field"));
1006 };
1007 let mut tag = None;
1008 for (key, value) in body.as_entries() {
1009 if key != field {
1010 continue;
1011 }
1012 if tag.is_some() {
1013 return Err(format!("tagged object has duplicate `{field}` field"));
1014 }
1015 let OrderedJsonValue::String(value) = value else {
1016 return Err(format!("tagged object has no string `{field}` field"));
1017 };
1018 tag = Some(value);
1019 }
1020 let Some(tag) = tag else {
1021 return Err(format!("tagged object has no string `{field}` field"));
1022 };
1023 Ok((tag.to_string(), body))
1024}
1025
1026fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1027 body.as_entries()
1028 .iter()
1029 .find_map(|(key, value)| (key == field).then_some(value))
1030}
1031
1032fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1033 match ordered_field(body, field) {
1034 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1035 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1036 None => Err(format!("tagged object has no `{field}` field")),
1037 }
1038}
1039
1040fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1041 let (tag, body) = read_ordered_tagged(value, "method")?;
1042 match tag.as_str() {
1043 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1044 digest: ordered_string(&body, "digest")?,
1045 }),
1046 "macos_spawn_inode" => {
1047 let device = ordered_field(&body, "device")
1048 .and_then(|value| match value {
1049 OrderedJsonValue::Number(number) => number.as_u64(),
1050 _ => None,
1051 })
1052 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1053 let inode = ordered_field(&body, "inode")
1054 .and_then(|value| match value {
1055 OrderedJsonValue::Number(number) => number.as_u64(),
1056 _ => None,
1057 })
1058 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
1059 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1060 }
1061 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1062 }
1063}
1064
1065impl Serialize for ModuleDeclaredProvenance {
1066 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1067 where
1068 S: Serializer,
1069 {
1070 match self {
1071 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1072 build: build.clone(),
1073 }
1074 .serialize(serializer),
1075 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1076 Self::Unknown { body, .. } => body.serialize(serializer),
1077 }
1078 }
1079}
1080
1081impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1082 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1083 where
1084 D: serde::Deserializer<'de>,
1085 {
1086 let (tag, value) = read_tagged(deserializer, "status")?;
1087 match tag.as_str() {
1088 "reported" => match serde_json::from_value(value.into_value())
1089 .map_err(D::Error::custom)?
1090 {
1091 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1092 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1093 },
1094 "unverifiable" => {
1095 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1096 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1097 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1098 }
1099 }
1100 _ => Ok(Self::Unknown { tag, body: value }),
1101 }
1102 }
1103}
1104
1105impl Serialize for RunningImageAgreement {
1106 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1107 where
1108 S: Serializer,
1109 {
1110 match self {
1111 Self::Match { evidence } => RunningImageAgreementWire::Match {
1112 evidence: evidence.clone(),
1113 }
1114 .serialize(serializer),
1115 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1116 running: running.clone(),
1117 disk: disk.clone(),
1118 }
1119 .serialize(serializer),
1120 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1121 reason: reason.clone(),
1122 }
1123 .serialize(serializer),
1124 Self::Unknown { body, .. } => body.serialize(serializer),
1125 }
1126 }
1127}
1128
1129impl<'de> Deserialize<'de> for RunningImageAgreement {
1130 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1131 where
1132 D: serde::Deserializer<'de>,
1133 {
1134 let (tag, value) = read_tagged(deserializer, "status")?;
1135 match tag.as_str() {
1136 "match" => Ok(Self::Match {
1137 evidence: decode_running_image_evidence(
1138 ordered_field(&value, "evidence")
1139 .cloned()
1140 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1141 )
1142 .map_err(D::Error::custom)?,
1143 }),
1144 "mismatch" => Ok(Self::Mismatch {
1145 running: decode_running_image_evidence(
1146 ordered_field(&value, "running")
1147 .cloned()
1148 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1149 )
1150 .map_err(D::Error::custom)?,
1151 disk: decode_running_image_evidence(
1152 ordered_field(&value, "disk")
1153 .cloned()
1154 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1155 )
1156 .map_err(D::Error::custom)?,
1157 }),
1158 "unavailable" => Ok(Self::Unavailable {
1159 reason: serde_json::from_value(
1160 ordered_field(&value, "reason")
1161 .cloned()
1162 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1163 .into_value(),
1164 )
1165 .map_err(D::Error::custom)?,
1166 }),
1167 _ => Ok(Self::Unknown { tag, body: value }),
1168 }
1169 }
1170}
1171
1172impl Serialize for RunningImageEvidence {
1173 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1174 where
1175 S: Serializer,
1176 {
1177 match self {
1178 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1179 digest: digest.clone(),
1180 }
1181 .serialize(serializer),
1182 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1183 device: *device,
1184 inode: *inode,
1185 }
1186 .serialize(serializer),
1187 Self::Unknown { body, .. } => body.serialize(serializer),
1188 }
1189 }
1190}
1191
1192impl<'de> Deserialize<'de> for RunningImageEvidence {
1193 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1194 where
1195 D: serde::Deserializer<'de>,
1196 {
1197 let (tag, value) = read_tagged(deserializer, "method")?;
1198 match tag.as_str() {
1199 "linux_proc_sha256" => {
1200 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1201 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1202 Ok(Self::LinuxProcSha256 { digest })
1203 }
1204 _ => unreachable!(),
1205 }
1206 }
1207 "macos_spawn_inode" => {
1208 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1209 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1210 Ok(Self::MacosSpawnInode { device, inode })
1211 }
1212 _ => unreachable!(),
1213 }
1214 }
1215 _ => Ok(Self::Unknown { tag, body: value }),
1216 }
1217 }
1218}
1219
1220impl Serialize for SupervisorRouteConsumer {
1221 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1222 where
1223 S: Serializer,
1224 {
1225 match self {
1226 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1227 module_id: module_id.clone(),
1228 }
1229 .serialize(serializer),
1230 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1231 connection_id: *connection_id,
1232 }
1233 .serialize(serializer),
1234 Self::Unknown { body, .. } => body.serialize(serializer),
1235 }
1236 }
1237}
1238
1239impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1240 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1241 where
1242 D: serde::Deserializer<'de>,
1243 {
1244 let (tag, value) = read_tagged(deserializer, "kind")?;
1245 match tag.as_str() {
1246 "reserved" => {
1247 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1248 SupervisorRouteConsumerWire::Reserved { module_id } => {
1249 Ok(Self::Reserved { module_id })
1250 }
1251 _ => unreachable!(),
1252 }
1253 }
1254 "direct" => {
1255 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1256 SupervisorRouteConsumerWire::Direct { connection_id } => {
1257 Ok(Self::Direct { connection_id })
1258 }
1259 _ => unreachable!(),
1260 }
1261 }
1262 _ => Ok(Self::Unknown { tag, body: value }),
1263 }
1264 }
1265}
1266
1267impl Serialize for StderrCaptureState {
1268 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1269 where
1270 S: Serializer,
1271 {
1272 match self {
1273 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1274 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1275 reason: reason.clone(),
1276 }
1277 .serialize(serializer),
1278 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1279 reason: reason.clone(),
1280 }
1281 .serialize(serializer),
1282 Self::Unknown { body, .. } => body.serialize(serializer),
1283 }
1284 }
1285}
1286
1287impl<'de> Deserialize<'de> for StderrCaptureState {
1288 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1289 where
1290 D: serde::Deserializer<'de>,
1291 {
1292 let (tag, value) = read_tagged(deserializer, "state")?;
1293 match tag.as_str() {
1294 "captured" => {
1295 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1296 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1297 _ => unreachable!(),
1298 }
1299 }
1300 "incomplete" => match serde_json::from_value(value.into_value())
1301 .map_err(D::Error::custom)?
1302 {
1303 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1304 _ => unreachable!(),
1305 },
1306 "not_captured" => match serde_json::from_value(value.into_value())
1307 .map_err(D::Error::custom)?
1308 {
1309 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1310 _ => unreachable!(),
1311 },
1312 _ => Ok(Self::Unknown { tag, body: value }),
1313 }
1314 }
1315}
1316
1317impl Serialize for StderrTailEntry {
1318 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1319 where
1320 S: Serializer,
1321 {
1322 match self {
1323 Self::Line { text, truncated } => StderrTailEntryWire::Line {
1324 text: text.clone(),
1325 truncated: *truncated,
1326 }
1327 .serialize(serializer),
1328 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1329 Self::Unknown { body, .. } => body.serialize(serializer),
1330 }
1331 }
1332}
1333
1334impl<'de> Deserialize<'de> for StderrTailEntry {
1335 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1336 where
1337 D: serde::Deserializer<'de>,
1338 {
1339 let (tag, value) = read_tagged(deserializer, "kind")?;
1340 match tag.as_str() {
1341 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1342 StderrTailEntryWire::Line { text, truncated } => Ok(Self::Line { text, truncated }),
1343 _ => unreachable!(),
1344 },
1345 "process_start" => {
1346 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1347 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1348 _ => unreachable!(),
1349 }
1350 }
1351 _ => Ok(Self::Unknown { tag, body: value }),
1352 }
1353 }
1354}
1355
1356fn is_zero_u64(value: &u64) -> bool {
1357 *value == 0
1358}
1359
1360fn default_true() -> bool {
1361 true
1362}
1363
1364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1366pub struct TerminalHistory {
1367 pub daemon_started_at_ms: u64,
1369 pub entries: Vec<TerminalEntry>,
1370 #[serde(default, skip_serializing_if = "is_zero_u64")]
1373 pub dropped: u64,
1374 #[serde(default, skip_serializing_if = "is_zero_u64")]
1377 pub journal_skipped_lines: u64,
1378 #[serde(default, skip_serializing_if = "is_zero_u64")]
1380 pub journal_read_errors: u64,
1381 #[serde(default, skip_serializing_if = "is_zero_u64")]
1383 pub journal_write_failures: u64,
1384}
1385
1386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1388pub struct TerminalEntry {
1389 #[serde(default, skip_serializing_if = "Option::is_none")]
1392 pub daemon_incarnation: Option<String>,
1393 #[serde(default, skip_serializing_if = "Option::is_none")]
1394 pub exit_code: Option<i32>,
1395 #[serde(default, skip_serializing_if = "Option::is_none")]
1396 pub exit_signal: Option<i32>,
1397 pub at_ms: u64,
1398 pub disposition: TerminalDisposition,
1399 #[serde(default, skip_serializing_if = "Option::is_none")]
1403 pub exit_kind: Option<TerminalExitKind>,
1404 #[serde(default, skip_serializing_if = "Option::is_none")]
1411 pub disposition_detail: Option<String>,
1412}
1413
1414#[derive(Debug, Clone, PartialEq, Eq)]
1419pub enum TerminalExitKind {
1420 Clean,
1421 Crash,
1422 DeliberateSeverance,
1423 Unknown(String),
1424}
1425
1426impl TerminalExitKind {
1427 fn wire_name(&self) -> &str {
1428 match self {
1429 Self::Clean => "clean",
1430 Self::Crash => "crash",
1431 Self::DeliberateSeverance => "deliberate_severance",
1432 Self::Unknown(value) => value,
1433 }
1434 }
1435}
1436
1437impl Serialize for TerminalExitKind {
1438 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1439 where
1440 S: serde::Serializer,
1441 {
1442 serializer.serialize_str(self.wire_name())
1443 }
1444}
1445
1446impl<'de> Deserialize<'de> for TerminalExitKind {
1447 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1448 where
1449 D: serde::Deserializer<'de>,
1450 {
1451 let value = String::deserialize(deserializer)?;
1452 Ok(match value.as_str() {
1453 "clean" => Self::Clean,
1454 "crash" => Self::Crash,
1455 "deliberate_severance" => Self::DeliberateSeverance,
1456 _ => Self::Unknown(value),
1457 })
1458 }
1459}
1460
1461open_string_enum! {
1462 TerminalDisposition {
1464 Stopped => "stopped",
1465 Disabled => "disabled",
1466 Failed => "failed",
1467 Restarting => "restarting",
1468 }
1469}
1470
1471#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1472#[serde(rename_all = "snake_case")]
1473pub enum PollKind {
1474 Status,
1475 Liveness,
1476}
1477
1478#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1479pub struct CatalogEntry {
1480 pub module_id: String,
1481 #[serde(default = "default_true")]
1485 pub ready: bool,
1486 #[serde(default, skip_serializing_if = "Option::is_none")]
1507 pub module_version: Option<String>,
1508 pub roles: Vec<ProviderRole>,
1509 pub control_ops: Vec<String>,
1510 #[serde(default, skip_serializing_if = "Option::is_none")]
1515 pub capabilities: Option<CapabilityDeclarations>,
1516 #[serde(default, skip_serializing_if = "Option::is_none")]
1519 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1520}
1521
1522#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1523pub struct CapabilityRequirementStatus {
1524 pub consumer: String,
1525 pub capability: String,
1526 pub need: String,
1527 pub verdict: String,
1528 pub episode_seq: u64,
1529 pub config_satisfiable: bool,
1530 pub runtime_available: bool,
1531 pub detail: String,
1532}
1533
1534#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1535pub struct SupervisorRescanResult {
1536 pub added: Vec<String>,
1537 pub removed: Vec<String>,
1538 pub changed_pending_reload: Vec<String>,
1539 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1552 pub enabled_changes: Vec<String>,
1553 pub unchanged: u32,
1554 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1562 pub preview: bool,
1563 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1581 pub restart_required: Vec<String>,
1582 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1586 pub capability_warnings: Vec<String>,
1587}
1588
1589#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1605#[serde(rename_all = "snake_case")]
1606pub enum ModuleProtocol {
1607 #[default]
1611 Subc,
1612 None,
1614}
1615
1616#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1617pub struct SupervisorEntry {
1618 pub module_id: String,
1619 pub state: String,
1620 pub enabled: bool,
1621 pub live: bool,
1631 #[serde(default)]
1635 pub protocol: ModuleProtocol,
1636 pub health: SupervisorHealthStatus,
1637 #[serde(default)]
1643 pub last_probe_ms: Option<u64>,
1644 #[serde(default, skip_serializing_if = "Option::is_none")]
1648 pub last_exit_code: Option<i32>,
1649 #[serde(default, skip_serializing_if = "Option::is_none")]
1653 pub last_exit_signal: Option<i32>,
1654 #[serde(default, skip_serializing_if = "Option::is_none")]
1658 pub last_exit_ms: Option<u64>,
1659 #[serde(default, skip_serializing_if = "Option::is_none")]
1662 pub last_exit_kind: Option<TerminalExitKind>,
1663 #[serde(default, skip_serializing_if = "Option::is_none")]
1680 pub restart_count: Option<u32>,
1681 #[serde(default, skip_serializing_if = "Option::is_none")]
1684 pub max_restarts: Option<u32>,
1685 #[serde(default, skip_serializing_if = "Option::is_none")]
1688 pub lifetime_restarts: Option<u32>,
1689 #[serde(default, skip_serializing_if = "Option::is_none")]
1693 pub spawn_generation: Option<u64>,
1694 #[serde(default, skip_serializing_if = "Option::is_none")]
1704 pub restart_window_secs: Option<u64>,
1705 #[serde(default, skip_serializing_if = "Option::is_none")]
1709 pub drain_timeout_ms: Option<u64>,
1710 #[serde(default, skip_serializing_if = "Option::is_none")]
1713 pub restart_backoff_ms: Option<u64>,
1714 #[serde(default, skip_serializing_if = "Option::is_none")]
1717 pub restart_max_backoff_ms: Option<u64>,
1718}
1719
1720#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1721#[serde(rename_all = "snake_case")]
1722pub enum SupervisorHealthStatus {
1723 Ok,
1724 Degraded,
1725 Failing,
1726 Unresponsive,
1727 Unknown,
1728}
1729
1730#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1731pub struct SupervisorHealthEntry {
1732 pub module_id: String,
1733 pub status: SupervisorHealthStatus,
1734 #[serde(default, skip_serializing_if = "Option::is_none")]
1740 pub detail: Option<String>,
1741 #[serde(default, skip_serializing_if = "Option::is_none")]
1746 pub metrics: Option<serde_json::Value>,
1747 pub consecutive_failures: u32,
1748 #[serde(default)]
1751 pub late_answer_count: u64,
1752 #[serde(default, skip_serializing_if = "Option::is_none")]
1754 pub last_late_answer_latency_ms: Option<u64>,
1755 #[serde(default)]
1760 pub last_action: Option<String>,
1761 #[serde(default)]
1764 pub last_action_ms: Option<u64>,
1765 #[serde(default, skip_serializing_if = "Option::is_none")]
1778 pub last_probe_ms: Option<u64>,
1779}
1780
1781#[cfg(test)]
1782mod tests {
1783 use super::*;
1784 use subc_protocol::{BindIdentity, RouteTarget};
1785
1786 #[test]
1787 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
1788 let entry = TerminalEntry {
1789 daemon_incarnation: Some("daemon-before-restart".into()),
1790 exit_code: Some(1),
1791 exit_signal: None,
1792 at_ms: 1_700_000_000_123,
1793 disposition: TerminalDisposition::Restarting,
1794 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
1795 disposition_detail: None,
1796 };
1797 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
1798 assert_eq!(
1799 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
1800 ["exit_kind"],
1801 "deliberate_severance"
1802 );
1803
1804 #[derive(serde::Deserialize)]
1805 struct LegacyTerminalEntry {
1806 exit_code: Option<i32>,
1807 exit_signal: Option<i32>,
1808 at_ms: u64,
1809 disposition: TerminalDisposition,
1810 }
1811
1812 let decoded: LegacyTerminalEntry =
1813 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
1814 assert_eq!(decoded.exit_code, Some(1));
1815 assert_eq!(decoded.exit_signal, None);
1816 assert_eq!(decoded.at_ms, 1_700_000_000_123);
1817 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
1818
1819 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
1820 let future: TerminalEntry =
1821 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
1822 assert_eq!(
1823 future.exit_kind,
1824 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
1825 );
1826 }
1827
1828 #[test]
1829 fn terminal_incarnation_is_optional_for_older_daemons() {
1830 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
1831 "at_ms": 123,
1832 "disposition": "stopped"
1833 }))
1834 .unwrap();
1835 let encoded = serde_json::to_value(&entry).unwrap();
1836 assert_eq!(
1837 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
1838 (None, None)
1839 );
1840 }
1841
1842 #[test]
1843 fn route_poll_uses_kind_field() {
1844 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
1845 route_channel: 7,
1846 route_epoch: 11,
1847 kind: PollKind::Status,
1848 })
1849 .unwrap();
1850
1851 assert_eq!(body["op"], "route.poll");
1852 assert_eq!(body["route_epoch"], 11);
1853 assert_eq!(body["kind"], "status");
1854 assert!(body.get("op").is_some());
1855 }
1856
1857 #[test]
1858 fn route_open_is_internally_tagged() {
1859 let request = ClientControlRequest::RouteOpen {
1860 target: RouteTarget::ToolProvider {
1861 module_id: "aft".to_string(),
1862 },
1863 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
1864 consumer_identity: None,
1865 consumer_capabilities: None,
1866 admission_facts: None,
1867 };
1868
1869 let body = serde_json::to_value(request).unwrap();
1870 assert_eq!(body["op"], "route.open");
1871 assert_eq!(body["target"]["kind"], "tool_provider");
1872 assert!(body.get("consumer_identity").is_none());
1873 assert!(body.get("consumer_capabilities").is_none());
1874 }
1875
1876 #[test]
1877 fn route_open_without_optional_fields_still_decodes() {
1878 let body = serde_json::json!({
1879 "op": "route.open",
1880 "target": { "kind": "tool_provider", "module_id": "aft" },
1881 "identity": {
1882 "project_root": "/tmp/project",
1883 "harness": "opencode",
1884 "session": "session-1"
1885 }
1886 });
1887
1888 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
1889 let ClientControlRequest::RouteOpen {
1890 consumer_identity,
1891 consumer_capabilities,
1892 admission_facts,
1893 ..
1894 } = decoded
1895 else {
1896 panic!("decoded wrong request variant");
1897 };
1898 assert_eq!(consumer_identity, None);
1899 assert_eq!(consumer_capabilities, None);
1900 assert_eq!(admission_facts, None);
1901 }
1902
1903 #[test]
1904 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
1905 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
1906 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
1907 match decoded {
1908 ClientControlPush::RouteClosed {
1909 excluded_subscriptions,
1910 terminal,
1911 ..
1912 } => {
1913 assert_eq!(excluded_subscriptions, 0);
1914 assert_eq!(terminal, None);
1915 }
1916 other => panic!("unexpected push: {other:?}"),
1917 }
1918 assert!(!serde_json::to_string(&decoded)
1919 .unwrap()
1920 .contains("terminal"));
1921 }
1922
1923 #[test]
1924 fn old_route_closed_decoder_ignores_new_terminal_field() {
1925 #[derive(serde::Deserialize)]
1926 #[serde(tag = "op")]
1927 enum LegacyClientControlPush {
1928 #[serde(rename = "route.closed")]
1929 RouteClosed {
1930 module_id: String,
1931 reason: RouteCloseReason,
1932 drained: bool,
1933 abandoned: u32,
1934 },
1935 }
1936
1937 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
1938 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
1939 match decoded {
1940 LegacyClientControlPush::RouteClosed {
1941 module_id,
1942 reason,
1943 drained,
1944 abandoned,
1945 } => {
1946 assert_eq!(module_id, "aft-tools");
1947 assert_eq!(reason, RouteCloseReason::Crash);
1948 assert!(!drained);
1949 assert_eq!(abandoned, 0);
1950 }
1951 }
1952 }
1953
1954 #[test]
1955 fn supervisor_routes_is_a_control_plane_request() {
1956 let body = serde_json::json!({
1957 "op": "supervisor.routes",
1958 "module_id": "aft"
1959 });
1960
1961 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
1962 assert_eq!(serde_json::to_value(request).unwrap(), body);
1963 }
1964
1965 #[test]
1966 fn diagnostic_string_enums_retain_unknown_wire_values() {
1967 let reason: RunningImageUnavailableReason =
1968 serde_json::from_str("\"future_reason\"").unwrap();
1969 let disposition: TerminalDisposition =
1970 serde_json::from_str("\"future_disposition\"").unwrap();
1971
1972 assert_eq!(
1973 reason,
1974 RunningImageUnavailableReason::Unknown("future_reason".to_string())
1975 );
1976 assert_eq!(
1977 disposition,
1978 TerminalDisposition::Unknown("future_disposition".to_string())
1979 );
1980 }
1981
1982 #[test]
1983 fn diagnostic_string_enums_preserve_existing_wire_names() {
1984 let names = [
1985 (RunningImageUnavailableReason::NotRunning, "not_running"),
1986 (
1987 RunningImageUnavailableReason::UnsupportedPlatform,
1988 "unsupported_platform",
1989 ),
1990 (
1991 RunningImageUnavailableReason::RunningExecutableUnreadable,
1992 "running_executable_unreadable",
1993 ),
1994 (
1995 RunningImageUnavailableReason::SpawnedPathUnreadable,
1996 "spawned_path_unreadable",
1997 ),
1998 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
1999 (
2000 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2001 "process_identity_unconfirmed",
2002 ),
2003 ];
2004 for (value, expected) in names {
2005 let wire = serde_json::to_string(&value).unwrap();
2006 assert_eq!(wire, format!("\"{expected}\""));
2007 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2008 assert_eq!(decoded, value);
2009 }
2010
2011 for (value, expected) in [
2012 (TerminalDisposition::Stopped, "stopped"),
2013 (TerminalDisposition::Disabled, "disabled"),
2014 (TerminalDisposition::Failed, "failed"),
2015 (TerminalDisposition::Restarting, "restarting"),
2016 ] {
2017 let wire = serde_json::to_string(&value).unwrap();
2018 assert_eq!(wire, format!("\"{expected}\""));
2019 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2020 assert_eq!(decoded, value);
2021 }
2022 }
2023
2024 #[test]
2025 fn diagnostic_string_enums_reject_non_string_bodies() {
2026 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2027 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2028 }
2029
2030 #[test]
2031 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2032 let body = serde_json::json!({
2033 "op": "supervisor.provenance",
2034 "daemon": {
2035 "daemon_build": {},
2036 "daemon_observed": {
2037 "running_image": {
2038 "status": "unavailable",
2039 "reason": "not_running"
2040 }
2041 }
2042 },
2043 "modules": [
2044 {
2045 "module_id": "future",
2046 "module_declared": { "status": "unverifiable" },
2047 "daemon_observed": {
2048 "running_image": {
2049 "status": "unavailable",
2050 "reason": "future_reason"
2051 }
2052 }
2053 },
2054 {
2055 "module_id": "healthy-a",
2056 "module_declared": { "status": "unverifiable" },
2057 "daemon_observed": {
2058 "running_image": {
2059 "status": "match",
2060 "evidence": {
2061 "method": "linux_proc_sha256",
2062 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2063 }
2064 }
2065 }
2066 },
2067 {
2068 "module_id": "healthy-b",
2069 "module_declared": { "status": "unverifiable" },
2070 "daemon_observed": {
2071 "running_image": {
2072 "status": "unavailable",
2073 "reason": "unsupported_platform"
2074 }
2075 }
2076 }
2077 ]
2078 });
2079
2080 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2081 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2082 panic!("decoded wrong response variant");
2083 };
2084 assert_eq!(modules.len(), 3);
2085 assert_eq!(modules[0].module_id, "future");
2086 assert_eq!(
2087 modules[0].daemon_observed.running_image,
2088 RunningImageAgreement::Unavailable {
2089 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2090 }
2091 );
2092 assert_eq!(modules[1].module_id, "healthy-a");
2093 assert_eq!(modules[2].module_id, "healthy-b");
2094 }
2095
2096 #[test]
2097 fn tagged_unknown_values_retain_tag_and_body() {
2098 macro_rules! assert_unknown_round_trip {
2099 ($ty:ident, $field:literal, $value:expr) => {
2100 let value = $value;
2101 let wire = serde_json::to_string(&value).unwrap();
2102 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2103 match decoded {
2104 $ty::Unknown { tag, body } => {
2105 assert_eq!(tag, value[$field].as_str().unwrap());
2106 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2107 }
2108 _ => panic!("decoded known variant"),
2109 }
2110 };
2111 }
2112
2113 assert_unknown_round_trip!(
2114 ModuleDeclaredProvenance,
2115 "status",
2116 serde_json::json!({"status": "future", "build": {"version": 7}})
2117 );
2118 assert_unknown_round_trip!(
2119 RunningImageAgreement,
2120 "status",
2121 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2122 );
2123 assert_unknown_round_trip!(
2124 RunningImageEvidence,
2125 "method",
2126 serde_json::json!({"method": "future", "digest": "abc"})
2127 );
2128 assert_unknown_round_trip!(
2129 SupervisorRouteConsumer,
2130 "kind",
2131 serde_json::json!({"kind": "future", "module_id": "m"})
2132 );
2133 assert_unknown_round_trip!(
2134 StderrCaptureState,
2135 "state",
2136 serde_json::json!({"state": "future", "reason": "because"})
2137 );
2138 assert_unknown_round_trip!(
2139 StderrTailEntry,
2140 "kind",
2141 serde_json::json!({"kind": "future", "text": "line"})
2142 );
2143 }
2144
2145 #[test]
2146 fn tagged_unknown_values_round_trip_the_original_json() {
2147 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2148 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2149 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2150 }
2151
2152 #[test]
2153 fn tagged_unknown_values_round_trip_trailing_tag() {
2154 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2155 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2156 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2157
2158 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2159 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2160 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2161 }
2162
2163 #[test]
2164 fn tagged_unknown_values_round_trip_middle_tag() {
2165 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2166 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2167 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2168
2169 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2170 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2171 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2172 }
2173
2174 #[test]
2175 fn tagged_unknown_values_round_trip_deep_payload() {
2176 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2177 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2178 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2179
2180 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2181 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2182 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2183 }
2184
2185 #[test]
2186 fn tagged_unknown_values_reject_non_object_bodies() {
2187 for wire in ["42", r#""future""#, "[]"] {
2188 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2189 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2190 }
2191 }
2192
2193 #[test]
2194 fn duplicate_discriminators_reject_without_panicking() {
2195 assert_eq!(
2196 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2197 .unwrap(),
2198 ModuleDeclaredProvenance::Unverifiable
2199 );
2200 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2201 .unwrap()
2202 {
2203 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2204 _ => panic!("future discriminator decoded as a known variant"),
2205 }
2206
2207 let wires = [
2208 r#"{"status":"reported","status":"unverifiable"}"#,
2209 r#"{"status":"unverifiable","status":"reported"}"#,
2210 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2211 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2212 ];
2213
2214 for wire in wires {
2215 let result =
2216 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2217 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2218 assert!(
2219 result.unwrap().is_err(),
2220 "duplicate discriminator decoded: {wire}"
2221 );
2222 }
2223
2224 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2225 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2226 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2227 assert!(
2228 result.unwrap().is_err(),
2229 "duplicate discriminator decoded: {wire}"
2230 );
2231 }
2232
2233 #[test]
2234 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2235 let known_wire =
2236 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2237 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2238 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2239
2240 for wire in [
2241 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2242 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2243 ] {
2244 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2245 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2246 }
2247
2248 for wire in [
2249 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2250 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2251 ] {
2252 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2253 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2254 }
2255
2256 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2257 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2258 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2259
2260 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2261 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2262 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2263 }
2264
2265 #[test]
2266 fn tagged_unknown_member_does_not_discard_known_siblings() {
2267 let body = serde_json::json!({
2268 "modules": [{
2269 "module_id": "target",
2270 "routes": [
2271 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2272 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2273 ]
2274 }]
2275 });
2276 let decoded: ClientControlResponse = serde_json::from_value(
2277 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2278 )
2279 .unwrap();
2280 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2281 panic!("decoded wrong response variant");
2282 };
2283 assert_eq!(modules[0].routes.len(), 2);
2284 assert_eq!(
2285 modules[0].routes[1].consumer,
2286 SupervisorRouteConsumer::Direct { connection_id: 7 }
2287 );
2288 }
2289}