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 scope::ScopeSelector,
20 session::HealthStatus,
21 BindIdentity, RouteTarget,
22};
23
24pub use subc_protocol::RouteCloseReason;
25
26macro_rules! open_string_enum {
27 (
28 $(#[$meta:meta])*
29 $name:ident {
30 $( $(#[$variant_meta:meta])* $variant:ident => $wire_name:literal ),+ $(,)?
31 }
32 ) => {
33 $(#[$meta])*
34 #[derive(Debug, Clone, PartialEq, Eq)]
35 pub enum $name {
36 $( $(#[$variant_meta])* $variant, )+
37 Unknown(String),
38 }
39
40 impl $name {
41 fn wire_name(&self) -> &str {
42 match self {
43 $( Self::$variant => $wire_name, )+
44 Self::Unknown(value) => value,
45 }
46 }
47 }
48
49 impl Serialize for $name {
50 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
51 where
52 S: serde::Serializer,
53 {
54 serializer.serialize_str(self.wire_name())
55 }
56 }
57
58 impl<'de> Deserialize<'de> for $name {
59 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
60 where
61 D: serde::Deserializer<'de>,
62 {
63 let value = String::deserialize(deserializer)?;
64 Ok(match value.as_str() {
65 $( $wire_name => Self::$variant, )+
66 _ => Self::Unknown(value),
67 })
68 }
69 }
70 };
71}
72
73#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
75pub struct ConsumerIdentity {
76 pub module_id: String,
77 pub launch_nonce: String,
78}
79
80impl std::fmt::Debug for ConsumerIdentity {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("ConsumerIdentity")
87 .field("module_id", &self.module_id)
88 .field(
89 "launch_nonce",
90 &format_args!("<{} bytes redacted>", self.launch_nonce.len()),
91 )
92 .finish()
93 }
94}
95
96pub mod ops {
107 pub const SERVER: &str = "server.";
108 pub const CATALOG: &str = "catalog.";
109 pub const ROUTE: &str = "route.";
110 pub const SUPERVISOR: &str = "supervisor.";
111 pub const CONFIG: &str = "config.";
112
113 pub const SERVER_DESCRIBE: &str = "server.describe";
114 pub const CATALOG_LIST: &str = "catalog.list";
115 pub const ROUTE_OPEN: &str = "route.open";
116 pub const ROUTE_POLL: &str = "route.poll";
117 pub const ROUTE_CLOSING: &str = "route.closing";
118 pub const ROUTE_CLOSED: &str = "route.closed";
119 pub const SUPERVISOR_LIST: &str = "supervisor.list";
120 pub const SUPERVISOR_RESTART: &str = "supervisor.restart";
121 pub const SUPERVISOR_SWAP: &str = "supervisor.swap";
122 pub const SUPERVISOR_RELOAD: &str = "supervisor.reload";
123 pub const SUPERVISOR_RESCAN: &str = "supervisor.rescan";
124 pub const SUPERVISOR_RELEASE_RESERVED: &str = "supervisor.release_reserved";
125 pub const SUPERVISOR_SET_ENABLED: &str = "supervisor.set_enabled";
126 pub const SUPERVISOR_HEALTH_PROBE: &str = "supervisor.health_probe";
127 pub const SUPERVISOR_HEALTH: &str = "supervisor.health";
128 pub const SUPERVISOR_STDERR_TAIL: &str = "supervisor.stderr_tail";
129 pub const SUPERVISOR_TERMINALS: &str = "supervisor.terminals";
130 pub const SUPERVISOR_ROUTES: &str = "supervisor.routes";
131 pub const SUPERVISOR_PROVENANCE: &str = "supervisor.provenance";
132 pub const SUPERVISOR_SPAWN_SNAPSHOT: &str = "supervisor.spawn_snapshot";
133 pub const SUPERVISOR_SPAWN_SUBSCRIBE: &str = "supervisor.spawn_subscribe";
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
138#[serde(tag = "op")]
139#[allow(clippy::large_enum_variant)]
142pub enum ClientControlRequest {
143 #[serde(rename = "server.describe")]
144 ServerDescribe {},
145 #[serde(rename = "catalog.list")]
146 CatalogList {
147 #[serde(default)]
152 module_id: Option<String>,
153 },
154 #[serde(rename = "route.open")]
155 RouteOpen {
156 target: RouteTarget,
157 identity: BindIdentity,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
167 consumer_identity: Option<ConsumerIdentity>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
176 consumer_capabilities: Option<Vec<String>>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
179 admission_facts: Option<serde_json::Value>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
187 scope: Option<ScopeSelector>,
188 },
189 #[serde(rename = "route.poll")]
190 RoutePoll {
191 route_channel: u16,
192 route_epoch: u32,
193 kind: PollKind,
194 },
195 #[serde(rename = "supervisor.list")]
196 SupervisorList {},
197 #[serde(rename = "supervisor.spawn_snapshot")]
199 SupervisorSpawnSnapshot {},
200 #[serde(rename = "supervisor.spawn_subscribe")]
216 SupervisorSpawnSubscribe {
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 since: Option<SpawnCursor>,
219 },
220 #[serde(rename = "supervisor.restart")]
221 SupervisorRestart {
222 module_id: String,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
231 drain_timeout_ms: Option<u64>,
232 },
233 #[serde(rename = "supervisor.swap")]
248 SupervisorSwap {
249 module_id: String,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
253 ready_timeout_ms: Option<u64>,
254 },
255 #[serde(rename = "supervisor.reload")]
256 SupervisorReload { module_id: String },
257 #[serde(rename = "supervisor.rescan")]
258 SupervisorRescan {
259 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
279 preview: bool,
280 },
281 #[serde(rename = "supervisor.release_reserved")]
285 SupervisorReleaseReserved { module_id: String },
286 #[serde(rename = "supervisor.set_enabled")]
287 SupervisorSetEnabled { module_id: String, enabled: bool },
288 #[serde(rename = "supervisor.health_probe")]
289 SupervisorHealthProbe { module_id: String },
290 #[serde(rename = "supervisor.health")]
291 SupervisorHealth {},
292 #[serde(rename = "supervisor.routes")]
305 SupervisorRoutes {
306 #[serde(default, skip_serializing_if = "Option::is_none")]
307 module_id: Option<String>,
308 },
309 #[serde(rename = "supervisor.provenance")]
312 SupervisorProvenance {
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 module_id: Option<String>,
315 },
316 #[serde(rename = "supervisor.stderr_tail")]
324 SupervisorStderrTail {
325 module_id: String,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
327 max_lines: Option<u32>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 max_bytes: Option<u32>,
330 },
331 #[serde(rename = "supervisor.terminals")]
344 SupervisorTerminals { module_id: String },
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
349#[serde(tag = "op")]
350pub enum ClientControlResponse {
351 #[serde(rename = "server.describe")]
352 ServerDescribe {
353 protocol_ver: u8,
354 subc_ops: Vec<String>,
355 capabilities: Vec<String>,
356 connected_clients: u64,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 counters: Option<serde_json::Value>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
366 build_git_sha: Option<String>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
373 build_lock_digest: Option<String>,
374 #[serde(default, skip_serializing_if = "Vec::is_empty")]
378 capability_requirements: Vec<CapabilityRequirementStatus>,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
384 machine_id: Option<String>,
385 },
386 #[serde(rename = "catalog.list")]
387 CatalogList {
388 generation: u64,
389 modules: Vec<CatalogEntry>,
390 subc_ops: Vec<String>,
391 },
392 #[serde(rename = "route.open")]
393 RouteOpen {
394 route_channel: u16,
395 route_epoch: u32,
396 },
397 #[serde(rename = "route.poll")]
398 RoutePoll {
399 route_channel: u16,
400 route_epoch: u32,
401 status: Option<String>,
402 live: Option<bool>,
403 },
404 #[serde(rename = "supervisor.list")]
405 SupervisorList {
406 generation: u64,
407 modules: Vec<SupervisorEntry>,
408 },
409 #[serde(rename = "supervisor.spawn_snapshot")]
410 SupervisorSpawnSnapshot {
411 #[serde(flatten)]
412 snapshot: SpawnSnapshot,
413 },
414 #[serde(rename = "supervisor.ack")]
415 SupervisorAck { module_id: String, applied: bool },
416 #[serde(rename = "supervisor.rescan")]
417 SupervisorRescan {
418 #[serde(flatten)]
419 result: SupervisorRescanResult,
420 },
421 #[serde(rename = "supervisor.health_probe")]
422 SupervisorHealthProbe {
423 module_id: String,
424 status: HealthStatus,
425 #[serde(default, skip_serializing_if = "Option::is_none")]
426 detail: Option<String>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
428 metrics: Option<serde_json::Value>,
429 },
430 #[serde(rename = "supervisor.health")]
431 SupervisorHealth {
432 generation: u64,
433 modules: Vec<SupervisorHealthEntry>,
434 },
435 #[serde(rename = "supervisor.routes")]
436 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
437 #[serde(rename = "supervisor.provenance")]
438 SupervisorProvenance {
439 daemon: SupervisorDaemonProvenance,
440 modules: Vec<SupervisorModuleProvenance>,
441 },
442 #[serde(rename = "supervisor.stderr_tail")]
443 SupervisorStderrTail {
444 module_id: String,
445 #[serde(flatten)]
446 tail: StderrTail,
447 },
448 #[serde(rename = "supervisor.terminals")]
449 SupervisorTerminals {
450 module_id: String,
451 #[serde(flatten)]
452 terminals: TerminalHistory,
453 },
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
461#[serde(tag = "op")]
462pub enum ClientControlPush {
463 #[serde(rename = "route.closing")]
464 RouteClosing {
465 module_id: String,
466 reason: RouteCloseReason,
467 },
468 #[serde(rename = "route.closed")]
469 RouteClosed {
470 module_id: String,
471 reason: RouteCloseReason,
472 drained: bool,
474 abandoned: u32,
477 #[serde(default)]
479 excluded_subscriptions: u32,
480 #[serde(default, skip_serializing_if = "Option::is_none")]
486 terminal: Option<bool>,
487 },
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
492pub struct SpawnCursor {
493 pub daemon_incarnation: String,
494 pub seq: u64,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
499pub struct LiveSpawn {
500 pub module_id: String,
501 pub spawn_generation: u64,
502 pub pid: u32,
503 pub spawned_at_ms: u64,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
508pub struct SpawnSnapshot {
509 pub cursor: SpawnCursor,
510 pub ring_bound: u64,
512 pub live: Vec<LiveSpawn>,
513}
514
515#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
517#[serde(rename_all = "snake_case")]
518pub enum SpawnEventKind {
519 Spawned,
520 Exited,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
529pub struct SpawnEvent {
530 pub cursor: SpawnCursor,
531 pub kind: SpawnEventKind,
532 pub module_id: String,
533 pub spawn_generation: u64,
534 pub pid: u32,
535 #[serde(default, skip_serializing_if = "Option::is_none")]
536 pub exit_code: Option<i32>,
537 #[serde(default, skip_serializing_if = "Option::is_none")]
538 pub exit_signal: Option<i32>,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
543pub struct StderrTail {
544 pub capture: StderrCaptureState,
545 pub entries: Vec<StderrTailEntry>,
546 #[serde(default, skip_serializing_if = "is_zero_u64")]
555 pub dropped_lines: u64,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
560pub struct SupervisorRouteModule {
561 pub module_id: String,
562 pub routes: Vec<SupervisorRoute>,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
567pub struct SupervisorRoute {
568 pub consumer: SupervisorRouteConsumer,
569 pub age_ms: u64,
571 pub draining: bool,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
580 pub drain_reason: Option<RouteCloseReason>,
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
585pub struct SupervisorModuleProvenance {
586 pub module_id: String,
587 pub module_declared: ModuleDeclaredProvenance,
588 pub daemon_observed: SupervisorObservedProcess,
589}
590
591#[derive(Debug, Clone, PartialEq)]
593pub enum ModuleDeclaredProvenance {
594 Reported {
595 build: ManifestProvenance,
596 },
597 Unverifiable,
598 Unknown {
601 tag: String,
602 body: OrderedJsonObject,
603 },
604}
605
606#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
611pub struct SupervisorObservedProcess {
612 #[serde(default, skip_serializing_if = "Option::is_none")]
613 pub pid: Option<u32>,
614 #[serde(default, skip_serializing_if = "Option::is_none")]
615 pub spawned_at_ms: Option<u64>,
616 #[serde(default, skip_serializing_if = "Option::is_none")]
617 pub spawned_from: Option<PathBuf>,
618 pub running_image: RunningImageAgreement,
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
624pub struct PendingReloadVerdict {
625 pub path: ReloadPathAgreement,
626 pub image: RunningImageAgreement,
627}
628
629#[derive(Debug, Clone, PartialEq)]
631pub enum ReloadPathAgreement {
632 Match,
633 Mismatch {
634 configured: PathBuf,
635 spawned_from: PathBuf,
636 },
637 Unavailable {
638 reason: ReloadPathUnavailableReason,
639 },
640 Unknown {
641 tag: String,
642 body: OrderedJsonObject,
643 },
644}
645
646open_string_enum! {
647 ReloadPathUnavailableReason {
649 NotRunning => "not_running",
650 SpawnedPathUnavailable => "spawned_path_unavailable",
651 }
652}
653
654#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
656pub struct SupervisorDaemonProvenance {
657 pub daemon_build: DaemonBuildProvenance,
658 pub daemon_observed: DaemonObservedProcess,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
663pub struct DaemonBuildProvenance {
664 #[serde(default, skip_serializing_if = "Option::is_none")]
665 pub build_git_sha: Option<String>,
666 #[serde(default, skip_serializing_if = "Option::is_none")]
667 pub build_lock_digest: Option<String>,
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
672pub struct DaemonObservedProcess {
673 #[serde(default, skip_serializing_if = "Option::is_none")]
674 pub pid: Option<u32>,
675 #[serde(default, skip_serializing_if = "Option::is_none")]
680 pub started_at_ms: Option<u64>,
681 pub running_image: RunningImageAgreement,
682}
683
684#[derive(Debug, Clone, PartialEq)]
686pub enum RunningImageAgreement {
687 Match {
688 evidence: RunningImageEvidence,
689 },
690 Mismatch {
691 running: RunningImageEvidence,
692 disk: RunningImageEvidence,
693 },
694 Unavailable {
695 reason: RunningImageUnavailableReason,
696 },
697 Unknown {
700 tag: String,
701 body: OrderedJsonObject,
702 },
703}
704
705#[derive(Debug, Clone, PartialEq)]
707pub enum RunningImageEvidence {
708 LinuxProcSha256 {
709 digest: String,
710 },
711 MacosSpawnInode {
712 device: u64,
713 inode: u64,
714 },
715 Unknown {
718 tag: String,
719 body: OrderedJsonObject,
720 },
721}
722
723open_string_enum! {
724 RunningImageUnavailableReason {
726 NotRunning => "not_running",
727 UnsupportedPlatform => "unsupported_platform",
728 RunningExecutableUnreadable => "running_executable_unreadable",
729 SpawnedPathUnreadable => "spawned_path_unreadable",
730 HashFailed => "hash_failed",
731 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
732 }
733}
734
735#[derive(Debug, Clone, PartialEq)]
741pub enum SupervisorRouteConsumer {
742 Reserved {
743 module_id: String,
744 },
745 Direct {
746 connection_id: u64,
747 },
748 Unknown {
751 tag: String,
752 body: OrderedJsonObject,
753 },
754}
755
756#[derive(Debug, Clone, PartialEq)]
763pub enum StderrCaptureState {
764 Captured,
767 Incomplete { reason: String },
769 NotCaptured { reason: String },
771 Unknown {
774 tag: String,
775 body: OrderedJsonObject,
776 },
777}
778
779#[derive(Debug, Clone, PartialEq)]
780pub enum StderrTailEntry {
781 Line {
782 text: String,
783 truncated: bool,
788 at_ms: Option<u64>,
792 },
793 ProcessStart,
798 Unknown {
801 tag: String,
802 body: OrderedJsonObject,
803 },
804}
805
806#[derive(Debug, Serialize, Deserialize)]
807#[serde(tag = "status", rename_all = "snake_case")]
808enum ModuleDeclaredProvenanceWire {
809 Reported { build: ManifestProvenance },
810 Unverifiable,
811}
812
813#[derive(Debug, Serialize, Deserialize)]
814#[serde(tag = "status", rename_all = "snake_case")]
815enum RunningImageAgreementWire {
816 Match {
817 evidence: RunningImageEvidence,
818 },
819 Mismatch {
820 running: RunningImageEvidence,
821 disk: RunningImageEvidence,
822 },
823 Unavailable {
824 reason: RunningImageUnavailableReason,
825 },
826}
827
828#[derive(Debug, Serialize, Deserialize)]
829#[serde(tag = "status", rename_all = "snake_case")]
830enum ReloadPathAgreementWire {
831 Match,
832 Mismatch {
833 configured: PathBuf,
834 spawned_from: PathBuf,
835 },
836 Unavailable {
837 reason: ReloadPathUnavailableReason,
838 },
839}
840
841#[derive(Debug, Serialize, Deserialize)]
842#[serde(tag = "method", rename_all = "snake_case")]
843enum RunningImageEvidenceWire {
844 LinuxProcSha256 { digest: String },
845 MacosSpawnInode { device: u64, inode: u64 },
846}
847
848#[derive(Debug, Serialize, Deserialize)]
849#[serde(tag = "kind", rename_all = "snake_case")]
850enum SupervisorRouteConsumerWire {
851 Reserved { module_id: String },
852 Direct { connection_id: u64 },
853}
854
855#[derive(Debug, Serialize, Deserialize)]
856#[serde(tag = "state", rename_all = "snake_case")]
857enum StderrCaptureStateWire {
858 Captured,
859 Incomplete { reason: String },
860 NotCaptured { reason: String },
861}
862
863#[derive(Debug, Serialize, Deserialize)]
864#[serde(tag = "status", rename_all = "snake_case")]
865enum ChildResourceUsageWire {
866 Measured(ChildResourceReading),
867 Unavailable {
868 reason: ChildResourceUnavailableReason,
869 },
870}
871
872#[derive(Debug, Serialize, Deserialize)]
873#[serde(tag = "kind", rename_all = "snake_case")]
874enum StderrTailEntryWire {
875 Line {
876 text: String,
877 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
878 truncated: bool,
879 #[serde(default, skip_serializing_if = "Option::is_none")]
882 at_ms: Option<u64>,
883 },
884 ProcessStart,
885}
886
887#[derive(Debug, Clone, PartialEq)]
889pub enum OrderedJsonValue {
890 Null,
891 Bool(bool),
892 Number(serde_json::Number),
893 String(String),
894 Array(Vec<Self>),
895 Object(OrderedJsonObject),
896}
897
898#[derive(Debug, Clone, PartialEq)]
900pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
901
902impl OrderedJsonObject {
903 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
905 &self.0
906 }
907
908 fn into_value(self) -> serde_json::Value {
909 serde_json::Value::Object(
910 self.0
911 .into_iter()
912 .map(|(key, value)| (key, value.into_value()))
913 .collect(),
914 )
915 }
916}
917
918impl OrderedJsonValue {
919 fn into_value(self) -> serde_json::Value {
920 match self {
921 Self::Null => serde_json::Value::Null,
922 Self::Bool(value) => serde_json::Value::Bool(value),
923 Self::Number(value) => serde_json::Value::Number(value),
924 Self::String(value) => serde_json::Value::String(value),
925 Self::Array(values) => {
926 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
927 }
928 Self::Object(value) => value.into_value(),
929 }
930 }
931}
932
933impl Serialize for OrderedJsonValue {
934 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
935 where
936 S: Serializer,
937 {
938 match self {
939 Self::Null => serializer.serialize_unit(),
940 Self::Bool(value) => serializer.serialize_bool(*value),
941 Self::Number(value) => value.serialize(serializer),
942 Self::String(value) => serializer.serialize_str(value),
943 Self::Array(values) => values.serialize(serializer),
944 Self::Object(value) => value.serialize(serializer),
945 }
946 }
947}
948
949impl<'de> Deserialize<'de> for OrderedJsonValue {
950 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
951 where
952 D: Deserializer<'de>,
953 {
954 struct OrderedValueVisitor;
955
956 impl<'de> Visitor<'de> for OrderedValueVisitor {
957 type Value = OrderedJsonValue;
958
959 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
960 formatter.write_str("a JSON value with ordered object members")
961 }
962
963 fn visit_unit<E>(self) -> Result<Self::Value, E>
964 where
965 E: serde::de::Error,
966 {
967 Ok(OrderedJsonValue::Null)
968 }
969
970 fn visit_none<E>(self) -> Result<Self::Value, E>
971 where
972 E: serde::de::Error,
973 {
974 Ok(OrderedJsonValue::Null)
975 }
976
977 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
978 where
979 D: Deserializer<'de>,
980 {
981 OrderedJsonValue::deserialize(deserializer)
982 }
983
984 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
985 where
986 E: serde::de::Error,
987 {
988 Ok(OrderedJsonValue::Bool(value))
989 }
990
991 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
992 where
993 E: serde::de::Error,
994 {
995 Ok(OrderedJsonValue::Number(value.into()))
996 }
997
998 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
999 where
1000 E: serde::de::Error,
1001 {
1002 Ok(OrderedJsonValue::Number(value.into()))
1003 }
1004
1005 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1006 where
1007 E: serde::de::Error,
1008 {
1009 serde_json::Number::from_f64(value)
1010 .map(OrderedJsonValue::Number)
1011 .ok_or_else(|| E::custom("non-finite JSON number"))
1012 }
1013
1014 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1015 where
1016 E: serde::de::Error,
1017 {
1018 Ok(OrderedJsonValue::String(value.to_owned()))
1019 }
1020
1021 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
1022 where
1023 E: serde::de::Error,
1024 {
1025 Ok(OrderedJsonValue::String(value))
1026 }
1027
1028 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1029 where
1030 A: SeqAccess<'de>,
1031 {
1032 let mut values = Vec::new();
1033 while let Some(value) = sequence.next_element()? {
1034 values.push(value);
1035 }
1036 Ok(OrderedJsonValue::Array(values))
1037 }
1038
1039 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1040 where
1041 A: MapAccess<'de>,
1042 {
1043 let mut entries = Vec::new();
1044 while let Some((key, value)) = map.next_entry()? {
1045 entries.push((key, value));
1046 }
1047 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
1048 }
1049 }
1050
1051 deserializer.deserialize_any(OrderedValueVisitor)
1052 }
1053}
1054
1055impl Serialize for OrderedJsonObject {
1056 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1057 where
1058 S: Serializer,
1059 {
1060 let mut map = serializer.serialize_map(Some(self.0.len()))?;
1061 for (key, value) in &self.0 {
1062 map.serialize_entry(key, value)?;
1063 }
1064 map.end()
1065 }
1066}
1067
1068impl<'de> Deserialize<'de> for OrderedJsonObject {
1069 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1070 where
1071 D: Deserializer<'de>,
1072 {
1073 struct OrderedObjectVisitor;
1074
1075 impl<'de> Visitor<'de> for OrderedObjectVisitor {
1076 type Value = OrderedJsonObject;
1077
1078 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1079 formatter.write_str("an object with ordered JSON members")
1080 }
1081
1082 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1083 where
1084 A: MapAccess<'de>,
1085 {
1086 let mut entries = Vec::new();
1087 while let Some((key, value)) = map.next_entry()? {
1088 entries.push((key, value));
1089 }
1090 Ok(OrderedJsonObject(entries))
1091 }
1092 }
1093
1094 deserializer.deserialize_map(OrderedObjectVisitor)
1095 }
1096}
1097
1098fn read_tagged<'de, D>(
1099 deserializer: D,
1100 field: &'static str,
1101) -> Result<(String, OrderedJsonObject), D::Error>
1102where
1103 D: Deserializer<'de>,
1104{
1105 let body = OrderedJsonObject::deserialize(deserializer)?;
1106 let mut tag = None;
1107 for (key, value) in body.as_entries() {
1108 if key != field {
1109 continue;
1110 }
1111 if tag.is_some() {
1112 return Err(D::Error::custom(format!(
1113 "tagged object has duplicate `{field}` field"
1114 )));
1115 }
1116 let OrderedJsonValue::String(value) = value else {
1117 return Err(D::Error::custom(format!(
1118 "tagged object has no string `{field}` field"
1119 )));
1120 };
1121 tag = Some(value);
1122 }
1123 let Some(tag) = tag else {
1124 return Err(D::Error::custom(format!(
1125 "tagged object has no string `{field}` field"
1126 )));
1127 };
1128 Ok((tag.to_string(), body))
1129}
1130
1131fn read_ordered_tagged(
1132 value: OrderedJsonValue,
1133 field: &'static str,
1134) -> Result<(String, OrderedJsonObject), String> {
1135 let OrderedJsonValue::Object(body) = value else {
1136 return Err(format!("expected tagged object with `{field}` field"));
1137 };
1138 let mut tag = None;
1139 for (key, value) in body.as_entries() {
1140 if key != field {
1141 continue;
1142 }
1143 if tag.is_some() {
1144 return Err(format!("tagged object has duplicate `{field}` field"));
1145 }
1146 let OrderedJsonValue::String(value) = value else {
1147 return Err(format!("tagged object has no string `{field}` field"));
1148 };
1149 tag = Some(value);
1150 }
1151 let Some(tag) = tag else {
1152 return Err(format!("tagged object has no string `{field}` field"));
1153 };
1154 Ok((tag.to_string(), body))
1155}
1156
1157fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1158 body.as_entries()
1159 .iter()
1160 .find_map(|(key, value)| (key == field).then_some(value))
1161}
1162
1163fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1164 match ordered_field(body, field) {
1165 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1166 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1167 None => Err(format!("tagged object has no `{field}` field")),
1168 }
1169}
1170
1171fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1172 let (tag, body) = read_ordered_tagged(value, "method")?;
1173 match tag.as_str() {
1174 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1175 digest: ordered_string(&body, "digest")?,
1176 }),
1177 "macos_spawn_inode" => {
1178 let device = ordered_field(&body, "device")
1179 .and_then(|value| match value {
1180 OrderedJsonValue::Number(number) => number.as_u64(),
1181 _ => None,
1182 })
1183 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1184 let inode = ordered_field(&body, "inode")
1185 .and_then(|value| match value {
1186 OrderedJsonValue::Number(number) => number.as_u64(),
1187 _ => None,
1188 })
1189 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
1190 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1191 }
1192 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1193 }
1194}
1195
1196impl Serialize for ModuleDeclaredProvenance {
1197 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1198 where
1199 S: Serializer,
1200 {
1201 match self {
1202 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1203 build: build.clone(),
1204 }
1205 .serialize(serializer),
1206 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1207 Self::Unknown { body, .. } => body.serialize(serializer),
1208 }
1209 }
1210}
1211
1212impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1213 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1214 where
1215 D: serde::Deserializer<'de>,
1216 {
1217 let (tag, value) = read_tagged(deserializer, "status")?;
1218 match tag.as_str() {
1219 "reported" => match serde_json::from_value(value.into_value())
1220 .map_err(D::Error::custom)?
1221 {
1222 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1223 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1224 },
1225 "unverifiable" => {
1226 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1227 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1228 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1229 }
1230 }
1231 _ => Ok(Self::Unknown { tag, body: value }),
1232 }
1233 }
1234}
1235
1236impl Serialize for RunningImageAgreement {
1237 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1238 where
1239 S: Serializer,
1240 {
1241 match self {
1242 Self::Match { evidence } => RunningImageAgreementWire::Match {
1243 evidence: evidence.clone(),
1244 }
1245 .serialize(serializer),
1246 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1247 running: running.clone(),
1248 disk: disk.clone(),
1249 }
1250 .serialize(serializer),
1251 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1252 reason: reason.clone(),
1253 }
1254 .serialize(serializer),
1255 Self::Unknown { body, .. } => body.serialize(serializer),
1256 }
1257 }
1258}
1259
1260impl Serialize for ReloadPathAgreement {
1261 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1262 where
1263 S: Serializer,
1264 {
1265 match self {
1266 Self::Match => ReloadPathAgreementWire::Match.serialize(serializer),
1267 Self::Mismatch {
1268 configured,
1269 spawned_from,
1270 } => ReloadPathAgreementWire::Mismatch {
1271 configured: configured.clone(),
1272 spawned_from: spawned_from.clone(),
1273 }
1274 .serialize(serializer),
1275 Self::Unavailable { reason } => ReloadPathAgreementWire::Unavailable {
1276 reason: reason.clone(),
1277 }
1278 .serialize(serializer),
1279 Self::Unknown { body, .. } => body.serialize(serializer),
1280 }
1281 }
1282}
1283
1284impl<'de> Deserialize<'de> for ReloadPathAgreement {
1285 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1286 where
1287 D: Deserializer<'de>,
1288 {
1289 let (tag, body) = read_tagged(deserializer, "status")?;
1290 match tag.as_str() {
1291 "match" => Ok(Self::Match),
1292 "mismatch" => {
1293 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1294 ReloadPathAgreementWire::Mismatch {
1295 configured,
1296 spawned_from,
1297 } => Ok(Self::Mismatch {
1298 configured,
1299 spawned_from,
1300 }),
1301 _ => unreachable!(),
1302 }
1303 }
1304 "unavailable" => match serde_json::from_value(body.into_value())
1305 .map_err(D::Error::custom)?
1306 {
1307 ReloadPathAgreementWire::Unavailable { reason } => Ok(Self::Unavailable { reason }),
1308 _ => unreachable!(),
1309 },
1310 _ => Ok(Self::Unknown { tag, body }),
1311 }
1312 }
1313}
1314
1315impl<'de> Deserialize<'de> for RunningImageAgreement {
1316 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1317 where
1318 D: serde::Deserializer<'de>,
1319 {
1320 let (tag, value) = read_tagged(deserializer, "status")?;
1321 match tag.as_str() {
1322 "match" => Ok(Self::Match {
1323 evidence: decode_running_image_evidence(
1324 ordered_field(&value, "evidence")
1325 .cloned()
1326 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1327 )
1328 .map_err(D::Error::custom)?,
1329 }),
1330 "mismatch" => Ok(Self::Mismatch {
1331 running: decode_running_image_evidence(
1332 ordered_field(&value, "running")
1333 .cloned()
1334 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1335 )
1336 .map_err(D::Error::custom)?,
1337 disk: decode_running_image_evidence(
1338 ordered_field(&value, "disk")
1339 .cloned()
1340 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1341 )
1342 .map_err(D::Error::custom)?,
1343 }),
1344 "unavailable" => Ok(Self::Unavailable {
1345 reason: serde_json::from_value(
1346 ordered_field(&value, "reason")
1347 .cloned()
1348 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1349 .into_value(),
1350 )
1351 .map_err(D::Error::custom)?,
1352 }),
1353 _ => Ok(Self::Unknown { tag, body: value }),
1354 }
1355 }
1356}
1357
1358impl Serialize for ChildResourceUsage {
1359 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1360 where
1361 S: Serializer,
1362 {
1363 match self {
1364 Self::Measured(reading) => {
1365 ChildResourceUsageWire::Measured(reading.clone()).serialize(serializer)
1366 }
1367 Self::Unavailable { reason } => ChildResourceUsageWire::Unavailable {
1368 reason: reason.clone(),
1369 }
1370 .serialize(serializer),
1371 Self::Unknown { body, .. } => body.serialize(serializer),
1372 }
1373 }
1374}
1375
1376impl<'de> Deserialize<'de> for ChildResourceUsage {
1377 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1378 where
1379 D: Deserializer<'de>,
1380 {
1381 let (tag, body) = read_tagged(deserializer, "status")?;
1382 match tag.as_str() {
1383 "measured" | "unavailable" => {
1384 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1385 ChildResourceUsageWire::Measured(reading) => Ok(Self::Measured(reading)),
1386 ChildResourceUsageWire::Unavailable { reason } => {
1387 Ok(Self::Unavailable { reason })
1388 }
1389 }
1390 }
1391 _ => Ok(Self::Unknown { tag, body }),
1392 }
1393 }
1394}
1395
1396impl Serialize for RunningImageEvidence {
1397 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1398 where
1399 S: Serializer,
1400 {
1401 match self {
1402 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1403 digest: digest.clone(),
1404 }
1405 .serialize(serializer),
1406 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1407 device: *device,
1408 inode: *inode,
1409 }
1410 .serialize(serializer),
1411 Self::Unknown { body, .. } => body.serialize(serializer),
1412 }
1413 }
1414}
1415
1416impl<'de> Deserialize<'de> for RunningImageEvidence {
1417 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1418 where
1419 D: serde::Deserializer<'de>,
1420 {
1421 let (tag, value) = read_tagged(deserializer, "method")?;
1422 match tag.as_str() {
1423 "linux_proc_sha256" => {
1424 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1425 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1426 Ok(Self::LinuxProcSha256 { digest })
1427 }
1428 _ => unreachable!(),
1429 }
1430 }
1431 "macos_spawn_inode" => {
1432 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1433 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1434 Ok(Self::MacosSpawnInode { device, inode })
1435 }
1436 _ => unreachable!(),
1437 }
1438 }
1439 _ => Ok(Self::Unknown { tag, body: value }),
1440 }
1441 }
1442}
1443
1444impl Serialize for SupervisorRouteConsumer {
1445 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1446 where
1447 S: Serializer,
1448 {
1449 match self {
1450 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1451 module_id: module_id.clone(),
1452 }
1453 .serialize(serializer),
1454 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1455 connection_id: *connection_id,
1456 }
1457 .serialize(serializer),
1458 Self::Unknown { body, .. } => body.serialize(serializer),
1459 }
1460 }
1461}
1462
1463impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1464 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1465 where
1466 D: serde::Deserializer<'de>,
1467 {
1468 let (tag, value) = read_tagged(deserializer, "kind")?;
1469 match tag.as_str() {
1470 "reserved" => {
1471 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1472 SupervisorRouteConsumerWire::Reserved { module_id } => {
1473 Ok(Self::Reserved { module_id })
1474 }
1475 _ => unreachable!(),
1476 }
1477 }
1478 "direct" => {
1479 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1480 SupervisorRouteConsumerWire::Direct { connection_id } => {
1481 Ok(Self::Direct { connection_id })
1482 }
1483 _ => unreachable!(),
1484 }
1485 }
1486 _ => Ok(Self::Unknown { tag, body: value }),
1487 }
1488 }
1489}
1490
1491impl Serialize for StderrCaptureState {
1492 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1493 where
1494 S: Serializer,
1495 {
1496 match self {
1497 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1498 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1499 reason: reason.clone(),
1500 }
1501 .serialize(serializer),
1502 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1503 reason: reason.clone(),
1504 }
1505 .serialize(serializer),
1506 Self::Unknown { body, .. } => body.serialize(serializer),
1507 }
1508 }
1509}
1510
1511impl<'de> Deserialize<'de> for StderrCaptureState {
1512 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1513 where
1514 D: serde::Deserializer<'de>,
1515 {
1516 let (tag, value) = read_tagged(deserializer, "state")?;
1517 match tag.as_str() {
1518 "captured" => {
1519 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1520 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1521 _ => unreachable!(),
1522 }
1523 }
1524 "incomplete" => match serde_json::from_value(value.into_value())
1525 .map_err(D::Error::custom)?
1526 {
1527 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1528 _ => unreachable!(),
1529 },
1530 "not_captured" => match serde_json::from_value(value.into_value())
1531 .map_err(D::Error::custom)?
1532 {
1533 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1534 _ => unreachable!(),
1535 },
1536 _ => Ok(Self::Unknown { tag, body: value }),
1537 }
1538 }
1539}
1540
1541impl Serialize for StderrTailEntry {
1542 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1543 where
1544 S: Serializer,
1545 {
1546 match self {
1547 Self::Line {
1548 text,
1549 truncated,
1550 at_ms,
1551 } => StderrTailEntryWire::Line {
1552 text: text.clone(),
1553 truncated: *truncated,
1554 at_ms: *at_ms,
1555 }
1556 .serialize(serializer),
1557 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1558 Self::Unknown { body, .. } => body.serialize(serializer),
1559 }
1560 }
1561}
1562
1563impl<'de> Deserialize<'de> for StderrTailEntry {
1564 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1565 where
1566 D: serde::Deserializer<'de>,
1567 {
1568 let (tag, value) = read_tagged(deserializer, "kind")?;
1569 match tag.as_str() {
1570 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1571 StderrTailEntryWire::Line {
1572 text,
1573 truncated,
1574 at_ms,
1575 } => Ok(Self::Line {
1576 text,
1577 truncated,
1578 at_ms,
1579 }),
1580 _ => unreachable!(),
1581 },
1582 "process_start" => {
1583 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1584 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1585 _ => unreachable!(),
1586 }
1587 }
1588 _ => Ok(Self::Unknown { tag, body: value }),
1589 }
1590 }
1591}
1592
1593fn is_zero_u64(value: &u64) -> bool {
1594 *value == 0
1595}
1596
1597fn default_true() -> bool {
1598 true
1599}
1600
1601#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1603pub struct TerminalHistory {
1604 pub daemon_started_at_ms: u64,
1606 pub entries: Vec<TerminalEntry>,
1607 #[serde(default, skip_serializing_if = "is_zero_u64")]
1610 pub dropped: u64,
1611 #[serde(default, skip_serializing_if = "is_zero_u64")]
1614 pub journal_skipped_lines: u64,
1615 #[serde(default, skip_serializing_if = "is_zero_u64")]
1617 pub journal_read_errors: u64,
1618 #[serde(default, skip_serializing_if = "is_zero_u64")]
1620 pub journal_write_failures: u64,
1621}
1622
1623#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1625pub struct TerminalEntry {
1626 #[serde(default, skip_serializing_if = "Option::is_none")]
1629 pub daemon_incarnation: Option<String>,
1630 #[serde(default, skip_serializing_if = "Option::is_none")]
1631 pub exit_code: Option<i32>,
1632 #[serde(default, skip_serializing_if = "Option::is_none")]
1633 pub exit_signal: Option<i32>,
1634 pub at_ms: u64,
1635 pub disposition: TerminalDisposition,
1636 #[serde(default, skip_serializing_if = "Option::is_none")]
1640 pub exit_kind: Option<TerminalExitKind>,
1641 #[serde(default, skip_serializing_if = "Option::is_none")]
1648 pub disposition_detail: Option<String>,
1649}
1650
1651#[derive(Debug, Clone, PartialEq, Eq)]
1656pub enum TerminalExitKind {
1657 Clean,
1658 Crash,
1659 DeliberateSeverance,
1660 Unknown(String),
1661}
1662
1663impl TerminalExitKind {
1664 fn wire_name(&self) -> &str {
1665 match self {
1666 Self::Clean => "clean",
1667 Self::Crash => "crash",
1668 Self::DeliberateSeverance => "deliberate_severance",
1669 Self::Unknown(value) => value,
1670 }
1671 }
1672}
1673
1674impl Serialize for TerminalExitKind {
1675 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1676 where
1677 S: serde::Serializer,
1678 {
1679 serializer.serialize_str(self.wire_name())
1680 }
1681}
1682
1683impl<'de> Deserialize<'de> for TerminalExitKind {
1684 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1685 where
1686 D: serde::Deserializer<'de>,
1687 {
1688 let value = String::deserialize(deserializer)?;
1689 Ok(match value.as_str() {
1690 "clean" => Self::Clean,
1691 "crash" => Self::Crash,
1692 "deliberate_severance" => Self::DeliberateSeverance,
1693 _ => Self::Unknown(value),
1694 })
1695 }
1696}
1697
1698open_string_enum! {
1699 TerminalDisposition {
1701 Stopped => "stopped",
1702 Disabled => "disabled",
1703 Failed => "failed",
1704 Restarting => "restarting",
1705 DaemonShutdown => "daemon_shutdown",
1710 }
1711}
1712
1713#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1714#[serde(rename_all = "snake_case")]
1715pub enum PollKind {
1716 Status,
1717 Liveness,
1718}
1719
1720#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1721pub struct CatalogEntry {
1722 pub module_id: String,
1723 #[serde(default = "default_true")]
1734 pub ready: bool,
1735 #[serde(default, skip_serializing_if = "Option::is_none")]
1739 pub not_ready: Option<NotReadyReason>,
1740 #[serde(default, skip_serializing_if = "Option::is_none")]
1761 pub module_version: Option<String>,
1762 pub roles: Vec<ProviderRole>,
1763 pub control_ops: Vec<String>,
1764 #[serde(default, skip_serializing_if = "Option::is_none")]
1769 pub capabilities: Option<CapabilityDeclarations>,
1770 #[serde(default, skip_serializing_if = "Option::is_none")]
1773 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1774}
1775
1776#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1778pub struct NotReadyReason {
1779 pub reason: String,
1784 #[serde(default, skip_serializing_if = "Option::is_none")]
1787 pub capability: Option<String>,
1788}
1789
1790impl NotReadyReason {
1791 pub const DECLARED_NOT_READY: &'static str = "declared_not_ready";
1792 pub const REQUIRED_CAPABILITY_UNPROVIDED: &'static str = "required_capability_unprovided";
1793}
1794
1795#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1796pub struct CapabilityRequirementStatus {
1797 pub consumer: String,
1798 pub capability: String,
1799 pub need: String,
1800 pub verdict: String,
1801 pub episode_seq: u64,
1802 pub config_satisfiable: bool,
1803 pub runtime_available: bool,
1804 pub detail: String,
1805}
1806
1807#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1808pub struct SupervisorRescanResult {
1809 pub added: Vec<String>,
1810 pub removed: Vec<String>,
1811 pub changed_pending_reload: Vec<String>,
1812 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1825 pub enabled_changes: Vec<String>,
1826 pub unchanged: u32,
1827 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1835 pub preview: bool,
1836 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1854 pub restart_required: Vec<String>,
1855 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1859 pub capability_warnings: Vec<String>,
1860}
1861
1862#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1878#[serde(rename_all = "snake_case")]
1879pub enum ModuleProtocol {
1880 #[default]
1884 Subc,
1885 None,
1894}
1895
1896#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1897pub struct SupervisorEntry {
1898 pub module_id: String,
1899 pub state: String,
1900 pub enabled: bool,
1901 pub live: bool,
1911 #[serde(default)]
1915 pub protocol: ModuleProtocol,
1916 #[serde(default, skip_serializing_if = "Option::is_none")]
1918 pub launch_nonce_env: Option<bool>,
1919 pub health: SupervisorHealthStatus,
1920 #[serde(default, skip_serializing_if = "Option::is_none")]
1923 pub pending_reload: Option<PendingReloadVerdict>,
1924 #[serde(default)]
1930 pub last_probe_ms: Option<u64>,
1931 #[serde(default, skip_serializing_if = "Option::is_none")]
1935 pub last_exit_code: Option<i32>,
1936 #[serde(default, skip_serializing_if = "Option::is_none")]
1940 pub last_exit_signal: Option<i32>,
1941 #[serde(default, skip_serializing_if = "Option::is_none")]
1945 pub last_exit_ms: Option<u64>,
1946 #[serde(default, skip_serializing_if = "Option::is_none")]
1949 pub last_exit_kind: Option<TerminalExitKind>,
1950 #[serde(default, skip_serializing_if = "Option::is_none")]
1967 pub restart_count: Option<u32>,
1968 #[serde(default, skip_serializing_if = "Option::is_none")]
1971 pub max_restarts: Option<u32>,
1972 #[serde(default, skip_serializing_if = "Option::is_none")]
1975 pub lifetime_restarts: Option<u32>,
1976 #[serde(default, skip_serializing_if = "Option::is_none")]
1980 pub spawn_generation: Option<u64>,
1981 #[serde(default, skip_serializing_if = "Option::is_none")]
1991 pub restart_window_secs: Option<u64>,
1992 #[serde(default, skip_serializing_if = "Option::is_none")]
1996 pub drain_timeout_ms: Option<u64>,
1997 #[serde(default, skip_serializing_if = "Option::is_none")]
2000 pub restart_backoff_ms: Option<u64>,
2001 #[serde(default, skip_serializing_if = "Option::is_none")]
2004 pub restart_max_backoff_ms: Option<u64>,
2005 #[serde(default, skip_serializing_if = "Option::is_none")]
2018 pub resources: Option<ChildResourceUsage>,
2019}
2020
2021#[derive(Debug, Clone, PartialEq)]
2024pub enum ChildResourceUsage {
2025 Measured(ChildResourceReading),
2026 Unavailable {
2027 reason: ChildResourceUnavailableReason,
2028 },
2029 Unknown {
2032 tag: String,
2033 body: OrderedJsonObject,
2034 },
2035}
2036
2037#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2039pub struct ChildResourceReading {
2040 pub memory_bytes: u64,
2043 pub memory_kind: ChildMemoryKind,
2044 #[serde(default, skip_serializing_if = "Option::is_none")]
2047 pub swap_bytes: Option<u64>,
2048 pub cpu_user_ms: u64,
2052 pub cpu_system_ms: u64,
2055}
2056
2057open_string_enum! {
2058 ChildMemoryKind {
2060 PhysFootprint => "phys_footprint",
2064 ResidentSet => "resident_set",
2067 }
2068}
2069
2070open_string_enum! {
2071 ChildResourceUnavailableReason {
2073 NotRunning => "not_running",
2075 UnsupportedPlatform => "unsupported_platform",
2077 Unreadable => "unreadable",
2080 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
2083 }
2084}
2085
2086#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2087#[serde(rename_all = "snake_case")]
2088pub enum SupervisorHealthStatus {
2089 Ok,
2090 Degraded,
2091 Failing,
2092 Unresponsive,
2093 Unknown,
2094}
2095
2096#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2097pub struct SupervisorHealthEntry {
2098 pub module_id: String,
2099 pub status: SupervisorHealthStatus,
2100 #[serde(default, skip_serializing_if = "Option::is_none")]
2106 pub detail: Option<String>,
2107 #[serde(default, skip_serializing_if = "Option::is_none")]
2112 pub metrics: Option<serde_json::Value>,
2113 pub consecutive_failures: u32,
2114 #[serde(default)]
2117 pub late_answer_count: u64,
2118 #[serde(default, skip_serializing_if = "Option::is_none")]
2120 pub last_late_answer_latency_ms: Option<u64>,
2121 #[serde(default)]
2126 pub last_action: Option<String>,
2127 #[serde(default)]
2130 pub last_action_ms: Option<u64>,
2131 #[serde(default, skip_serializing_if = "Option::is_none")]
2144 pub last_probe_ms: Option<u64>,
2145}
2146
2147#[cfg(test)]
2148mod tests {
2149 use super::*;
2150 use subc_protocol::{BindIdentity, RouteTarget};
2151
2152 #[test]
2153 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
2154 let entry = TerminalEntry {
2155 daemon_incarnation: Some("daemon-before-restart".into()),
2156 exit_code: Some(1),
2157 exit_signal: None,
2158 at_ms: 1_700_000_000_123,
2159 disposition: TerminalDisposition::Restarting,
2160 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
2161 disposition_detail: None,
2162 };
2163 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
2164 assert_eq!(
2165 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
2166 ["exit_kind"],
2167 "deliberate_severance"
2168 );
2169
2170 #[derive(serde::Deserialize)]
2171 struct LegacyTerminalEntry {
2172 exit_code: Option<i32>,
2173 exit_signal: Option<i32>,
2174 at_ms: u64,
2175 disposition: TerminalDisposition,
2176 }
2177
2178 let decoded: LegacyTerminalEntry =
2179 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
2180 assert_eq!(decoded.exit_code, Some(1));
2181 assert_eq!(decoded.exit_signal, None);
2182 assert_eq!(decoded.at_ms, 1_700_000_000_123);
2183 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
2184
2185 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
2186 let future: TerminalEntry =
2187 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
2188 assert_eq!(
2189 future.exit_kind,
2190 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
2191 );
2192 }
2193
2194 #[test]
2195 fn terminal_incarnation_is_optional_for_older_daemons() {
2196 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
2197 "at_ms": 123,
2198 "disposition": "stopped"
2199 }))
2200 .unwrap();
2201 let encoded = serde_json::to_value(&entry).unwrap();
2202 assert_eq!(
2203 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
2204 (None, None)
2205 );
2206 }
2207
2208 #[test]
2209 fn route_poll_uses_kind_field() {
2210 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
2211 route_channel: 7,
2212 route_epoch: 11,
2213 kind: PollKind::Status,
2214 })
2215 .unwrap();
2216
2217 assert_eq!(body["op"], "route.poll");
2218 assert_eq!(body["route_epoch"], 11);
2219 assert_eq!(body["kind"], "status");
2220 assert!(body.get("op").is_some());
2221 }
2222
2223 #[test]
2224 fn route_open_is_internally_tagged() {
2225 let request = ClientControlRequest::RouteOpen {
2226 target: RouteTarget::ToolProvider {
2227 module_id: "aft".to_string(),
2228 },
2229 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
2230 consumer_identity: None,
2231 consumer_capabilities: None,
2232 admission_facts: None,
2233 scope: None,
2234 };
2235
2236 let body = serde_json::to_value(request).unwrap();
2237 assert_eq!(body["op"], "route.open");
2238 assert_eq!(body["target"]["kind"], "tool_provider");
2239 assert!(body.get("consumer_identity").is_none());
2240 assert!(body.get("consumer_capabilities").is_none());
2241 }
2242
2243 #[test]
2244 fn route_open_without_optional_fields_still_decodes() {
2245 let body = serde_json::json!({
2246 "op": "route.open",
2247 "target": { "kind": "tool_provider", "module_id": "aft" },
2248 "identity": {
2249 "project_root": "/tmp/project",
2250 "harness": "opencode",
2251 "session": "session-1"
2252 }
2253 });
2254
2255 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
2256 let ClientControlRequest::RouteOpen {
2257 consumer_identity,
2258 consumer_capabilities,
2259 admission_facts,
2260 ..
2261 } = decoded
2262 else {
2263 panic!("decoded wrong request variant");
2264 };
2265 assert_eq!(consumer_identity, None);
2266 assert_eq!(consumer_capabilities, None);
2267 assert_eq!(admission_facts, None);
2268 }
2269
2270 #[test]
2271 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
2272 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
2273 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
2274 match decoded {
2275 ClientControlPush::RouteClosed {
2276 excluded_subscriptions,
2277 terminal,
2278 ..
2279 } => {
2280 assert_eq!(excluded_subscriptions, 0);
2281 assert_eq!(terminal, None);
2282 }
2283 other => panic!("unexpected push: {other:?}"),
2284 }
2285 assert!(!serde_json::to_string(&decoded)
2286 .unwrap()
2287 .contains("terminal"));
2288 }
2289
2290 #[test]
2291 fn old_route_closed_decoder_ignores_new_terminal_field() {
2292 #[derive(serde::Deserialize)]
2293 #[serde(tag = "op")]
2294 enum LegacyClientControlPush {
2295 #[serde(rename = "route.closed")]
2296 RouteClosed {
2297 module_id: String,
2298 reason: RouteCloseReason,
2299 drained: bool,
2300 abandoned: u32,
2301 },
2302 }
2303
2304 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
2305 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
2306 match decoded {
2307 LegacyClientControlPush::RouteClosed {
2308 module_id,
2309 reason,
2310 drained,
2311 abandoned,
2312 } => {
2313 assert_eq!(module_id, "aft-tools");
2314 assert_eq!(reason, RouteCloseReason::Crash);
2315 assert!(!drained);
2316 assert_eq!(abandoned, 0);
2317 }
2318 }
2319 }
2320
2321 #[test]
2322 fn supervisor_routes_is_a_control_plane_request() {
2323 let body = serde_json::json!({
2324 "op": "supervisor.routes",
2325 "module_id": "aft"
2326 });
2327
2328 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
2329 assert_eq!(serde_json::to_value(request).unwrap(), body);
2330 }
2331
2332 #[test]
2333 fn diagnostic_string_enums_retain_unknown_wire_values() {
2334 let reason: RunningImageUnavailableReason =
2335 serde_json::from_str("\"future_reason\"").unwrap();
2336 let disposition: TerminalDisposition =
2337 serde_json::from_str("\"future_disposition\"").unwrap();
2338
2339 assert_eq!(
2340 reason,
2341 RunningImageUnavailableReason::Unknown("future_reason".to_string())
2342 );
2343 assert_eq!(
2344 disposition,
2345 TerminalDisposition::Unknown("future_disposition".to_string())
2346 );
2347 }
2348
2349 #[test]
2350 fn diagnostic_string_enums_preserve_existing_wire_names() {
2351 let names = [
2352 (RunningImageUnavailableReason::NotRunning, "not_running"),
2353 (
2354 RunningImageUnavailableReason::UnsupportedPlatform,
2355 "unsupported_platform",
2356 ),
2357 (
2358 RunningImageUnavailableReason::RunningExecutableUnreadable,
2359 "running_executable_unreadable",
2360 ),
2361 (
2362 RunningImageUnavailableReason::SpawnedPathUnreadable,
2363 "spawned_path_unreadable",
2364 ),
2365 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2366 (
2367 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2368 "process_identity_unconfirmed",
2369 ),
2370 ];
2371 for (value, expected) in names {
2372 let wire = serde_json::to_string(&value).unwrap();
2373 assert_eq!(wire, format!("\"{expected}\""));
2374 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2375 assert_eq!(decoded, value);
2376 }
2377
2378 for (value, expected) in [
2379 (TerminalDisposition::Stopped, "stopped"),
2380 (TerminalDisposition::Disabled, "disabled"),
2381 (TerminalDisposition::Failed, "failed"),
2382 (TerminalDisposition::Restarting, "restarting"),
2383 (TerminalDisposition::DaemonShutdown, "daemon_shutdown"),
2384 ] {
2385 let wire = serde_json::to_string(&value).unwrap();
2386 assert_eq!(wire, format!("\"{expected}\""));
2387 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2388 assert_eq!(decoded, value);
2389 }
2390 }
2391
2392 #[test]
2393 fn diagnostic_string_enums_reject_non_string_bodies() {
2394 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2395 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2396 }
2397
2398 #[test]
2399 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2400 let body = serde_json::json!({
2401 "op": "supervisor.provenance",
2402 "daemon": {
2403 "daemon_build": {},
2404 "daemon_observed": {
2405 "running_image": {
2406 "status": "unavailable",
2407 "reason": "not_running"
2408 }
2409 }
2410 },
2411 "modules": [
2412 {
2413 "module_id": "future",
2414 "module_declared": { "status": "unverifiable" },
2415 "daemon_observed": {
2416 "running_image": {
2417 "status": "unavailable",
2418 "reason": "future_reason"
2419 }
2420 }
2421 },
2422 {
2423 "module_id": "healthy-a",
2424 "module_declared": { "status": "unverifiable" },
2425 "daemon_observed": {
2426 "running_image": {
2427 "status": "match",
2428 "evidence": {
2429 "method": "linux_proc_sha256",
2430 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2431 }
2432 }
2433 }
2434 },
2435 {
2436 "module_id": "healthy-b",
2437 "module_declared": { "status": "unverifiable" },
2438 "daemon_observed": {
2439 "running_image": {
2440 "status": "unavailable",
2441 "reason": "unsupported_platform"
2442 }
2443 }
2444 }
2445 ]
2446 });
2447
2448 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2449 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2450 panic!("decoded wrong response variant");
2451 };
2452 assert_eq!(modules.len(), 3);
2453 assert_eq!(modules[0].module_id, "future");
2454 assert_eq!(
2455 modules[0].daemon_observed.running_image,
2456 RunningImageAgreement::Unavailable {
2457 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2458 }
2459 );
2460 assert_eq!(modules[1].module_id, "healthy-a");
2461 assert_eq!(modules[2].module_id, "healthy-b");
2462 }
2463
2464 #[test]
2465 fn tagged_unknown_values_retain_tag_and_body() {
2466 macro_rules! assert_unknown_round_trip {
2467 ($ty:ident, $field:literal, $value:expr) => {
2468 let value = $value;
2469 let wire = serde_json::to_string(&value).unwrap();
2470 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2471 match decoded {
2472 $ty::Unknown { tag, body } => {
2473 assert_eq!(tag, value[$field].as_str().unwrap());
2474 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2475 }
2476 _ => panic!("decoded known variant"),
2477 }
2478 };
2479 }
2480
2481 assert_unknown_round_trip!(
2482 ModuleDeclaredProvenance,
2483 "status",
2484 serde_json::json!({"status": "future", "build": {"version": 7}})
2485 );
2486 assert_unknown_round_trip!(
2487 RunningImageAgreement,
2488 "status",
2489 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2490 );
2491 assert_unknown_round_trip!(
2492 RunningImageEvidence,
2493 "method",
2494 serde_json::json!({"method": "future", "digest": "abc"})
2495 );
2496 assert_unknown_round_trip!(
2497 SupervisorRouteConsumer,
2498 "kind",
2499 serde_json::json!({"kind": "future", "module_id": "m"})
2500 );
2501 assert_unknown_round_trip!(
2502 StderrCaptureState,
2503 "state",
2504 serde_json::json!({"state": "future", "reason": "because"})
2505 );
2506 assert_unknown_round_trip!(
2507 StderrTailEntry,
2508 "kind",
2509 serde_json::json!({"kind": "future", "text": "line"})
2510 );
2511 assert_unknown_round_trip!(
2512 ChildResourceUsage,
2513 "status",
2514 serde_json::json!({"status": "future", "memory_bytes": 1})
2515 );
2516 }
2517
2518 #[test]
2519 fn child_resource_usage_round_trips_both_known_states() {
2520 let measured = ChildResourceUsage::Measured(ChildResourceReading {
2521 memory_bytes: 0,
2522 memory_kind: ChildMemoryKind::ResidentSet,
2523 swap_bytes: Some(0),
2524 cpu_user_ms: 0,
2525 cpu_system_ms: 0,
2526 });
2527 let wire = serde_json::to_value(&measured).unwrap();
2528 assert_eq!(
2529 wire,
2530 serde_json::json!({
2531 "status": "measured",
2532 "memory_bytes": 0,
2533 "memory_kind": "resident_set",
2534 "swap_bytes": 0,
2535 "cpu_user_ms": 0,
2536 "cpu_system_ms": 0
2537 })
2538 );
2539 assert_eq!(
2540 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2541 measured
2542 );
2543
2544 let unavailable = ChildResourceUsage::Unavailable {
2545 reason: ChildResourceUnavailableReason::NotRunning,
2546 };
2547 let wire = serde_json::to_value(&unavailable).unwrap();
2548 assert_eq!(
2549 wire,
2550 serde_json::json!({"status": "unavailable", "reason": "not_running"})
2551 );
2552 assert_eq!(
2553 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2554 unavailable
2555 );
2556 }
2557
2558 #[test]
2559 fn a_stderr_line_decodes_with_and_without_its_capture_time() {
2560 let stamped: StderrTailEntry = serde_json::from_str(
2564 r#"{"kind":"line","text":"boom","truncated":true,"at_ms":1789801440685}"#,
2565 )
2566 .unwrap();
2567 assert_eq!(
2568 stamped,
2569 StderrTailEntry::Line {
2570 text: "boom".to_string(),
2571 truncated: true,
2572 at_ms: Some(1_789_801_440_685),
2573 }
2574 );
2575 let unstamped: StderrTailEntry =
2576 serde_json::from_str(r#"{"kind":"line","text":"boom"}"#).unwrap();
2577 assert_eq!(
2578 unstamped,
2579 StderrTailEntry::Line {
2580 text: "boom".to_string(),
2581 truncated: false,
2582 at_ms: None,
2583 }
2584 );
2585 assert_eq!(
2588 serde_json::to_string(&unstamped).unwrap(),
2589 r#"{"kind":"line","text":"boom"}"#
2590 );
2591 assert_eq!(
2592 serde_json::to_value(&stamped).unwrap()["at_ms"],
2593 serde_json::json!(1_789_801_440_685u64)
2594 );
2595 }
2596
2597 #[test]
2598 fn tagged_unknown_values_round_trip_the_original_json() {
2599 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2600 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2601 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2602 }
2603
2604 #[test]
2605 fn tagged_unknown_values_round_trip_trailing_tag() {
2606 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2607 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2608 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2609
2610 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2611 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2612 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2613 }
2614
2615 #[test]
2616 fn tagged_unknown_values_round_trip_middle_tag() {
2617 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2618 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2619 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2620
2621 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2622 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2623 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2624 }
2625
2626 #[test]
2627 fn tagged_unknown_values_round_trip_deep_payload() {
2628 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2629 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2630 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2631
2632 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2633 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2634 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2635 }
2636
2637 #[test]
2638 fn tagged_unknown_values_reject_non_object_bodies() {
2639 for wire in ["42", r#""future""#, "[]"] {
2640 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2641 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2642 }
2643 }
2644
2645 #[test]
2646 fn duplicate_discriminators_reject_without_panicking() {
2647 assert_eq!(
2648 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2649 .unwrap(),
2650 ModuleDeclaredProvenance::Unverifiable
2651 );
2652 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2653 .unwrap()
2654 {
2655 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2656 _ => panic!("future discriminator decoded as a known variant"),
2657 }
2658
2659 let wires = [
2660 r#"{"status":"reported","status":"unverifiable"}"#,
2661 r#"{"status":"unverifiable","status":"reported"}"#,
2662 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2663 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2664 ];
2665
2666 for wire in wires {
2667 let result =
2668 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2669 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2670 assert!(
2671 result.unwrap().is_err(),
2672 "duplicate discriminator decoded: {wire}"
2673 );
2674 }
2675
2676 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2677 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2678 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2679 assert!(
2680 result.unwrap().is_err(),
2681 "duplicate discriminator decoded: {wire}"
2682 );
2683 }
2684
2685 #[test]
2686 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2687 let known_wire =
2688 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2689 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2690 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2691
2692 for wire in [
2693 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2694 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2695 ] {
2696 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2697 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2698 }
2699
2700 for wire in [
2701 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2702 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2703 ] {
2704 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2705 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2706 }
2707
2708 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2709 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2710 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2711
2712 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2713 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2714 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2715 }
2716
2717 #[test]
2718 fn tagged_unknown_member_does_not_discard_known_siblings() {
2719 let body = serde_json::json!({
2720 "modules": [{
2721 "module_id": "target",
2722 "routes": [
2723 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2724 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2725 ]
2726 }]
2727 });
2728 let decoded: ClientControlResponse = serde_json::from_value(
2729 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2730 )
2731 .unwrap();
2732 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2733 panic!("decoded wrong response variant");
2734 };
2735 assert_eq!(modules[0].routes.len(), 2);
2736 assert_eq!(
2737 modules[0].routes[1].consumer,
2738 SupervisorRouteConsumer::Direct { connection_id: 7 }
2739 );
2740 }
2741}
2742
2743#[cfg(test)]
2744mod launch_nonce_redaction_tests {
2745 use super::*;
2746
2747 const NONCE: &str = "nonce-f00dfeed1234abcd";
2748
2749 fn identity() -> ConsumerIdentity {
2750 ConsumerIdentity {
2751 module_id: "wernicke".to_string(),
2752 launch_nonce: NONCE.to_string(),
2753 }
2754 }
2755
2756 #[test]
2757 fn consumer_identity_debug_names_the_module_and_never_the_nonce() {
2758 let printed = format!("{:?}", identity());
2759 assert!(printed.contains("wernicke"), "{printed}");
2760 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
2761 }
2762
2763 #[test]
2764 fn route_open_request_debug_never_prints_the_nonce() {
2765 let request = ClientControlRequest::RouteOpen {
2766 target: subc_protocol::RouteTarget::ToolProvider {
2767 module_id: "broca".to_string(),
2768 },
2769 identity: subc_protocol::BindIdentity::new(
2770 PathBuf::from("/tmp/project"),
2771 "test".to_string(),
2772 "session".to_string(),
2773 ),
2774 consumer_identity: Some(identity()),
2775 consumer_capabilities: None,
2776 admission_facts: None,
2777 scope: None,
2778 };
2779 let printed = format!("{request:?}");
2780 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
2781 }
2782}