1#![forbid(unsafe_code)]
9
10use std::path::PathBuf;
11
12use serde::{
13 de::{Error as _, MapAccess, SeqAccess, Visitor},
14 ser::SerializeMap,
15 Deserialize, Deserializer, Serialize, Serializer,
16};
17use subc_protocol::{
18 manifest::{CapabilityDeclarations, ManifestProvenance, ProviderRole, SelfSignalDeclaration},
19 session::HealthStatus,
20 BindIdentity, RouteTarget,
21};
22
23pub use subc_protocol::RouteCloseReason;
24
25macro_rules! open_string_enum {
26 (
27 $(#[$meta:meta])*
28 $name:ident {
29 $( $(#[$variant_meta:meta])* $variant:ident => $wire_name:literal ),+ $(,)?
30 }
31 ) => {
32 $(#[$meta])*
33 #[derive(Debug, Clone, PartialEq, Eq)]
34 pub enum $name {
35 $( $(#[$variant_meta])* $variant, )+
36 Unknown(String),
37 }
38
39 impl $name {
40 fn wire_name(&self) -> &str {
41 match self {
42 $( Self::$variant => $wire_name, )+
43 Self::Unknown(value) => value,
44 }
45 }
46 }
47
48 impl Serialize for $name {
49 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
50 where
51 S: serde::Serializer,
52 {
53 serializer.serialize_str(self.wire_name())
54 }
55 }
56
57 impl<'de> Deserialize<'de> for $name {
58 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59 where
60 D: serde::Deserializer<'de>,
61 {
62 let value = String::deserialize(deserializer)?;
63 Ok(match value.as_str() {
64 $( $wire_name => Self::$variant, )+
65 _ => Self::Unknown(value),
66 })
67 }
68 }
69 };
70}
71
72#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
74pub struct ConsumerIdentity {
75 pub module_id: String,
76 pub launch_nonce: String,
77}
78
79impl std::fmt::Debug for ConsumerIdentity {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.debug_struct("ConsumerIdentity")
86 .field("module_id", &self.module_id)
87 .field(
88 "launch_nonce",
89 &format_args!("<{} bytes redacted>", self.launch_nonce.len()),
90 )
91 .finish()
92 }
93}
94
95pub mod ops {
106 pub const SERVER: &str = "server.";
107 pub const CATALOG: &str = "catalog.";
108 pub const ROUTE: &str = "route.";
109 pub const SUPERVISOR: &str = "supervisor.";
110 pub const CONFIG: &str = "config.";
111
112 pub const SERVER_DESCRIBE: &str = "server.describe";
113 pub const CATALOG_LIST: &str = "catalog.list";
114 pub const ROUTE_OPEN: &str = "route.open";
115 pub const ROUTE_POLL: &str = "route.poll";
116 pub const ROUTE_CLOSING: &str = "route.closing";
117 pub const ROUTE_CLOSED: &str = "route.closed";
118 pub const SUPERVISOR_LIST: &str = "supervisor.list";
119 pub const SUPERVISOR_RESTART: &str = "supervisor.restart";
120 pub const SUPERVISOR_SWAP: &str = "supervisor.swap";
121 pub const SUPERVISOR_RELOAD: &str = "supervisor.reload";
122 pub const SUPERVISOR_RESCAN: &str = "supervisor.rescan";
123 pub const SUPERVISOR_RELEASE_RESERVED: &str = "supervisor.release_reserved";
124 pub const SUPERVISOR_SET_ENABLED: &str = "supervisor.set_enabled";
125 pub const SUPERVISOR_HEALTH_PROBE: &str = "supervisor.health_probe";
126 pub const SUPERVISOR_HEALTH: &str = "supervisor.health";
127 pub const SUPERVISOR_STDERR_TAIL: &str = "supervisor.stderr_tail";
128 pub const SUPERVISOR_TERMINALS: &str = "supervisor.terminals";
129 pub const SUPERVISOR_ROUTES: &str = "supervisor.routes";
130 pub const SUPERVISOR_PROVENANCE: &str = "supervisor.provenance";
131 pub const SUPERVISOR_SPAWN_SNAPSHOT: &str = "supervisor.spawn_snapshot";
132 pub const SUPERVISOR_SPAWN_SUBSCRIBE: &str = "supervisor.spawn_subscribe";
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137#[serde(tag = "op")]
138#[allow(clippy::large_enum_variant)]
141pub enum ClientControlRequest {
142 #[serde(rename = "server.describe")]
143 ServerDescribe {},
144 #[serde(rename = "catalog.list")]
145 CatalogList {
146 #[serde(default)]
151 module_id: Option<String>,
152 },
153 #[serde(rename = "route.open")]
154 RouteOpen {
155 target: RouteTarget,
156 identity: BindIdentity,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
166 consumer_identity: Option<ConsumerIdentity>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
175 consumer_capabilities: Option<Vec<String>>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 admission_facts: Option<serde_json::Value>,
179 },
180 #[serde(rename = "route.poll")]
181 RoutePoll {
182 route_channel: u16,
183 route_epoch: u32,
184 kind: PollKind,
185 },
186 #[serde(rename = "supervisor.list")]
187 SupervisorList {},
188 #[serde(rename = "supervisor.spawn_snapshot")]
190 SupervisorSpawnSnapshot {},
191 #[serde(rename = "supervisor.spawn_subscribe")]
207 SupervisorSpawnSubscribe {
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 since: Option<SpawnCursor>,
210 },
211 #[serde(rename = "supervisor.restart")]
212 SupervisorRestart {
213 module_id: String,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
222 drain_timeout_ms: Option<u64>,
223 },
224 #[serde(rename = "supervisor.swap")]
239 SupervisorSwap {
240 module_id: String,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
244 ready_timeout_ms: Option<u64>,
245 },
246 #[serde(rename = "supervisor.reload")]
247 SupervisorReload { module_id: String },
248 #[serde(rename = "supervisor.rescan")]
249 SupervisorRescan {
250 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
270 preview: bool,
271 },
272 #[serde(rename = "supervisor.release_reserved")]
276 SupervisorReleaseReserved { module_id: String },
277 #[serde(rename = "supervisor.set_enabled")]
278 SupervisorSetEnabled { module_id: String, enabled: bool },
279 #[serde(rename = "supervisor.health_probe")]
280 SupervisorHealthProbe { module_id: String },
281 #[serde(rename = "supervisor.health")]
282 SupervisorHealth {},
283 #[serde(rename = "supervisor.routes")]
296 SupervisorRoutes {
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 module_id: Option<String>,
299 },
300 #[serde(rename = "supervisor.provenance")]
303 SupervisorProvenance {
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 module_id: Option<String>,
306 },
307 #[serde(rename = "supervisor.stderr_tail")]
315 SupervisorStderrTail {
316 module_id: String,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 max_lines: Option<u32>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 max_bytes: Option<u32>,
321 },
322 #[serde(rename = "supervisor.terminals")]
335 SupervisorTerminals { module_id: String },
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
340#[serde(tag = "op")]
341pub enum ClientControlResponse {
342 #[serde(rename = "server.describe")]
343 ServerDescribe {
344 protocol_ver: u8,
345 subc_ops: Vec<String>,
346 capabilities: Vec<String>,
347 connected_clients: u64,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
349 counters: Option<serde_json::Value>,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
357 build_git_sha: Option<String>,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
364 build_lock_digest: Option<String>,
365 #[serde(default, skip_serializing_if = "Vec::is_empty")]
369 capability_requirements: Vec<CapabilityRequirementStatus>,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
375 machine_id: Option<String>,
376 },
377 #[serde(rename = "catalog.list")]
378 CatalogList {
379 generation: u64,
380 modules: Vec<CatalogEntry>,
381 subc_ops: Vec<String>,
382 },
383 #[serde(rename = "route.open")]
384 RouteOpen {
385 route_channel: u16,
386 route_epoch: u32,
387 },
388 #[serde(rename = "route.poll")]
389 RoutePoll {
390 route_channel: u16,
391 route_epoch: u32,
392 status: Option<String>,
393 live: Option<bool>,
394 },
395 #[serde(rename = "supervisor.list")]
396 SupervisorList {
397 generation: u64,
398 modules: Vec<SupervisorEntry>,
399 },
400 #[serde(rename = "supervisor.spawn_snapshot")]
401 SupervisorSpawnSnapshot {
402 #[serde(flatten)]
403 snapshot: SpawnSnapshot,
404 },
405 #[serde(rename = "supervisor.ack")]
406 SupervisorAck { module_id: String, applied: bool },
407 #[serde(rename = "supervisor.rescan")]
408 SupervisorRescan {
409 #[serde(flatten)]
410 result: SupervisorRescanResult,
411 },
412 #[serde(rename = "supervisor.health_probe")]
413 SupervisorHealthProbe {
414 module_id: String,
415 status: HealthStatus,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
417 detail: Option<String>,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
419 metrics: Option<serde_json::Value>,
420 },
421 #[serde(rename = "supervisor.health")]
422 SupervisorHealth {
423 generation: u64,
424 modules: Vec<SupervisorHealthEntry>,
425 },
426 #[serde(rename = "supervisor.routes")]
427 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
428 #[serde(rename = "supervisor.provenance")]
429 SupervisorProvenance {
430 daemon: SupervisorDaemonProvenance,
431 modules: Vec<SupervisorModuleProvenance>,
432 },
433 #[serde(rename = "supervisor.stderr_tail")]
434 SupervisorStderrTail {
435 module_id: String,
436 #[serde(flatten)]
437 tail: StderrTail,
438 },
439 #[serde(rename = "supervisor.terminals")]
440 SupervisorTerminals {
441 module_id: String,
442 #[serde(flatten)]
443 terminals: TerminalHistory,
444 },
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
452#[serde(tag = "op")]
453pub enum ClientControlPush {
454 #[serde(rename = "route.closing")]
455 RouteClosing {
456 module_id: String,
457 reason: RouteCloseReason,
458 },
459 #[serde(rename = "route.closed")]
460 RouteClosed {
461 module_id: String,
462 reason: RouteCloseReason,
463 drained: bool,
465 abandoned: u32,
468 #[serde(default)]
470 excluded_subscriptions: u32,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
477 terminal: Option<bool>,
478 },
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
483pub struct SpawnCursor {
484 pub daemon_incarnation: String,
485 pub seq: u64,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
490pub struct LiveSpawn {
491 pub module_id: String,
492 pub spawn_generation: u64,
493 pub pid: u32,
494 pub spawned_at_ms: u64,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
499pub struct SpawnSnapshot {
500 pub cursor: SpawnCursor,
501 pub ring_bound: u64,
503 pub live: Vec<LiveSpawn>,
504}
505
506#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
508#[serde(rename_all = "snake_case")]
509pub enum SpawnEventKind {
510 Spawned,
511 Exited,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
520pub struct SpawnEvent {
521 pub cursor: SpawnCursor,
522 pub kind: SpawnEventKind,
523 pub module_id: String,
524 pub spawn_generation: u64,
525 pub pid: u32,
526 #[serde(default, skip_serializing_if = "Option::is_none")]
527 pub exit_code: Option<i32>,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
529 pub exit_signal: Option<i32>,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
534pub struct StderrTail {
535 pub capture: StderrCaptureState,
536 pub entries: Vec<StderrTailEntry>,
537 #[serde(default, skip_serializing_if = "is_zero_u64")]
546 pub dropped_lines: u64,
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
551pub struct SupervisorRouteModule {
552 pub module_id: String,
553 pub routes: Vec<SupervisorRoute>,
554}
555
556#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
558pub struct SupervisorRoute {
559 pub consumer: SupervisorRouteConsumer,
560 pub age_ms: u64,
562 pub draining: bool,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
571 pub drain_reason: Option<RouteCloseReason>,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
576pub struct SupervisorModuleProvenance {
577 pub module_id: String,
578 pub module_declared: ModuleDeclaredProvenance,
579 pub daemon_observed: SupervisorObservedProcess,
580}
581
582#[derive(Debug, Clone, PartialEq)]
584pub enum ModuleDeclaredProvenance {
585 Reported {
586 build: ManifestProvenance,
587 },
588 Unverifiable,
589 Unknown {
592 tag: String,
593 body: OrderedJsonObject,
594 },
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
602pub struct SupervisorObservedProcess {
603 #[serde(default, skip_serializing_if = "Option::is_none")]
604 pub pid: Option<u32>,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
606 pub spawned_at_ms: Option<u64>,
607 #[serde(default, skip_serializing_if = "Option::is_none")]
608 pub spawned_from: Option<PathBuf>,
609 pub running_image: RunningImageAgreement,
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
615pub struct PendingReloadVerdict {
616 pub path: ReloadPathAgreement,
617 pub image: RunningImageAgreement,
618}
619
620#[derive(Debug, Clone, PartialEq)]
622pub enum ReloadPathAgreement {
623 Match,
624 Mismatch {
625 configured: PathBuf,
626 spawned_from: PathBuf,
627 },
628 Unavailable {
629 reason: ReloadPathUnavailableReason,
630 },
631 Unknown {
632 tag: String,
633 body: OrderedJsonObject,
634 },
635}
636
637open_string_enum! {
638 ReloadPathUnavailableReason {
640 NotRunning => "not_running",
641 SpawnedPathUnavailable => "spawned_path_unavailable",
642 }
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
647pub struct SupervisorDaemonProvenance {
648 pub daemon_build: DaemonBuildProvenance,
649 pub daemon_observed: DaemonObservedProcess,
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
654pub struct DaemonBuildProvenance {
655 #[serde(default, skip_serializing_if = "Option::is_none")]
656 pub build_git_sha: Option<String>,
657 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub build_lock_digest: Option<String>,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
663pub struct DaemonObservedProcess {
664 #[serde(default, skip_serializing_if = "Option::is_none")]
665 pub pid: Option<u32>,
666 #[serde(default, skip_serializing_if = "Option::is_none")]
671 pub started_at_ms: Option<u64>,
672 pub running_image: RunningImageAgreement,
673}
674
675#[derive(Debug, Clone, PartialEq)]
677pub enum RunningImageAgreement {
678 Match {
679 evidence: RunningImageEvidence,
680 },
681 Mismatch {
682 running: RunningImageEvidence,
683 disk: RunningImageEvidence,
684 },
685 Unavailable {
686 reason: RunningImageUnavailableReason,
687 },
688 Unknown {
691 tag: String,
692 body: OrderedJsonObject,
693 },
694}
695
696#[derive(Debug, Clone, PartialEq)]
698pub enum RunningImageEvidence {
699 LinuxProcSha256 {
700 digest: String,
701 },
702 MacosSpawnInode {
703 device: u64,
704 inode: u64,
705 },
706 Unknown {
709 tag: String,
710 body: OrderedJsonObject,
711 },
712}
713
714open_string_enum! {
715 RunningImageUnavailableReason {
717 NotRunning => "not_running",
718 UnsupportedPlatform => "unsupported_platform",
719 RunningExecutableUnreadable => "running_executable_unreadable",
720 SpawnedPathUnreadable => "spawned_path_unreadable",
721 HashFailed => "hash_failed",
722 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
723 }
724}
725
726#[derive(Debug, Clone, PartialEq)]
732pub enum SupervisorRouteConsumer {
733 Reserved {
734 module_id: String,
735 },
736 Direct {
737 connection_id: u64,
738 },
739 Unknown {
742 tag: String,
743 body: OrderedJsonObject,
744 },
745}
746
747#[derive(Debug, Clone, PartialEq)]
754pub enum StderrCaptureState {
755 Captured,
758 Incomplete { reason: String },
760 NotCaptured { reason: String },
762 Unknown {
765 tag: String,
766 body: OrderedJsonObject,
767 },
768}
769
770#[derive(Debug, Clone, PartialEq)]
771pub enum StderrTailEntry {
772 Line {
773 text: String,
774 truncated: bool,
779 },
780 ProcessStart,
785 Unknown {
788 tag: String,
789 body: OrderedJsonObject,
790 },
791}
792
793#[derive(Debug, Serialize, Deserialize)]
794#[serde(tag = "status", rename_all = "snake_case")]
795enum ModuleDeclaredProvenanceWire {
796 Reported { build: ManifestProvenance },
797 Unverifiable,
798}
799
800#[derive(Debug, Serialize, Deserialize)]
801#[serde(tag = "status", rename_all = "snake_case")]
802enum RunningImageAgreementWire {
803 Match {
804 evidence: RunningImageEvidence,
805 },
806 Mismatch {
807 running: RunningImageEvidence,
808 disk: RunningImageEvidence,
809 },
810 Unavailable {
811 reason: RunningImageUnavailableReason,
812 },
813}
814
815#[derive(Debug, Serialize, Deserialize)]
816#[serde(tag = "status", rename_all = "snake_case")]
817enum ReloadPathAgreementWire {
818 Match,
819 Mismatch {
820 configured: PathBuf,
821 spawned_from: PathBuf,
822 },
823 Unavailable {
824 reason: ReloadPathUnavailableReason,
825 },
826}
827
828#[derive(Debug, Serialize, Deserialize)]
829#[serde(tag = "method", rename_all = "snake_case")]
830enum RunningImageEvidenceWire {
831 LinuxProcSha256 { digest: String },
832 MacosSpawnInode { device: u64, inode: u64 },
833}
834
835#[derive(Debug, Serialize, Deserialize)]
836#[serde(tag = "kind", rename_all = "snake_case")]
837enum SupervisorRouteConsumerWire {
838 Reserved { module_id: String },
839 Direct { connection_id: u64 },
840}
841
842#[derive(Debug, Serialize, Deserialize)]
843#[serde(tag = "state", rename_all = "snake_case")]
844enum StderrCaptureStateWire {
845 Captured,
846 Incomplete { reason: String },
847 NotCaptured { reason: String },
848}
849
850#[derive(Debug, Serialize, Deserialize)]
851#[serde(tag = "status", rename_all = "snake_case")]
852enum ChildResourceUsageWire {
853 Measured(ChildResourceReading),
854 Unavailable {
855 reason: ChildResourceUnavailableReason,
856 },
857}
858
859#[derive(Debug, Serialize, Deserialize)]
860#[serde(tag = "kind", rename_all = "snake_case")]
861enum StderrTailEntryWire {
862 Line {
863 text: String,
864 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
865 truncated: bool,
866 },
867 ProcessStart,
868}
869
870#[derive(Debug, Clone, PartialEq)]
872pub enum OrderedJsonValue {
873 Null,
874 Bool(bool),
875 Number(serde_json::Number),
876 String(String),
877 Array(Vec<Self>),
878 Object(OrderedJsonObject),
879}
880
881#[derive(Debug, Clone, PartialEq)]
883pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
884
885impl OrderedJsonObject {
886 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
888 &self.0
889 }
890
891 fn into_value(self) -> serde_json::Value {
892 serde_json::Value::Object(
893 self.0
894 .into_iter()
895 .map(|(key, value)| (key, value.into_value()))
896 .collect(),
897 )
898 }
899}
900
901impl OrderedJsonValue {
902 fn into_value(self) -> serde_json::Value {
903 match self {
904 Self::Null => serde_json::Value::Null,
905 Self::Bool(value) => serde_json::Value::Bool(value),
906 Self::Number(value) => serde_json::Value::Number(value),
907 Self::String(value) => serde_json::Value::String(value),
908 Self::Array(values) => {
909 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
910 }
911 Self::Object(value) => value.into_value(),
912 }
913 }
914}
915
916impl Serialize for OrderedJsonValue {
917 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
918 where
919 S: Serializer,
920 {
921 match self {
922 Self::Null => serializer.serialize_unit(),
923 Self::Bool(value) => serializer.serialize_bool(*value),
924 Self::Number(value) => value.serialize(serializer),
925 Self::String(value) => serializer.serialize_str(value),
926 Self::Array(values) => values.serialize(serializer),
927 Self::Object(value) => value.serialize(serializer),
928 }
929 }
930}
931
932impl<'de> Deserialize<'de> for OrderedJsonValue {
933 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
934 where
935 D: Deserializer<'de>,
936 {
937 struct OrderedValueVisitor;
938
939 impl<'de> Visitor<'de> for OrderedValueVisitor {
940 type Value = OrderedJsonValue;
941
942 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
943 formatter.write_str("a JSON value with ordered object members")
944 }
945
946 fn visit_unit<E>(self) -> Result<Self::Value, E>
947 where
948 E: serde::de::Error,
949 {
950 Ok(OrderedJsonValue::Null)
951 }
952
953 fn visit_none<E>(self) -> Result<Self::Value, E>
954 where
955 E: serde::de::Error,
956 {
957 Ok(OrderedJsonValue::Null)
958 }
959
960 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
961 where
962 D: Deserializer<'de>,
963 {
964 OrderedJsonValue::deserialize(deserializer)
965 }
966
967 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
968 where
969 E: serde::de::Error,
970 {
971 Ok(OrderedJsonValue::Bool(value))
972 }
973
974 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
975 where
976 E: serde::de::Error,
977 {
978 Ok(OrderedJsonValue::Number(value.into()))
979 }
980
981 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
982 where
983 E: serde::de::Error,
984 {
985 Ok(OrderedJsonValue::Number(value.into()))
986 }
987
988 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
989 where
990 E: serde::de::Error,
991 {
992 serde_json::Number::from_f64(value)
993 .map(OrderedJsonValue::Number)
994 .ok_or_else(|| E::custom("non-finite JSON number"))
995 }
996
997 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
998 where
999 E: serde::de::Error,
1000 {
1001 Ok(OrderedJsonValue::String(value.to_owned()))
1002 }
1003
1004 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
1005 where
1006 E: serde::de::Error,
1007 {
1008 Ok(OrderedJsonValue::String(value))
1009 }
1010
1011 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1012 where
1013 A: SeqAccess<'de>,
1014 {
1015 let mut values = Vec::new();
1016 while let Some(value) = sequence.next_element()? {
1017 values.push(value);
1018 }
1019 Ok(OrderedJsonValue::Array(values))
1020 }
1021
1022 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1023 where
1024 A: MapAccess<'de>,
1025 {
1026 let mut entries = Vec::new();
1027 while let Some((key, value)) = map.next_entry()? {
1028 entries.push((key, value));
1029 }
1030 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
1031 }
1032 }
1033
1034 deserializer.deserialize_any(OrderedValueVisitor)
1035 }
1036}
1037
1038impl Serialize for OrderedJsonObject {
1039 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1040 where
1041 S: Serializer,
1042 {
1043 let mut map = serializer.serialize_map(Some(self.0.len()))?;
1044 for (key, value) in &self.0 {
1045 map.serialize_entry(key, value)?;
1046 }
1047 map.end()
1048 }
1049}
1050
1051impl<'de> Deserialize<'de> for OrderedJsonObject {
1052 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1053 where
1054 D: Deserializer<'de>,
1055 {
1056 struct OrderedObjectVisitor;
1057
1058 impl<'de> Visitor<'de> for OrderedObjectVisitor {
1059 type Value = OrderedJsonObject;
1060
1061 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1062 formatter.write_str("an object with ordered JSON members")
1063 }
1064
1065 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1066 where
1067 A: MapAccess<'de>,
1068 {
1069 let mut entries = Vec::new();
1070 while let Some((key, value)) = map.next_entry()? {
1071 entries.push((key, value));
1072 }
1073 Ok(OrderedJsonObject(entries))
1074 }
1075 }
1076
1077 deserializer.deserialize_map(OrderedObjectVisitor)
1078 }
1079}
1080
1081fn read_tagged<'de, D>(
1082 deserializer: D,
1083 field: &'static str,
1084) -> Result<(String, OrderedJsonObject), D::Error>
1085where
1086 D: Deserializer<'de>,
1087{
1088 let body = OrderedJsonObject::deserialize(deserializer)?;
1089 let mut tag = None;
1090 for (key, value) in body.as_entries() {
1091 if key != field {
1092 continue;
1093 }
1094 if tag.is_some() {
1095 return Err(D::Error::custom(format!(
1096 "tagged object has duplicate `{field}` field"
1097 )));
1098 }
1099 let OrderedJsonValue::String(value) = value else {
1100 return Err(D::Error::custom(format!(
1101 "tagged object has no string `{field}` field"
1102 )));
1103 };
1104 tag = Some(value);
1105 }
1106 let Some(tag) = tag else {
1107 return Err(D::Error::custom(format!(
1108 "tagged object has no string `{field}` field"
1109 )));
1110 };
1111 Ok((tag.to_string(), body))
1112}
1113
1114fn read_ordered_tagged(
1115 value: OrderedJsonValue,
1116 field: &'static str,
1117) -> Result<(String, OrderedJsonObject), String> {
1118 let OrderedJsonValue::Object(body) = value else {
1119 return Err(format!("expected tagged object with `{field}` field"));
1120 };
1121 let mut tag = None;
1122 for (key, value) in body.as_entries() {
1123 if key != field {
1124 continue;
1125 }
1126 if tag.is_some() {
1127 return Err(format!("tagged object has duplicate `{field}` field"));
1128 }
1129 let OrderedJsonValue::String(value) = value else {
1130 return Err(format!("tagged object has no string `{field}` field"));
1131 };
1132 tag = Some(value);
1133 }
1134 let Some(tag) = tag else {
1135 return Err(format!("tagged object has no string `{field}` field"));
1136 };
1137 Ok((tag.to_string(), body))
1138}
1139
1140fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1141 body.as_entries()
1142 .iter()
1143 .find_map(|(key, value)| (key == field).then_some(value))
1144}
1145
1146fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1147 match ordered_field(body, field) {
1148 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1149 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1150 None => Err(format!("tagged object has no `{field}` field")),
1151 }
1152}
1153
1154fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1155 let (tag, body) = read_ordered_tagged(value, "method")?;
1156 match tag.as_str() {
1157 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1158 digest: ordered_string(&body, "digest")?,
1159 }),
1160 "macos_spawn_inode" => {
1161 let device = ordered_field(&body, "device")
1162 .and_then(|value| match value {
1163 OrderedJsonValue::Number(number) => number.as_u64(),
1164 _ => None,
1165 })
1166 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1167 let inode = ordered_field(&body, "inode")
1168 .and_then(|value| match value {
1169 OrderedJsonValue::Number(number) => number.as_u64(),
1170 _ => None,
1171 })
1172 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
1173 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1174 }
1175 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1176 }
1177}
1178
1179impl Serialize for ModuleDeclaredProvenance {
1180 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1181 where
1182 S: Serializer,
1183 {
1184 match self {
1185 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1186 build: build.clone(),
1187 }
1188 .serialize(serializer),
1189 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1190 Self::Unknown { body, .. } => body.serialize(serializer),
1191 }
1192 }
1193}
1194
1195impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1196 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1197 where
1198 D: serde::Deserializer<'de>,
1199 {
1200 let (tag, value) = read_tagged(deserializer, "status")?;
1201 match tag.as_str() {
1202 "reported" => match serde_json::from_value(value.into_value())
1203 .map_err(D::Error::custom)?
1204 {
1205 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1206 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1207 },
1208 "unverifiable" => {
1209 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1210 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1211 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1212 }
1213 }
1214 _ => Ok(Self::Unknown { tag, body: value }),
1215 }
1216 }
1217}
1218
1219impl Serialize for RunningImageAgreement {
1220 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1221 where
1222 S: Serializer,
1223 {
1224 match self {
1225 Self::Match { evidence } => RunningImageAgreementWire::Match {
1226 evidence: evidence.clone(),
1227 }
1228 .serialize(serializer),
1229 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1230 running: running.clone(),
1231 disk: disk.clone(),
1232 }
1233 .serialize(serializer),
1234 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1235 reason: reason.clone(),
1236 }
1237 .serialize(serializer),
1238 Self::Unknown { body, .. } => body.serialize(serializer),
1239 }
1240 }
1241}
1242
1243impl Serialize for ReloadPathAgreement {
1244 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1245 where
1246 S: Serializer,
1247 {
1248 match self {
1249 Self::Match => ReloadPathAgreementWire::Match.serialize(serializer),
1250 Self::Mismatch {
1251 configured,
1252 spawned_from,
1253 } => ReloadPathAgreementWire::Mismatch {
1254 configured: configured.clone(),
1255 spawned_from: spawned_from.clone(),
1256 }
1257 .serialize(serializer),
1258 Self::Unavailable { reason } => ReloadPathAgreementWire::Unavailable {
1259 reason: reason.clone(),
1260 }
1261 .serialize(serializer),
1262 Self::Unknown { body, .. } => body.serialize(serializer),
1263 }
1264 }
1265}
1266
1267impl<'de> Deserialize<'de> for ReloadPathAgreement {
1268 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1269 where
1270 D: Deserializer<'de>,
1271 {
1272 let (tag, body) = read_tagged(deserializer, "status")?;
1273 match tag.as_str() {
1274 "match" => Ok(Self::Match),
1275 "mismatch" => {
1276 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1277 ReloadPathAgreementWire::Mismatch {
1278 configured,
1279 spawned_from,
1280 } => Ok(Self::Mismatch {
1281 configured,
1282 spawned_from,
1283 }),
1284 _ => unreachable!(),
1285 }
1286 }
1287 "unavailable" => match serde_json::from_value(body.into_value())
1288 .map_err(D::Error::custom)?
1289 {
1290 ReloadPathAgreementWire::Unavailable { reason } => Ok(Self::Unavailable { reason }),
1291 _ => unreachable!(),
1292 },
1293 _ => Ok(Self::Unknown { tag, body }),
1294 }
1295 }
1296}
1297
1298impl<'de> Deserialize<'de> for RunningImageAgreement {
1299 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1300 where
1301 D: serde::Deserializer<'de>,
1302 {
1303 let (tag, value) = read_tagged(deserializer, "status")?;
1304 match tag.as_str() {
1305 "match" => Ok(Self::Match {
1306 evidence: decode_running_image_evidence(
1307 ordered_field(&value, "evidence")
1308 .cloned()
1309 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1310 )
1311 .map_err(D::Error::custom)?,
1312 }),
1313 "mismatch" => Ok(Self::Mismatch {
1314 running: decode_running_image_evidence(
1315 ordered_field(&value, "running")
1316 .cloned()
1317 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1318 )
1319 .map_err(D::Error::custom)?,
1320 disk: decode_running_image_evidence(
1321 ordered_field(&value, "disk")
1322 .cloned()
1323 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1324 )
1325 .map_err(D::Error::custom)?,
1326 }),
1327 "unavailable" => Ok(Self::Unavailable {
1328 reason: serde_json::from_value(
1329 ordered_field(&value, "reason")
1330 .cloned()
1331 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1332 .into_value(),
1333 )
1334 .map_err(D::Error::custom)?,
1335 }),
1336 _ => Ok(Self::Unknown { tag, body: value }),
1337 }
1338 }
1339}
1340
1341impl Serialize for ChildResourceUsage {
1342 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1343 where
1344 S: Serializer,
1345 {
1346 match self {
1347 Self::Measured(reading) => {
1348 ChildResourceUsageWire::Measured(reading.clone()).serialize(serializer)
1349 }
1350 Self::Unavailable { reason } => ChildResourceUsageWire::Unavailable {
1351 reason: reason.clone(),
1352 }
1353 .serialize(serializer),
1354 Self::Unknown { body, .. } => body.serialize(serializer),
1355 }
1356 }
1357}
1358
1359impl<'de> Deserialize<'de> for ChildResourceUsage {
1360 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1361 where
1362 D: Deserializer<'de>,
1363 {
1364 let (tag, body) = read_tagged(deserializer, "status")?;
1365 match tag.as_str() {
1366 "measured" | "unavailable" => {
1367 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1368 ChildResourceUsageWire::Measured(reading) => Ok(Self::Measured(reading)),
1369 ChildResourceUsageWire::Unavailable { reason } => {
1370 Ok(Self::Unavailable { reason })
1371 }
1372 }
1373 }
1374 _ => Ok(Self::Unknown { tag, body }),
1375 }
1376 }
1377}
1378
1379impl Serialize for RunningImageEvidence {
1380 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1381 where
1382 S: Serializer,
1383 {
1384 match self {
1385 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1386 digest: digest.clone(),
1387 }
1388 .serialize(serializer),
1389 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1390 device: *device,
1391 inode: *inode,
1392 }
1393 .serialize(serializer),
1394 Self::Unknown { body, .. } => body.serialize(serializer),
1395 }
1396 }
1397}
1398
1399impl<'de> Deserialize<'de> for RunningImageEvidence {
1400 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1401 where
1402 D: serde::Deserializer<'de>,
1403 {
1404 let (tag, value) = read_tagged(deserializer, "method")?;
1405 match tag.as_str() {
1406 "linux_proc_sha256" => {
1407 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1408 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1409 Ok(Self::LinuxProcSha256 { digest })
1410 }
1411 _ => unreachable!(),
1412 }
1413 }
1414 "macos_spawn_inode" => {
1415 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1416 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1417 Ok(Self::MacosSpawnInode { device, inode })
1418 }
1419 _ => unreachable!(),
1420 }
1421 }
1422 _ => Ok(Self::Unknown { tag, body: value }),
1423 }
1424 }
1425}
1426
1427impl Serialize for SupervisorRouteConsumer {
1428 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1429 where
1430 S: Serializer,
1431 {
1432 match self {
1433 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1434 module_id: module_id.clone(),
1435 }
1436 .serialize(serializer),
1437 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1438 connection_id: *connection_id,
1439 }
1440 .serialize(serializer),
1441 Self::Unknown { body, .. } => body.serialize(serializer),
1442 }
1443 }
1444}
1445
1446impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1447 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1448 where
1449 D: serde::Deserializer<'de>,
1450 {
1451 let (tag, value) = read_tagged(deserializer, "kind")?;
1452 match tag.as_str() {
1453 "reserved" => {
1454 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1455 SupervisorRouteConsumerWire::Reserved { module_id } => {
1456 Ok(Self::Reserved { module_id })
1457 }
1458 _ => unreachable!(),
1459 }
1460 }
1461 "direct" => {
1462 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1463 SupervisorRouteConsumerWire::Direct { connection_id } => {
1464 Ok(Self::Direct { connection_id })
1465 }
1466 _ => unreachable!(),
1467 }
1468 }
1469 _ => Ok(Self::Unknown { tag, body: value }),
1470 }
1471 }
1472}
1473
1474impl Serialize for StderrCaptureState {
1475 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1476 where
1477 S: Serializer,
1478 {
1479 match self {
1480 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1481 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1482 reason: reason.clone(),
1483 }
1484 .serialize(serializer),
1485 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1486 reason: reason.clone(),
1487 }
1488 .serialize(serializer),
1489 Self::Unknown { body, .. } => body.serialize(serializer),
1490 }
1491 }
1492}
1493
1494impl<'de> Deserialize<'de> for StderrCaptureState {
1495 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1496 where
1497 D: serde::Deserializer<'de>,
1498 {
1499 let (tag, value) = read_tagged(deserializer, "state")?;
1500 match tag.as_str() {
1501 "captured" => {
1502 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1503 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1504 _ => unreachable!(),
1505 }
1506 }
1507 "incomplete" => match serde_json::from_value(value.into_value())
1508 .map_err(D::Error::custom)?
1509 {
1510 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1511 _ => unreachable!(),
1512 },
1513 "not_captured" => match serde_json::from_value(value.into_value())
1514 .map_err(D::Error::custom)?
1515 {
1516 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1517 _ => unreachable!(),
1518 },
1519 _ => Ok(Self::Unknown { tag, body: value }),
1520 }
1521 }
1522}
1523
1524impl Serialize for StderrTailEntry {
1525 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1526 where
1527 S: Serializer,
1528 {
1529 match self {
1530 Self::Line { text, truncated } => StderrTailEntryWire::Line {
1531 text: text.clone(),
1532 truncated: *truncated,
1533 }
1534 .serialize(serializer),
1535 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1536 Self::Unknown { body, .. } => body.serialize(serializer),
1537 }
1538 }
1539}
1540
1541impl<'de> Deserialize<'de> for StderrTailEntry {
1542 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1543 where
1544 D: serde::Deserializer<'de>,
1545 {
1546 let (tag, value) = read_tagged(deserializer, "kind")?;
1547 match tag.as_str() {
1548 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1549 StderrTailEntryWire::Line { text, truncated } => Ok(Self::Line { text, truncated }),
1550 _ => unreachable!(),
1551 },
1552 "process_start" => {
1553 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1554 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1555 _ => unreachable!(),
1556 }
1557 }
1558 _ => Ok(Self::Unknown { tag, body: value }),
1559 }
1560 }
1561}
1562
1563fn is_zero_u64(value: &u64) -> bool {
1564 *value == 0
1565}
1566
1567fn default_true() -> bool {
1568 true
1569}
1570
1571#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1573pub struct TerminalHistory {
1574 pub daemon_started_at_ms: u64,
1576 pub entries: Vec<TerminalEntry>,
1577 #[serde(default, skip_serializing_if = "is_zero_u64")]
1580 pub dropped: u64,
1581 #[serde(default, skip_serializing_if = "is_zero_u64")]
1584 pub journal_skipped_lines: u64,
1585 #[serde(default, skip_serializing_if = "is_zero_u64")]
1587 pub journal_read_errors: u64,
1588 #[serde(default, skip_serializing_if = "is_zero_u64")]
1590 pub journal_write_failures: u64,
1591}
1592
1593#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1595pub struct TerminalEntry {
1596 #[serde(default, skip_serializing_if = "Option::is_none")]
1599 pub daemon_incarnation: Option<String>,
1600 #[serde(default, skip_serializing_if = "Option::is_none")]
1601 pub exit_code: Option<i32>,
1602 #[serde(default, skip_serializing_if = "Option::is_none")]
1603 pub exit_signal: Option<i32>,
1604 pub at_ms: u64,
1605 pub disposition: TerminalDisposition,
1606 #[serde(default, skip_serializing_if = "Option::is_none")]
1610 pub exit_kind: Option<TerminalExitKind>,
1611 #[serde(default, skip_serializing_if = "Option::is_none")]
1618 pub disposition_detail: Option<String>,
1619}
1620
1621#[derive(Debug, Clone, PartialEq, Eq)]
1626pub enum TerminalExitKind {
1627 Clean,
1628 Crash,
1629 DeliberateSeverance,
1630 Unknown(String),
1631}
1632
1633impl TerminalExitKind {
1634 fn wire_name(&self) -> &str {
1635 match self {
1636 Self::Clean => "clean",
1637 Self::Crash => "crash",
1638 Self::DeliberateSeverance => "deliberate_severance",
1639 Self::Unknown(value) => value,
1640 }
1641 }
1642}
1643
1644impl Serialize for TerminalExitKind {
1645 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1646 where
1647 S: serde::Serializer,
1648 {
1649 serializer.serialize_str(self.wire_name())
1650 }
1651}
1652
1653impl<'de> Deserialize<'de> for TerminalExitKind {
1654 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1655 where
1656 D: serde::Deserializer<'de>,
1657 {
1658 let value = String::deserialize(deserializer)?;
1659 Ok(match value.as_str() {
1660 "clean" => Self::Clean,
1661 "crash" => Self::Crash,
1662 "deliberate_severance" => Self::DeliberateSeverance,
1663 _ => Self::Unknown(value),
1664 })
1665 }
1666}
1667
1668open_string_enum! {
1669 TerminalDisposition {
1671 Stopped => "stopped",
1672 Disabled => "disabled",
1673 Failed => "failed",
1674 Restarting => "restarting",
1675 DaemonShutdown => "daemon_shutdown",
1680 }
1681}
1682
1683#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1684#[serde(rename_all = "snake_case")]
1685pub enum PollKind {
1686 Status,
1687 Liveness,
1688}
1689
1690#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1691pub struct CatalogEntry {
1692 pub module_id: String,
1693 #[serde(default = "default_true")]
1704 pub ready: bool,
1705 #[serde(default, skip_serializing_if = "Option::is_none")]
1709 pub not_ready: Option<NotReadyReason>,
1710 #[serde(default, skip_serializing_if = "Option::is_none")]
1731 pub module_version: Option<String>,
1732 pub roles: Vec<ProviderRole>,
1733 pub control_ops: Vec<String>,
1734 #[serde(default, skip_serializing_if = "Option::is_none")]
1739 pub capabilities: Option<CapabilityDeclarations>,
1740 #[serde(default, skip_serializing_if = "Option::is_none")]
1743 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1744}
1745
1746#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1748pub struct NotReadyReason {
1749 pub reason: String,
1754 #[serde(default, skip_serializing_if = "Option::is_none")]
1757 pub capability: Option<String>,
1758}
1759
1760impl NotReadyReason {
1761 pub const DECLARED_NOT_READY: &'static str = "declared_not_ready";
1762 pub const REQUIRED_CAPABILITY_UNPROVIDED: &'static str = "required_capability_unprovided";
1763}
1764
1765#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1766pub struct CapabilityRequirementStatus {
1767 pub consumer: String,
1768 pub capability: String,
1769 pub need: String,
1770 pub verdict: String,
1771 pub episode_seq: u64,
1772 pub config_satisfiable: bool,
1773 pub runtime_available: bool,
1774 pub detail: String,
1775}
1776
1777#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1778pub struct SupervisorRescanResult {
1779 pub added: Vec<String>,
1780 pub removed: Vec<String>,
1781 pub changed_pending_reload: Vec<String>,
1782 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1795 pub enabled_changes: Vec<String>,
1796 pub unchanged: u32,
1797 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1805 pub preview: bool,
1806 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1824 pub restart_required: Vec<String>,
1825 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1829 pub capability_warnings: Vec<String>,
1830}
1831
1832#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1848#[serde(rename_all = "snake_case")]
1849pub enum ModuleProtocol {
1850 #[default]
1854 Subc,
1855 None,
1857}
1858
1859#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1860pub struct SupervisorEntry {
1861 pub module_id: String,
1862 pub state: String,
1863 pub enabled: bool,
1864 pub live: bool,
1874 #[serde(default)]
1878 pub protocol: ModuleProtocol,
1879 pub health: SupervisorHealthStatus,
1880 #[serde(default, skip_serializing_if = "Option::is_none")]
1883 pub pending_reload: Option<PendingReloadVerdict>,
1884 #[serde(default)]
1890 pub last_probe_ms: Option<u64>,
1891 #[serde(default, skip_serializing_if = "Option::is_none")]
1895 pub last_exit_code: Option<i32>,
1896 #[serde(default, skip_serializing_if = "Option::is_none")]
1900 pub last_exit_signal: Option<i32>,
1901 #[serde(default, skip_serializing_if = "Option::is_none")]
1905 pub last_exit_ms: Option<u64>,
1906 #[serde(default, skip_serializing_if = "Option::is_none")]
1909 pub last_exit_kind: Option<TerminalExitKind>,
1910 #[serde(default, skip_serializing_if = "Option::is_none")]
1927 pub restart_count: Option<u32>,
1928 #[serde(default, skip_serializing_if = "Option::is_none")]
1931 pub max_restarts: Option<u32>,
1932 #[serde(default, skip_serializing_if = "Option::is_none")]
1935 pub lifetime_restarts: Option<u32>,
1936 #[serde(default, skip_serializing_if = "Option::is_none")]
1940 pub spawn_generation: Option<u64>,
1941 #[serde(default, skip_serializing_if = "Option::is_none")]
1951 pub restart_window_secs: Option<u64>,
1952 #[serde(default, skip_serializing_if = "Option::is_none")]
1956 pub drain_timeout_ms: Option<u64>,
1957 #[serde(default, skip_serializing_if = "Option::is_none")]
1960 pub restart_backoff_ms: Option<u64>,
1961 #[serde(default, skip_serializing_if = "Option::is_none")]
1964 pub restart_max_backoff_ms: Option<u64>,
1965 #[serde(default, skip_serializing_if = "Option::is_none")]
1978 pub resources: Option<ChildResourceUsage>,
1979}
1980
1981#[derive(Debug, Clone, PartialEq)]
1984pub enum ChildResourceUsage {
1985 Measured(ChildResourceReading),
1986 Unavailable {
1987 reason: ChildResourceUnavailableReason,
1988 },
1989 Unknown {
1992 tag: String,
1993 body: OrderedJsonObject,
1994 },
1995}
1996
1997#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1999pub struct ChildResourceReading {
2000 pub memory_bytes: u64,
2003 pub memory_kind: ChildMemoryKind,
2004 #[serde(default, skip_serializing_if = "Option::is_none")]
2007 pub swap_bytes: Option<u64>,
2008 pub cpu_user_ms: u64,
2012 pub cpu_system_ms: u64,
2015}
2016
2017open_string_enum! {
2018 ChildMemoryKind {
2020 PhysFootprint => "phys_footprint",
2024 ResidentSet => "resident_set",
2027 }
2028}
2029
2030open_string_enum! {
2031 ChildResourceUnavailableReason {
2033 NotRunning => "not_running",
2035 UnsupportedPlatform => "unsupported_platform",
2037 Unreadable => "unreadable",
2040 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
2043 }
2044}
2045
2046#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2047#[serde(rename_all = "snake_case")]
2048pub enum SupervisorHealthStatus {
2049 Ok,
2050 Degraded,
2051 Failing,
2052 Unresponsive,
2053 Unknown,
2054}
2055
2056#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2057pub struct SupervisorHealthEntry {
2058 pub module_id: String,
2059 pub status: SupervisorHealthStatus,
2060 #[serde(default, skip_serializing_if = "Option::is_none")]
2066 pub detail: Option<String>,
2067 #[serde(default, skip_serializing_if = "Option::is_none")]
2072 pub metrics: Option<serde_json::Value>,
2073 pub consecutive_failures: u32,
2074 #[serde(default)]
2077 pub late_answer_count: u64,
2078 #[serde(default, skip_serializing_if = "Option::is_none")]
2080 pub last_late_answer_latency_ms: Option<u64>,
2081 #[serde(default)]
2086 pub last_action: Option<String>,
2087 #[serde(default)]
2090 pub last_action_ms: Option<u64>,
2091 #[serde(default, skip_serializing_if = "Option::is_none")]
2104 pub last_probe_ms: Option<u64>,
2105}
2106
2107#[cfg(test)]
2108mod tests {
2109 use super::*;
2110 use subc_protocol::{BindIdentity, RouteTarget};
2111
2112 #[test]
2113 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
2114 let entry = TerminalEntry {
2115 daemon_incarnation: Some("daemon-before-restart".into()),
2116 exit_code: Some(1),
2117 exit_signal: None,
2118 at_ms: 1_700_000_000_123,
2119 disposition: TerminalDisposition::Restarting,
2120 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
2121 disposition_detail: None,
2122 };
2123 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
2124 assert_eq!(
2125 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
2126 ["exit_kind"],
2127 "deliberate_severance"
2128 );
2129
2130 #[derive(serde::Deserialize)]
2131 struct LegacyTerminalEntry {
2132 exit_code: Option<i32>,
2133 exit_signal: Option<i32>,
2134 at_ms: u64,
2135 disposition: TerminalDisposition,
2136 }
2137
2138 let decoded: LegacyTerminalEntry =
2139 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
2140 assert_eq!(decoded.exit_code, Some(1));
2141 assert_eq!(decoded.exit_signal, None);
2142 assert_eq!(decoded.at_ms, 1_700_000_000_123);
2143 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
2144
2145 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
2146 let future: TerminalEntry =
2147 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
2148 assert_eq!(
2149 future.exit_kind,
2150 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
2151 );
2152 }
2153
2154 #[test]
2155 fn terminal_incarnation_is_optional_for_older_daemons() {
2156 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
2157 "at_ms": 123,
2158 "disposition": "stopped"
2159 }))
2160 .unwrap();
2161 let encoded = serde_json::to_value(&entry).unwrap();
2162 assert_eq!(
2163 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
2164 (None, None)
2165 );
2166 }
2167
2168 #[test]
2169 fn route_poll_uses_kind_field() {
2170 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
2171 route_channel: 7,
2172 route_epoch: 11,
2173 kind: PollKind::Status,
2174 })
2175 .unwrap();
2176
2177 assert_eq!(body["op"], "route.poll");
2178 assert_eq!(body["route_epoch"], 11);
2179 assert_eq!(body["kind"], "status");
2180 assert!(body.get("op").is_some());
2181 }
2182
2183 #[test]
2184 fn route_open_is_internally_tagged() {
2185 let request = ClientControlRequest::RouteOpen {
2186 target: RouteTarget::ToolProvider {
2187 module_id: "aft".to_string(),
2188 },
2189 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
2190 consumer_identity: None,
2191 consumer_capabilities: None,
2192 admission_facts: None,
2193 };
2194
2195 let body = serde_json::to_value(request).unwrap();
2196 assert_eq!(body["op"], "route.open");
2197 assert_eq!(body["target"]["kind"], "tool_provider");
2198 assert!(body.get("consumer_identity").is_none());
2199 assert!(body.get("consumer_capabilities").is_none());
2200 }
2201
2202 #[test]
2203 fn route_open_without_optional_fields_still_decodes() {
2204 let body = serde_json::json!({
2205 "op": "route.open",
2206 "target": { "kind": "tool_provider", "module_id": "aft" },
2207 "identity": {
2208 "project_root": "/tmp/project",
2209 "harness": "opencode",
2210 "session": "session-1"
2211 }
2212 });
2213
2214 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
2215 let ClientControlRequest::RouteOpen {
2216 consumer_identity,
2217 consumer_capabilities,
2218 admission_facts,
2219 ..
2220 } = decoded
2221 else {
2222 panic!("decoded wrong request variant");
2223 };
2224 assert_eq!(consumer_identity, None);
2225 assert_eq!(consumer_capabilities, None);
2226 assert_eq!(admission_facts, None);
2227 }
2228
2229 #[test]
2230 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
2231 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
2232 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
2233 match decoded {
2234 ClientControlPush::RouteClosed {
2235 excluded_subscriptions,
2236 terminal,
2237 ..
2238 } => {
2239 assert_eq!(excluded_subscriptions, 0);
2240 assert_eq!(terminal, None);
2241 }
2242 other => panic!("unexpected push: {other:?}"),
2243 }
2244 assert!(!serde_json::to_string(&decoded)
2245 .unwrap()
2246 .contains("terminal"));
2247 }
2248
2249 #[test]
2250 fn old_route_closed_decoder_ignores_new_terminal_field() {
2251 #[derive(serde::Deserialize)]
2252 #[serde(tag = "op")]
2253 enum LegacyClientControlPush {
2254 #[serde(rename = "route.closed")]
2255 RouteClosed {
2256 module_id: String,
2257 reason: RouteCloseReason,
2258 drained: bool,
2259 abandoned: u32,
2260 },
2261 }
2262
2263 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
2264 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
2265 match decoded {
2266 LegacyClientControlPush::RouteClosed {
2267 module_id,
2268 reason,
2269 drained,
2270 abandoned,
2271 } => {
2272 assert_eq!(module_id, "aft-tools");
2273 assert_eq!(reason, RouteCloseReason::Crash);
2274 assert!(!drained);
2275 assert_eq!(abandoned, 0);
2276 }
2277 }
2278 }
2279
2280 #[test]
2281 fn supervisor_routes_is_a_control_plane_request() {
2282 let body = serde_json::json!({
2283 "op": "supervisor.routes",
2284 "module_id": "aft"
2285 });
2286
2287 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
2288 assert_eq!(serde_json::to_value(request).unwrap(), body);
2289 }
2290
2291 #[test]
2292 fn diagnostic_string_enums_retain_unknown_wire_values() {
2293 let reason: RunningImageUnavailableReason =
2294 serde_json::from_str("\"future_reason\"").unwrap();
2295 let disposition: TerminalDisposition =
2296 serde_json::from_str("\"future_disposition\"").unwrap();
2297
2298 assert_eq!(
2299 reason,
2300 RunningImageUnavailableReason::Unknown("future_reason".to_string())
2301 );
2302 assert_eq!(
2303 disposition,
2304 TerminalDisposition::Unknown("future_disposition".to_string())
2305 );
2306 }
2307
2308 #[test]
2309 fn diagnostic_string_enums_preserve_existing_wire_names() {
2310 let names = [
2311 (RunningImageUnavailableReason::NotRunning, "not_running"),
2312 (
2313 RunningImageUnavailableReason::UnsupportedPlatform,
2314 "unsupported_platform",
2315 ),
2316 (
2317 RunningImageUnavailableReason::RunningExecutableUnreadable,
2318 "running_executable_unreadable",
2319 ),
2320 (
2321 RunningImageUnavailableReason::SpawnedPathUnreadable,
2322 "spawned_path_unreadable",
2323 ),
2324 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2325 (
2326 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2327 "process_identity_unconfirmed",
2328 ),
2329 ];
2330 for (value, expected) in names {
2331 let wire = serde_json::to_string(&value).unwrap();
2332 assert_eq!(wire, format!("\"{expected}\""));
2333 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2334 assert_eq!(decoded, value);
2335 }
2336
2337 for (value, expected) in [
2338 (TerminalDisposition::Stopped, "stopped"),
2339 (TerminalDisposition::Disabled, "disabled"),
2340 (TerminalDisposition::Failed, "failed"),
2341 (TerminalDisposition::Restarting, "restarting"),
2342 (TerminalDisposition::DaemonShutdown, "daemon_shutdown"),
2343 ] {
2344 let wire = serde_json::to_string(&value).unwrap();
2345 assert_eq!(wire, format!("\"{expected}\""));
2346 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2347 assert_eq!(decoded, value);
2348 }
2349 }
2350
2351 #[test]
2352 fn diagnostic_string_enums_reject_non_string_bodies() {
2353 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2354 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2355 }
2356
2357 #[test]
2358 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2359 let body = serde_json::json!({
2360 "op": "supervisor.provenance",
2361 "daemon": {
2362 "daemon_build": {},
2363 "daemon_observed": {
2364 "running_image": {
2365 "status": "unavailable",
2366 "reason": "not_running"
2367 }
2368 }
2369 },
2370 "modules": [
2371 {
2372 "module_id": "future",
2373 "module_declared": { "status": "unverifiable" },
2374 "daemon_observed": {
2375 "running_image": {
2376 "status": "unavailable",
2377 "reason": "future_reason"
2378 }
2379 }
2380 },
2381 {
2382 "module_id": "healthy-a",
2383 "module_declared": { "status": "unverifiable" },
2384 "daemon_observed": {
2385 "running_image": {
2386 "status": "match",
2387 "evidence": {
2388 "method": "linux_proc_sha256",
2389 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2390 }
2391 }
2392 }
2393 },
2394 {
2395 "module_id": "healthy-b",
2396 "module_declared": { "status": "unverifiable" },
2397 "daemon_observed": {
2398 "running_image": {
2399 "status": "unavailable",
2400 "reason": "unsupported_platform"
2401 }
2402 }
2403 }
2404 ]
2405 });
2406
2407 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2408 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2409 panic!("decoded wrong response variant");
2410 };
2411 assert_eq!(modules.len(), 3);
2412 assert_eq!(modules[0].module_id, "future");
2413 assert_eq!(
2414 modules[0].daemon_observed.running_image,
2415 RunningImageAgreement::Unavailable {
2416 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2417 }
2418 );
2419 assert_eq!(modules[1].module_id, "healthy-a");
2420 assert_eq!(modules[2].module_id, "healthy-b");
2421 }
2422
2423 #[test]
2424 fn tagged_unknown_values_retain_tag_and_body() {
2425 macro_rules! assert_unknown_round_trip {
2426 ($ty:ident, $field:literal, $value:expr) => {
2427 let value = $value;
2428 let wire = serde_json::to_string(&value).unwrap();
2429 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2430 match decoded {
2431 $ty::Unknown { tag, body } => {
2432 assert_eq!(tag, value[$field].as_str().unwrap());
2433 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2434 }
2435 _ => panic!("decoded known variant"),
2436 }
2437 };
2438 }
2439
2440 assert_unknown_round_trip!(
2441 ModuleDeclaredProvenance,
2442 "status",
2443 serde_json::json!({"status": "future", "build": {"version": 7}})
2444 );
2445 assert_unknown_round_trip!(
2446 RunningImageAgreement,
2447 "status",
2448 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2449 );
2450 assert_unknown_round_trip!(
2451 RunningImageEvidence,
2452 "method",
2453 serde_json::json!({"method": "future", "digest": "abc"})
2454 );
2455 assert_unknown_round_trip!(
2456 SupervisorRouteConsumer,
2457 "kind",
2458 serde_json::json!({"kind": "future", "module_id": "m"})
2459 );
2460 assert_unknown_round_trip!(
2461 StderrCaptureState,
2462 "state",
2463 serde_json::json!({"state": "future", "reason": "because"})
2464 );
2465 assert_unknown_round_trip!(
2466 StderrTailEntry,
2467 "kind",
2468 serde_json::json!({"kind": "future", "text": "line"})
2469 );
2470 assert_unknown_round_trip!(
2471 ChildResourceUsage,
2472 "status",
2473 serde_json::json!({"status": "future", "memory_bytes": 1})
2474 );
2475 }
2476
2477 #[test]
2478 fn child_resource_usage_round_trips_both_known_states() {
2479 let measured = ChildResourceUsage::Measured(ChildResourceReading {
2480 memory_bytes: 0,
2481 memory_kind: ChildMemoryKind::ResidentSet,
2482 swap_bytes: Some(0),
2483 cpu_user_ms: 0,
2484 cpu_system_ms: 0,
2485 });
2486 let wire = serde_json::to_value(&measured).unwrap();
2487 assert_eq!(
2488 wire,
2489 serde_json::json!({
2490 "status": "measured",
2491 "memory_bytes": 0,
2492 "memory_kind": "resident_set",
2493 "swap_bytes": 0,
2494 "cpu_user_ms": 0,
2495 "cpu_system_ms": 0
2496 })
2497 );
2498 assert_eq!(
2499 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2500 measured
2501 );
2502
2503 let unavailable = ChildResourceUsage::Unavailable {
2504 reason: ChildResourceUnavailableReason::NotRunning,
2505 };
2506 let wire = serde_json::to_value(&unavailable).unwrap();
2507 assert_eq!(
2508 wire,
2509 serde_json::json!({"status": "unavailable", "reason": "not_running"})
2510 );
2511 assert_eq!(
2512 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2513 unavailable
2514 );
2515 }
2516
2517 #[test]
2518 fn tagged_unknown_values_round_trip_the_original_json() {
2519 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2520 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2521 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2522 }
2523
2524 #[test]
2525 fn tagged_unknown_values_round_trip_trailing_tag() {
2526 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2527 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2528 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2529
2530 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2531 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2532 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2533 }
2534
2535 #[test]
2536 fn tagged_unknown_values_round_trip_middle_tag() {
2537 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2538 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2539 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2540
2541 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2542 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2543 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2544 }
2545
2546 #[test]
2547 fn tagged_unknown_values_round_trip_deep_payload() {
2548 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2549 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2550 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2551
2552 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2553 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2554 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2555 }
2556
2557 #[test]
2558 fn tagged_unknown_values_reject_non_object_bodies() {
2559 for wire in ["42", r#""future""#, "[]"] {
2560 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2561 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2562 }
2563 }
2564
2565 #[test]
2566 fn duplicate_discriminators_reject_without_panicking() {
2567 assert_eq!(
2568 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2569 .unwrap(),
2570 ModuleDeclaredProvenance::Unverifiable
2571 );
2572 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2573 .unwrap()
2574 {
2575 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2576 _ => panic!("future discriminator decoded as a known variant"),
2577 }
2578
2579 let wires = [
2580 r#"{"status":"reported","status":"unverifiable"}"#,
2581 r#"{"status":"unverifiable","status":"reported"}"#,
2582 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2583 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2584 ];
2585
2586 for wire in wires {
2587 let result =
2588 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2589 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2590 assert!(
2591 result.unwrap().is_err(),
2592 "duplicate discriminator decoded: {wire}"
2593 );
2594 }
2595
2596 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2597 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2598 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2599 assert!(
2600 result.unwrap().is_err(),
2601 "duplicate discriminator decoded: {wire}"
2602 );
2603 }
2604
2605 #[test]
2606 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2607 let known_wire =
2608 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2609 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2610 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2611
2612 for wire in [
2613 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2614 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2615 ] {
2616 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2617 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2618 }
2619
2620 for wire in [
2621 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2622 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2623 ] {
2624 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2625 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2626 }
2627
2628 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2629 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2630 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2631
2632 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2633 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2634 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2635 }
2636
2637 #[test]
2638 fn tagged_unknown_member_does_not_discard_known_siblings() {
2639 let body = serde_json::json!({
2640 "modules": [{
2641 "module_id": "target",
2642 "routes": [
2643 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2644 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2645 ]
2646 }]
2647 });
2648 let decoded: ClientControlResponse = serde_json::from_value(
2649 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2650 )
2651 .unwrap();
2652 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2653 panic!("decoded wrong response variant");
2654 };
2655 assert_eq!(modules[0].routes.len(), 2);
2656 assert_eq!(
2657 modules[0].routes[1].consumer,
2658 SupervisorRouteConsumer::Direct { connection_id: 7 }
2659 );
2660 }
2661}
2662
2663#[cfg(test)]
2664mod launch_nonce_redaction_tests {
2665 use super::*;
2666
2667 const NONCE: &str = "nonce-f00dfeed1234abcd";
2668
2669 fn identity() -> ConsumerIdentity {
2670 ConsumerIdentity {
2671 module_id: "wernicke".to_string(),
2672 launch_nonce: NONCE.to_string(),
2673 }
2674 }
2675
2676 #[test]
2677 fn consumer_identity_debug_names_the_module_and_never_the_nonce() {
2678 let printed = format!("{:?}", identity());
2679 assert!(printed.contains("wernicke"), "{printed}");
2680 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
2681 }
2682
2683 #[test]
2684 fn route_open_request_debug_never_prints_the_nonce() {
2685 let request = ClientControlRequest::RouteOpen {
2686 target: subc_protocol::RouteTarget::ToolProvider {
2687 module_id: "broca".to_string(),
2688 },
2689 identity: subc_protocol::BindIdentity::new(
2690 PathBuf::from("/tmp/project"),
2691 "test".to_string(),
2692 "session".to_string(),
2693 ),
2694 consumer_identity: Some(identity()),
2695 consumer_capabilities: None,
2696 admission_facts: None,
2697 };
2698 let printed = format!("{request:?}");
2699 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
2700 }
2701}