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")]
583 pub started_at_ms: Option<u64>,
584 pub running_image: RunningImageAgreement,
585}
586
587#[derive(Debug, Clone, PartialEq)]
589pub enum RunningImageAgreement {
590 Match {
591 evidence: RunningImageEvidence,
592 },
593 Mismatch {
594 running: RunningImageEvidence,
595 disk: RunningImageEvidence,
596 },
597 Unavailable {
598 reason: RunningImageUnavailableReason,
599 },
600 Unknown {
603 tag: String,
604 body: OrderedJsonObject,
605 },
606}
607
608#[derive(Debug, Clone, PartialEq)]
610pub enum RunningImageEvidence {
611 LinuxProcSha256 {
612 digest: String,
613 },
614 MacosSpawnInode {
615 device: u64,
616 inode: u64,
617 },
618 Unknown {
621 tag: String,
622 body: OrderedJsonObject,
623 },
624}
625
626open_string_enum! {
627 RunningImageUnavailableReason {
629 NotRunning => "not_running",
630 UnsupportedPlatform => "unsupported_platform",
631 RunningExecutableUnreadable => "running_executable_unreadable",
632 SpawnedPathUnreadable => "spawned_path_unreadable",
633 HashFailed => "hash_failed",
634 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
635 }
636}
637
638#[derive(Debug, Clone, PartialEq)]
644pub enum SupervisorRouteConsumer {
645 Reserved {
646 module_id: String,
647 },
648 Direct {
649 connection_id: u64,
650 },
651 Unknown {
654 tag: String,
655 body: OrderedJsonObject,
656 },
657}
658
659#[derive(Debug, Clone, PartialEq)]
666pub enum StderrCaptureState {
667 Captured,
670 Incomplete { reason: String },
672 NotCaptured { reason: String },
674 Unknown {
677 tag: String,
678 body: OrderedJsonObject,
679 },
680}
681
682#[derive(Debug, Clone, PartialEq)]
683pub enum StderrTailEntry {
684 Line {
685 text: String,
686 truncated: bool,
691 },
692 ProcessStart,
697 Unknown {
700 tag: String,
701 body: OrderedJsonObject,
702 },
703}
704
705#[derive(Debug, Serialize, Deserialize)]
706#[serde(tag = "status", rename_all = "snake_case")]
707enum ModuleDeclaredProvenanceWire {
708 Reported { build: ManifestProvenance },
709 Unverifiable,
710}
711
712#[derive(Debug, Serialize, Deserialize)]
713#[serde(tag = "status", rename_all = "snake_case")]
714enum RunningImageAgreementWire {
715 Match {
716 evidence: RunningImageEvidence,
717 },
718 Mismatch {
719 running: RunningImageEvidence,
720 disk: RunningImageEvidence,
721 },
722 Unavailable {
723 reason: RunningImageUnavailableReason,
724 },
725}
726
727#[derive(Debug, Serialize, Deserialize)]
728#[serde(tag = "method", rename_all = "snake_case")]
729enum RunningImageEvidenceWire {
730 LinuxProcSha256 { digest: String },
731 MacosSpawnInode { device: u64, inode: u64 },
732}
733
734#[derive(Debug, Serialize, Deserialize)]
735#[serde(tag = "kind", rename_all = "snake_case")]
736enum SupervisorRouteConsumerWire {
737 Reserved { module_id: String },
738 Direct { connection_id: u64 },
739}
740
741#[derive(Debug, Serialize, Deserialize)]
742#[serde(tag = "state", rename_all = "snake_case")]
743enum StderrCaptureStateWire {
744 Captured,
745 Incomplete { reason: String },
746 NotCaptured { reason: String },
747}
748
749#[derive(Debug, Serialize, Deserialize)]
750#[serde(tag = "kind", rename_all = "snake_case")]
751enum StderrTailEntryWire {
752 Line {
753 text: String,
754 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
755 truncated: bool,
756 },
757 ProcessStart,
758}
759
760#[derive(Debug, Clone, PartialEq)]
762pub enum OrderedJsonValue {
763 Null,
764 Bool(bool),
765 Number(serde_json::Number),
766 String(String),
767 Array(Vec<Self>),
768 Object(OrderedJsonObject),
769}
770
771#[derive(Debug, Clone, PartialEq)]
773pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
774
775impl OrderedJsonObject {
776 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
778 &self.0
779 }
780
781 fn into_value(self) -> serde_json::Value {
782 serde_json::Value::Object(
783 self.0
784 .into_iter()
785 .map(|(key, value)| (key, value.into_value()))
786 .collect(),
787 )
788 }
789}
790
791impl OrderedJsonValue {
792 fn into_value(self) -> serde_json::Value {
793 match self {
794 Self::Null => serde_json::Value::Null,
795 Self::Bool(value) => serde_json::Value::Bool(value),
796 Self::Number(value) => serde_json::Value::Number(value),
797 Self::String(value) => serde_json::Value::String(value),
798 Self::Array(values) => {
799 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
800 }
801 Self::Object(value) => value.into_value(),
802 }
803 }
804}
805
806impl Serialize for OrderedJsonValue {
807 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
808 where
809 S: Serializer,
810 {
811 match self {
812 Self::Null => serializer.serialize_unit(),
813 Self::Bool(value) => serializer.serialize_bool(*value),
814 Self::Number(value) => value.serialize(serializer),
815 Self::String(value) => serializer.serialize_str(value),
816 Self::Array(values) => values.serialize(serializer),
817 Self::Object(value) => value.serialize(serializer),
818 }
819 }
820}
821
822impl<'de> Deserialize<'de> for OrderedJsonValue {
823 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
824 where
825 D: Deserializer<'de>,
826 {
827 struct OrderedValueVisitor;
828
829 impl<'de> Visitor<'de> for OrderedValueVisitor {
830 type Value = OrderedJsonValue;
831
832 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
833 formatter.write_str("a JSON value with ordered object members")
834 }
835
836 fn visit_unit<E>(self) -> Result<Self::Value, E>
837 where
838 E: serde::de::Error,
839 {
840 Ok(OrderedJsonValue::Null)
841 }
842
843 fn visit_none<E>(self) -> Result<Self::Value, E>
844 where
845 E: serde::de::Error,
846 {
847 Ok(OrderedJsonValue::Null)
848 }
849
850 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
851 where
852 D: Deserializer<'de>,
853 {
854 OrderedJsonValue::deserialize(deserializer)
855 }
856
857 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
858 where
859 E: serde::de::Error,
860 {
861 Ok(OrderedJsonValue::Bool(value))
862 }
863
864 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
865 where
866 E: serde::de::Error,
867 {
868 Ok(OrderedJsonValue::Number(value.into()))
869 }
870
871 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
872 where
873 E: serde::de::Error,
874 {
875 Ok(OrderedJsonValue::Number(value.into()))
876 }
877
878 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
879 where
880 E: serde::de::Error,
881 {
882 serde_json::Number::from_f64(value)
883 .map(OrderedJsonValue::Number)
884 .ok_or_else(|| E::custom("non-finite JSON number"))
885 }
886
887 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
888 where
889 E: serde::de::Error,
890 {
891 Ok(OrderedJsonValue::String(value.to_owned()))
892 }
893
894 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
895 where
896 E: serde::de::Error,
897 {
898 Ok(OrderedJsonValue::String(value))
899 }
900
901 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
902 where
903 A: SeqAccess<'de>,
904 {
905 let mut values = Vec::new();
906 while let Some(value) = sequence.next_element()? {
907 values.push(value);
908 }
909 Ok(OrderedJsonValue::Array(values))
910 }
911
912 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
913 where
914 A: MapAccess<'de>,
915 {
916 let mut entries = Vec::new();
917 while let Some((key, value)) = map.next_entry()? {
918 entries.push((key, value));
919 }
920 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
921 }
922 }
923
924 deserializer.deserialize_any(OrderedValueVisitor)
925 }
926}
927
928impl Serialize for OrderedJsonObject {
929 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
930 where
931 S: Serializer,
932 {
933 let mut map = serializer.serialize_map(Some(self.0.len()))?;
934 for (key, value) in &self.0 {
935 map.serialize_entry(key, value)?;
936 }
937 map.end()
938 }
939}
940
941impl<'de> Deserialize<'de> for OrderedJsonObject {
942 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
943 where
944 D: Deserializer<'de>,
945 {
946 struct OrderedObjectVisitor;
947
948 impl<'de> Visitor<'de> for OrderedObjectVisitor {
949 type Value = OrderedJsonObject;
950
951 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
952 formatter.write_str("an object with ordered JSON members")
953 }
954
955 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
956 where
957 A: MapAccess<'de>,
958 {
959 let mut entries = Vec::new();
960 while let Some((key, value)) = map.next_entry()? {
961 entries.push((key, value));
962 }
963 Ok(OrderedJsonObject(entries))
964 }
965 }
966
967 deserializer.deserialize_map(OrderedObjectVisitor)
968 }
969}
970
971fn read_tagged<'de, D>(
972 deserializer: D,
973 field: &'static str,
974) -> Result<(String, OrderedJsonObject), D::Error>
975where
976 D: Deserializer<'de>,
977{
978 let body = OrderedJsonObject::deserialize(deserializer)?;
979 let mut tag = None;
980 for (key, value) in body.as_entries() {
981 if key != field {
982 continue;
983 }
984 if tag.is_some() {
985 return Err(D::Error::custom(format!(
986 "tagged object has duplicate `{field}` field"
987 )));
988 }
989 let OrderedJsonValue::String(value) = value else {
990 return Err(D::Error::custom(format!(
991 "tagged object has no string `{field}` field"
992 )));
993 };
994 tag = Some(value);
995 }
996 let Some(tag) = tag else {
997 return Err(D::Error::custom(format!(
998 "tagged object has no string `{field}` field"
999 )));
1000 };
1001 Ok((tag.to_string(), body))
1002}
1003
1004fn read_ordered_tagged(
1005 value: OrderedJsonValue,
1006 field: &'static str,
1007) -> Result<(String, OrderedJsonObject), String> {
1008 let OrderedJsonValue::Object(body) = value else {
1009 return Err(format!("expected tagged object with `{field}` field"));
1010 };
1011 let mut tag = None;
1012 for (key, value) in body.as_entries() {
1013 if key != field {
1014 continue;
1015 }
1016 if tag.is_some() {
1017 return Err(format!("tagged object has duplicate `{field}` field"));
1018 }
1019 let OrderedJsonValue::String(value) = value else {
1020 return Err(format!("tagged object has no string `{field}` field"));
1021 };
1022 tag = Some(value);
1023 }
1024 let Some(tag) = tag else {
1025 return Err(format!("tagged object has no string `{field}` field"));
1026 };
1027 Ok((tag.to_string(), body))
1028}
1029
1030fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1031 body.as_entries()
1032 .iter()
1033 .find_map(|(key, value)| (key == field).then_some(value))
1034}
1035
1036fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1037 match ordered_field(body, field) {
1038 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1039 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1040 None => Err(format!("tagged object has no `{field}` field")),
1041 }
1042}
1043
1044fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1045 let (tag, body) = read_ordered_tagged(value, "method")?;
1046 match tag.as_str() {
1047 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1048 digest: ordered_string(&body, "digest")?,
1049 }),
1050 "macos_spawn_inode" => {
1051 let device = ordered_field(&body, "device")
1052 .and_then(|value| match value {
1053 OrderedJsonValue::Number(number) => number.as_u64(),
1054 _ => None,
1055 })
1056 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1057 let inode = ordered_field(&body, "inode")
1058 .and_then(|value| match value {
1059 OrderedJsonValue::Number(number) => number.as_u64(),
1060 _ => None,
1061 })
1062 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
1063 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1064 }
1065 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1066 }
1067}
1068
1069impl Serialize for ModuleDeclaredProvenance {
1070 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1071 where
1072 S: Serializer,
1073 {
1074 match self {
1075 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1076 build: build.clone(),
1077 }
1078 .serialize(serializer),
1079 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1080 Self::Unknown { body, .. } => body.serialize(serializer),
1081 }
1082 }
1083}
1084
1085impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1086 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1087 where
1088 D: serde::Deserializer<'de>,
1089 {
1090 let (tag, value) = read_tagged(deserializer, "status")?;
1091 match tag.as_str() {
1092 "reported" => match serde_json::from_value(value.into_value())
1093 .map_err(D::Error::custom)?
1094 {
1095 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1096 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1097 },
1098 "unverifiable" => {
1099 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1100 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1101 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1102 }
1103 }
1104 _ => Ok(Self::Unknown { tag, body: value }),
1105 }
1106 }
1107}
1108
1109impl Serialize for RunningImageAgreement {
1110 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1111 where
1112 S: Serializer,
1113 {
1114 match self {
1115 Self::Match { evidence } => RunningImageAgreementWire::Match {
1116 evidence: evidence.clone(),
1117 }
1118 .serialize(serializer),
1119 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1120 running: running.clone(),
1121 disk: disk.clone(),
1122 }
1123 .serialize(serializer),
1124 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1125 reason: reason.clone(),
1126 }
1127 .serialize(serializer),
1128 Self::Unknown { body, .. } => body.serialize(serializer),
1129 }
1130 }
1131}
1132
1133impl<'de> Deserialize<'de> for RunningImageAgreement {
1134 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1135 where
1136 D: serde::Deserializer<'de>,
1137 {
1138 let (tag, value) = read_tagged(deserializer, "status")?;
1139 match tag.as_str() {
1140 "match" => Ok(Self::Match {
1141 evidence: decode_running_image_evidence(
1142 ordered_field(&value, "evidence")
1143 .cloned()
1144 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1145 )
1146 .map_err(D::Error::custom)?,
1147 }),
1148 "mismatch" => Ok(Self::Mismatch {
1149 running: decode_running_image_evidence(
1150 ordered_field(&value, "running")
1151 .cloned()
1152 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1153 )
1154 .map_err(D::Error::custom)?,
1155 disk: decode_running_image_evidence(
1156 ordered_field(&value, "disk")
1157 .cloned()
1158 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1159 )
1160 .map_err(D::Error::custom)?,
1161 }),
1162 "unavailable" => Ok(Self::Unavailable {
1163 reason: serde_json::from_value(
1164 ordered_field(&value, "reason")
1165 .cloned()
1166 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1167 .into_value(),
1168 )
1169 .map_err(D::Error::custom)?,
1170 }),
1171 _ => Ok(Self::Unknown { tag, body: value }),
1172 }
1173 }
1174}
1175
1176impl Serialize for RunningImageEvidence {
1177 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1178 where
1179 S: Serializer,
1180 {
1181 match self {
1182 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1183 digest: digest.clone(),
1184 }
1185 .serialize(serializer),
1186 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1187 device: *device,
1188 inode: *inode,
1189 }
1190 .serialize(serializer),
1191 Self::Unknown { body, .. } => body.serialize(serializer),
1192 }
1193 }
1194}
1195
1196impl<'de> Deserialize<'de> for RunningImageEvidence {
1197 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1198 where
1199 D: serde::Deserializer<'de>,
1200 {
1201 let (tag, value) = read_tagged(deserializer, "method")?;
1202 match tag.as_str() {
1203 "linux_proc_sha256" => {
1204 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1205 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1206 Ok(Self::LinuxProcSha256 { digest })
1207 }
1208 _ => unreachable!(),
1209 }
1210 }
1211 "macos_spawn_inode" => {
1212 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1213 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1214 Ok(Self::MacosSpawnInode { device, inode })
1215 }
1216 _ => unreachable!(),
1217 }
1218 }
1219 _ => Ok(Self::Unknown { tag, body: value }),
1220 }
1221 }
1222}
1223
1224impl Serialize for SupervisorRouteConsumer {
1225 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1226 where
1227 S: Serializer,
1228 {
1229 match self {
1230 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1231 module_id: module_id.clone(),
1232 }
1233 .serialize(serializer),
1234 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1235 connection_id: *connection_id,
1236 }
1237 .serialize(serializer),
1238 Self::Unknown { body, .. } => body.serialize(serializer),
1239 }
1240 }
1241}
1242
1243impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1244 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1245 where
1246 D: serde::Deserializer<'de>,
1247 {
1248 let (tag, value) = read_tagged(deserializer, "kind")?;
1249 match tag.as_str() {
1250 "reserved" => {
1251 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1252 SupervisorRouteConsumerWire::Reserved { module_id } => {
1253 Ok(Self::Reserved { module_id })
1254 }
1255 _ => unreachable!(),
1256 }
1257 }
1258 "direct" => {
1259 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1260 SupervisorRouteConsumerWire::Direct { connection_id } => {
1261 Ok(Self::Direct { connection_id })
1262 }
1263 _ => unreachable!(),
1264 }
1265 }
1266 _ => Ok(Self::Unknown { tag, body: value }),
1267 }
1268 }
1269}
1270
1271impl Serialize for StderrCaptureState {
1272 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1273 where
1274 S: Serializer,
1275 {
1276 match self {
1277 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1278 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1279 reason: reason.clone(),
1280 }
1281 .serialize(serializer),
1282 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1283 reason: reason.clone(),
1284 }
1285 .serialize(serializer),
1286 Self::Unknown { body, .. } => body.serialize(serializer),
1287 }
1288 }
1289}
1290
1291impl<'de> Deserialize<'de> for StderrCaptureState {
1292 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1293 where
1294 D: serde::Deserializer<'de>,
1295 {
1296 let (tag, value) = read_tagged(deserializer, "state")?;
1297 match tag.as_str() {
1298 "captured" => {
1299 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1300 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1301 _ => unreachable!(),
1302 }
1303 }
1304 "incomplete" => match serde_json::from_value(value.into_value())
1305 .map_err(D::Error::custom)?
1306 {
1307 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1308 _ => unreachable!(),
1309 },
1310 "not_captured" => match serde_json::from_value(value.into_value())
1311 .map_err(D::Error::custom)?
1312 {
1313 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1314 _ => unreachable!(),
1315 },
1316 _ => Ok(Self::Unknown { tag, body: value }),
1317 }
1318 }
1319}
1320
1321impl Serialize for StderrTailEntry {
1322 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1323 where
1324 S: Serializer,
1325 {
1326 match self {
1327 Self::Line { text, truncated } => StderrTailEntryWire::Line {
1328 text: text.clone(),
1329 truncated: *truncated,
1330 }
1331 .serialize(serializer),
1332 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1333 Self::Unknown { body, .. } => body.serialize(serializer),
1334 }
1335 }
1336}
1337
1338impl<'de> Deserialize<'de> for StderrTailEntry {
1339 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1340 where
1341 D: serde::Deserializer<'de>,
1342 {
1343 let (tag, value) = read_tagged(deserializer, "kind")?;
1344 match tag.as_str() {
1345 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1346 StderrTailEntryWire::Line { text, truncated } => Ok(Self::Line { text, truncated }),
1347 _ => unreachable!(),
1348 },
1349 "process_start" => {
1350 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1351 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1352 _ => unreachable!(),
1353 }
1354 }
1355 _ => Ok(Self::Unknown { tag, body: value }),
1356 }
1357 }
1358}
1359
1360fn is_zero_u64(value: &u64) -> bool {
1361 *value == 0
1362}
1363
1364fn default_true() -> bool {
1365 true
1366}
1367
1368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1370pub struct TerminalHistory {
1371 pub daemon_started_at_ms: u64,
1373 pub entries: Vec<TerminalEntry>,
1374 #[serde(default, skip_serializing_if = "is_zero_u64")]
1377 pub dropped: u64,
1378 #[serde(default, skip_serializing_if = "is_zero_u64")]
1381 pub journal_skipped_lines: u64,
1382 #[serde(default, skip_serializing_if = "is_zero_u64")]
1384 pub journal_read_errors: u64,
1385 #[serde(default, skip_serializing_if = "is_zero_u64")]
1387 pub journal_write_failures: u64,
1388}
1389
1390#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1392pub struct TerminalEntry {
1393 #[serde(default, skip_serializing_if = "Option::is_none")]
1396 pub daemon_incarnation: Option<String>,
1397 #[serde(default, skip_serializing_if = "Option::is_none")]
1398 pub exit_code: Option<i32>,
1399 #[serde(default, skip_serializing_if = "Option::is_none")]
1400 pub exit_signal: Option<i32>,
1401 pub at_ms: u64,
1402 pub disposition: TerminalDisposition,
1403 #[serde(default, skip_serializing_if = "Option::is_none")]
1407 pub exit_kind: Option<TerminalExitKind>,
1408 #[serde(default, skip_serializing_if = "Option::is_none")]
1415 pub disposition_detail: Option<String>,
1416}
1417
1418#[derive(Debug, Clone, PartialEq, Eq)]
1423pub enum TerminalExitKind {
1424 Clean,
1425 Crash,
1426 DeliberateSeverance,
1427 Unknown(String),
1428}
1429
1430impl TerminalExitKind {
1431 fn wire_name(&self) -> &str {
1432 match self {
1433 Self::Clean => "clean",
1434 Self::Crash => "crash",
1435 Self::DeliberateSeverance => "deliberate_severance",
1436 Self::Unknown(value) => value,
1437 }
1438 }
1439}
1440
1441impl Serialize for TerminalExitKind {
1442 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1443 where
1444 S: serde::Serializer,
1445 {
1446 serializer.serialize_str(self.wire_name())
1447 }
1448}
1449
1450impl<'de> Deserialize<'de> for TerminalExitKind {
1451 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1452 where
1453 D: serde::Deserializer<'de>,
1454 {
1455 let value = String::deserialize(deserializer)?;
1456 Ok(match value.as_str() {
1457 "clean" => Self::Clean,
1458 "crash" => Self::Crash,
1459 "deliberate_severance" => Self::DeliberateSeverance,
1460 _ => Self::Unknown(value),
1461 })
1462 }
1463}
1464
1465open_string_enum! {
1466 TerminalDisposition {
1468 Stopped => "stopped",
1469 Disabled => "disabled",
1470 Failed => "failed",
1471 Restarting => "restarting",
1472 }
1473}
1474
1475#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1476#[serde(rename_all = "snake_case")]
1477pub enum PollKind {
1478 Status,
1479 Liveness,
1480}
1481
1482#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1483pub struct CatalogEntry {
1484 pub module_id: String,
1485 #[serde(default = "default_true")]
1489 pub ready: bool,
1490 #[serde(default, skip_serializing_if = "Option::is_none")]
1511 pub module_version: Option<String>,
1512 pub roles: Vec<ProviderRole>,
1513 pub control_ops: Vec<String>,
1514 #[serde(default, skip_serializing_if = "Option::is_none")]
1519 pub capabilities: Option<CapabilityDeclarations>,
1520 #[serde(default, skip_serializing_if = "Option::is_none")]
1523 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1524}
1525
1526#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1527pub struct CapabilityRequirementStatus {
1528 pub consumer: String,
1529 pub capability: String,
1530 pub need: String,
1531 pub verdict: String,
1532 pub episode_seq: u64,
1533 pub config_satisfiable: bool,
1534 pub runtime_available: bool,
1535 pub detail: String,
1536}
1537
1538#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1539pub struct SupervisorRescanResult {
1540 pub added: Vec<String>,
1541 pub removed: Vec<String>,
1542 pub changed_pending_reload: Vec<String>,
1543 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1556 pub enabled_changes: Vec<String>,
1557 pub unchanged: u32,
1558 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1566 pub preview: bool,
1567 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1585 pub restart_required: Vec<String>,
1586 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1590 pub capability_warnings: Vec<String>,
1591}
1592
1593#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1609#[serde(rename_all = "snake_case")]
1610pub enum ModuleProtocol {
1611 #[default]
1615 Subc,
1616 None,
1618}
1619
1620#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1621pub struct SupervisorEntry {
1622 pub module_id: String,
1623 pub state: String,
1624 pub enabled: bool,
1625 pub live: bool,
1635 #[serde(default)]
1639 pub protocol: ModuleProtocol,
1640 pub health: SupervisorHealthStatus,
1641 #[serde(default)]
1647 pub last_probe_ms: Option<u64>,
1648 #[serde(default, skip_serializing_if = "Option::is_none")]
1652 pub last_exit_code: Option<i32>,
1653 #[serde(default, skip_serializing_if = "Option::is_none")]
1657 pub last_exit_signal: Option<i32>,
1658 #[serde(default, skip_serializing_if = "Option::is_none")]
1662 pub last_exit_ms: Option<u64>,
1663 #[serde(default, skip_serializing_if = "Option::is_none")]
1666 pub last_exit_kind: Option<TerminalExitKind>,
1667 #[serde(default, skip_serializing_if = "Option::is_none")]
1684 pub restart_count: Option<u32>,
1685 #[serde(default, skip_serializing_if = "Option::is_none")]
1688 pub max_restarts: Option<u32>,
1689 #[serde(default, skip_serializing_if = "Option::is_none")]
1692 pub lifetime_restarts: Option<u32>,
1693 #[serde(default, skip_serializing_if = "Option::is_none")]
1697 pub spawn_generation: Option<u64>,
1698 #[serde(default, skip_serializing_if = "Option::is_none")]
1708 pub restart_window_secs: Option<u64>,
1709 #[serde(default, skip_serializing_if = "Option::is_none")]
1713 pub drain_timeout_ms: Option<u64>,
1714 #[serde(default, skip_serializing_if = "Option::is_none")]
1717 pub restart_backoff_ms: Option<u64>,
1718 #[serde(default, skip_serializing_if = "Option::is_none")]
1721 pub restart_max_backoff_ms: Option<u64>,
1722}
1723
1724#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1725#[serde(rename_all = "snake_case")]
1726pub enum SupervisorHealthStatus {
1727 Ok,
1728 Degraded,
1729 Failing,
1730 Unresponsive,
1731 Unknown,
1732}
1733
1734#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1735pub struct SupervisorHealthEntry {
1736 pub module_id: String,
1737 pub status: SupervisorHealthStatus,
1738 #[serde(default, skip_serializing_if = "Option::is_none")]
1744 pub detail: Option<String>,
1745 #[serde(default, skip_serializing_if = "Option::is_none")]
1750 pub metrics: Option<serde_json::Value>,
1751 pub consecutive_failures: u32,
1752 #[serde(default)]
1755 pub late_answer_count: u64,
1756 #[serde(default, skip_serializing_if = "Option::is_none")]
1758 pub last_late_answer_latency_ms: Option<u64>,
1759 #[serde(default)]
1764 pub last_action: Option<String>,
1765 #[serde(default)]
1768 pub last_action_ms: Option<u64>,
1769 #[serde(default, skip_serializing_if = "Option::is_none")]
1782 pub last_probe_ms: Option<u64>,
1783}
1784
1785#[cfg(test)]
1786mod tests {
1787 use super::*;
1788 use subc_protocol::{BindIdentity, RouteTarget};
1789
1790 #[test]
1791 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
1792 let entry = TerminalEntry {
1793 daemon_incarnation: Some("daemon-before-restart".into()),
1794 exit_code: Some(1),
1795 exit_signal: None,
1796 at_ms: 1_700_000_000_123,
1797 disposition: TerminalDisposition::Restarting,
1798 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
1799 disposition_detail: None,
1800 };
1801 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
1802 assert_eq!(
1803 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
1804 ["exit_kind"],
1805 "deliberate_severance"
1806 );
1807
1808 #[derive(serde::Deserialize)]
1809 struct LegacyTerminalEntry {
1810 exit_code: Option<i32>,
1811 exit_signal: Option<i32>,
1812 at_ms: u64,
1813 disposition: TerminalDisposition,
1814 }
1815
1816 let decoded: LegacyTerminalEntry =
1817 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
1818 assert_eq!(decoded.exit_code, Some(1));
1819 assert_eq!(decoded.exit_signal, None);
1820 assert_eq!(decoded.at_ms, 1_700_000_000_123);
1821 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
1822
1823 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
1824 let future: TerminalEntry =
1825 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
1826 assert_eq!(
1827 future.exit_kind,
1828 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
1829 );
1830 }
1831
1832 #[test]
1833 fn terminal_incarnation_is_optional_for_older_daemons() {
1834 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
1835 "at_ms": 123,
1836 "disposition": "stopped"
1837 }))
1838 .unwrap();
1839 let encoded = serde_json::to_value(&entry).unwrap();
1840 assert_eq!(
1841 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
1842 (None, None)
1843 );
1844 }
1845
1846 #[test]
1847 fn route_poll_uses_kind_field() {
1848 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
1849 route_channel: 7,
1850 route_epoch: 11,
1851 kind: PollKind::Status,
1852 })
1853 .unwrap();
1854
1855 assert_eq!(body["op"], "route.poll");
1856 assert_eq!(body["route_epoch"], 11);
1857 assert_eq!(body["kind"], "status");
1858 assert!(body.get("op").is_some());
1859 }
1860
1861 #[test]
1862 fn route_open_is_internally_tagged() {
1863 let request = ClientControlRequest::RouteOpen {
1864 target: RouteTarget::ToolProvider {
1865 module_id: "aft".to_string(),
1866 },
1867 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
1868 consumer_identity: None,
1869 consumer_capabilities: None,
1870 admission_facts: None,
1871 };
1872
1873 let body = serde_json::to_value(request).unwrap();
1874 assert_eq!(body["op"], "route.open");
1875 assert_eq!(body["target"]["kind"], "tool_provider");
1876 assert!(body.get("consumer_identity").is_none());
1877 assert!(body.get("consumer_capabilities").is_none());
1878 }
1879
1880 #[test]
1881 fn route_open_without_optional_fields_still_decodes() {
1882 let body = serde_json::json!({
1883 "op": "route.open",
1884 "target": { "kind": "tool_provider", "module_id": "aft" },
1885 "identity": {
1886 "project_root": "/tmp/project",
1887 "harness": "opencode",
1888 "session": "session-1"
1889 }
1890 });
1891
1892 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
1893 let ClientControlRequest::RouteOpen {
1894 consumer_identity,
1895 consumer_capabilities,
1896 admission_facts,
1897 ..
1898 } = decoded
1899 else {
1900 panic!("decoded wrong request variant");
1901 };
1902 assert_eq!(consumer_identity, None);
1903 assert_eq!(consumer_capabilities, None);
1904 assert_eq!(admission_facts, None);
1905 }
1906
1907 #[test]
1908 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
1909 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
1910 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
1911 match decoded {
1912 ClientControlPush::RouteClosed {
1913 excluded_subscriptions,
1914 terminal,
1915 ..
1916 } => {
1917 assert_eq!(excluded_subscriptions, 0);
1918 assert_eq!(terminal, None);
1919 }
1920 other => panic!("unexpected push: {other:?}"),
1921 }
1922 assert!(!serde_json::to_string(&decoded)
1923 .unwrap()
1924 .contains("terminal"));
1925 }
1926
1927 #[test]
1928 fn old_route_closed_decoder_ignores_new_terminal_field() {
1929 #[derive(serde::Deserialize)]
1930 #[serde(tag = "op")]
1931 enum LegacyClientControlPush {
1932 #[serde(rename = "route.closed")]
1933 RouteClosed {
1934 module_id: String,
1935 reason: RouteCloseReason,
1936 drained: bool,
1937 abandoned: u32,
1938 },
1939 }
1940
1941 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
1942 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
1943 match decoded {
1944 LegacyClientControlPush::RouteClosed {
1945 module_id,
1946 reason,
1947 drained,
1948 abandoned,
1949 } => {
1950 assert_eq!(module_id, "aft-tools");
1951 assert_eq!(reason, RouteCloseReason::Crash);
1952 assert!(!drained);
1953 assert_eq!(abandoned, 0);
1954 }
1955 }
1956 }
1957
1958 #[test]
1959 fn supervisor_routes_is_a_control_plane_request() {
1960 let body = serde_json::json!({
1961 "op": "supervisor.routes",
1962 "module_id": "aft"
1963 });
1964
1965 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
1966 assert_eq!(serde_json::to_value(request).unwrap(), body);
1967 }
1968
1969 #[test]
1970 fn diagnostic_string_enums_retain_unknown_wire_values() {
1971 let reason: RunningImageUnavailableReason =
1972 serde_json::from_str("\"future_reason\"").unwrap();
1973 let disposition: TerminalDisposition =
1974 serde_json::from_str("\"future_disposition\"").unwrap();
1975
1976 assert_eq!(
1977 reason,
1978 RunningImageUnavailableReason::Unknown("future_reason".to_string())
1979 );
1980 assert_eq!(
1981 disposition,
1982 TerminalDisposition::Unknown("future_disposition".to_string())
1983 );
1984 }
1985
1986 #[test]
1987 fn diagnostic_string_enums_preserve_existing_wire_names() {
1988 let names = [
1989 (RunningImageUnavailableReason::NotRunning, "not_running"),
1990 (
1991 RunningImageUnavailableReason::UnsupportedPlatform,
1992 "unsupported_platform",
1993 ),
1994 (
1995 RunningImageUnavailableReason::RunningExecutableUnreadable,
1996 "running_executable_unreadable",
1997 ),
1998 (
1999 RunningImageUnavailableReason::SpawnedPathUnreadable,
2000 "spawned_path_unreadable",
2001 ),
2002 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2003 (
2004 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2005 "process_identity_unconfirmed",
2006 ),
2007 ];
2008 for (value, expected) in names {
2009 let wire = serde_json::to_string(&value).unwrap();
2010 assert_eq!(wire, format!("\"{expected}\""));
2011 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2012 assert_eq!(decoded, value);
2013 }
2014
2015 for (value, expected) in [
2016 (TerminalDisposition::Stopped, "stopped"),
2017 (TerminalDisposition::Disabled, "disabled"),
2018 (TerminalDisposition::Failed, "failed"),
2019 (TerminalDisposition::Restarting, "restarting"),
2020 ] {
2021 let wire = serde_json::to_string(&value).unwrap();
2022 assert_eq!(wire, format!("\"{expected}\""));
2023 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2024 assert_eq!(decoded, value);
2025 }
2026 }
2027
2028 #[test]
2029 fn diagnostic_string_enums_reject_non_string_bodies() {
2030 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2031 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2032 }
2033
2034 #[test]
2035 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2036 let body = serde_json::json!({
2037 "op": "supervisor.provenance",
2038 "daemon": {
2039 "daemon_build": {},
2040 "daemon_observed": {
2041 "running_image": {
2042 "status": "unavailable",
2043 "reason": "not_running"
2044 }
2045 }
2046 },
2047 "modules": [
2048 {
2049 "module_id": "future",
2050 "module_declared": { "status": "unverifiable" },
2051 "daemon_observed": {
2052 "running_image": {
2053 "status": "unavailable",
2054 "reason": "future_reason"
2055 }
2056 }
2057 },
2058 {
2059 "module_id": "healthy-a",
2060 "module_declared": { "status": "unverifiable" },
2061 "daemon_observed": {
2062 "running_image": {
2063 "status": "match",
2064 "evidence": {
2065 "method": "linux_proc_sha256",
2066 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2067 }
2068 }
2069 }
2070 },
2071 {
2072 "module_id": "healthy-b",
2073 "module_declared": { "status": "unverifiable" },
2074 "daemon_observed": {
2075 "running_image": {
2076 "status": "unavailable",
2077 "reason": "unsupported_platform"
2078 }
2079 }
2080 }
2081 ]
2082 });
2083
2084 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2085 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2086 panic!("decoded wrong response variant");
2087 };
2088 assert_eq!(modules.len(), 3);
2089 assert_eq!(modules[0].module_id, "future");
2090 assert_eq!(
2091 modules[0].daemon_observed.running_image,
2092 RunningImageAgreement::Unavailable {
2093 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2094 }
2095 );
2096 assert_eq!(modules[1].module_id, "healthy-a");
2097 assert_eq!(modules[2].module_id, "healthy-b");
2098 }
2099
2100 #[test]
2101 fn tagged_unknown_values_retain_tag_and_body() {
2102 macro_rules! assert_unknown_round_trip {
2103 ($ty:ident, $field:literal, $value:expr) => {
2104 let value = $value;
2105 let wire = serde_json::to_string(&value).unwrap();
2106 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2107 match decoded {
2108 $ty::Unknown { tag, body } => {
2109 assert_eq!(tag, value[$field].as_str().unwrap());
2110 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2111 }
2112 _ => panic!("decoded known variant"),
2113 }
2114 };
2115 }
2116
2117 assert_unknown_round_trip!(
2118 ModuleDeclaredProvenance,
2119 "status",
2120 serde_json::json!({"status": "future", "build": {"version": 7}})
2121 );
2122 assert_unknown_round_trip!(
2123 RunningImageAgreement,
2124 "status",
2125 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2126 );
2127 assert_unknown_round_trip!(
2128 RunningImageEvidence,
2129 "method",
2130 serde_json::json!({"method": "future", "digest": "abc"})
2131 );
2132 assert_unknown_round_trip!(
2133 SupervisorRouteConsumer,
2134 "kind",
2135 serde_json::json!({"kind": "future", "module_id": "m"})
2136 );
2137 assert_unknown_round_trip!(
2138 StderrCaptureState,
2139 "state",
2140 serde_json::json!({"state": "future", "reason": "because"})
2141 );
2142 assert_unknown_round_trip!(
2143 StderrTailEntry,
2144 "kind",
2145 serde_json::json!({"kind": "future", "text": "line"})
2146 );
2147 }
2148
2149 #[test]
2150 fn tagged_unknown_values_round_trip_the_original_json() {
2151 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2152 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2153 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2154 }
2155
2156 #[test]
2157 fn tagged_unknown_values_round_trip_trailing_tag() {
2158 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2159 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2160 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2161
2162 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2163 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2164 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2165 }
2166
2167 #[test]
2168 fn tagged_unknown_values_round_trip_middle_tag() {
2169 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2170 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2171 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2172
2173 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2174 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2175 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2176 }
2177
2178 #[test]
2179 fn tagged_unknown_values_round_trip_deep_payload() {
2180 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2181 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2182 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2183
2184 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2185 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2186 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2187 }
2188
2189 #[test]
2190 fn tagged_unknown_values_reject_non_object_bodies() {
2191 for wire in ["42", r#""future""#, "[]"] {
2192 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2193 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2194 }
2195 }
2196
2197 #[test]
2198 fn duplicate_discriminators_reject_without_panicking() {
2199 assert_eq!(
2200 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2201 .unwrap(),
2202 ModuleDeclaredProvenance::Unverifiable
2203 );
2204 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2205 .unwrap()
2206 {
2207 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2208 _ => panic!("future discriminator decoded as a known variant"),
2209 }
2210
2211 let wires = [
2212 r#"{"status":"reported","status":"unverifiable"}"#,
2213 r#"{"status":"unverifiable","status":"reported"}"#,
2214 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2215 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2216 ];
2217
2218 for wire in wires {
2219 let result =
2220 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2221 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2222 assert!(
2223 result.unwrap().is_err(),
2224 "duplicate discriminator decoded: {wire}"
2225 );
2226 }
2227
2228 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2229 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2230 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2231 assert!(
2232 result.unwrap().is_err(),
2233 "duplicate discriminator decoded: {wire}"
2234 );
2235 }
2236
2237 #[test]
2238 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2239 let known_wire =
2240 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2241 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2242 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2243
2244 for wire in [
2245 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2246 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2247 ] {
2248 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2249 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2250 }
2251
2252 for wire in [
2253 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2254 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2255 ] {
2256 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2257 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2258 }
2259
2260 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2261 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2262 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2263
2264 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2265 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2266 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2267 }
2268
2269 #[test]
2270 fn tagged_unknown_member_does_not_discard_known_siblings() {
2271 let body = serde_json::json!({
2272 "modules": [{
2273 "module_id": "target",
2274 "routes": [
2275 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2276 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2277 ]
2278 }]
2279 });
2280 let decoded: ClientControlResponse = serde_json::from_value(
2281 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2282 )
2283 .unwrap();
2284 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2285 panic!("decoded wrong response variant");
2286 };
2287 assert_eq!(modules[0].routes.len(), 2);
2288 assert_eq!(
2289 modules[0].routes[1].consumer,
2290 SupervisorRouteConsumer::Direct { connection_id: 7 }
2291 );
2292 }
2293}