1#![forbid(unsafe_code)]
9
10use std::{collections::BTreeMap, 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::{
19 CapabilityDeclarations, EventDeclaration, ManifestProvenance, ProviderRole,
20 SelfSignalDeclaration,
21 },
22 scope::ScopeSelector,
23 session::HealthStatus,
24 BindIdentity, RouteTarget,
25};
26
27pub use subc_protocol::RouteCloseReason;
28
29macro_rules! open_string_enum {
30 (
31 $(#[$meta:meta])*
32 $name:ident {
33 $( $(#[$variant_meta:meta])* $variant:ident => $wire_name:literal ),+ $(,)?
34 }
35 ) => {
36 $(#[$meta])*
37 #[derive(Debug, Clone, PartialEq, Eq)]
38 pub enum $name {
39 $( $(#[$variant_meta])* $variant, )+
40 Unknown(String),
41 }
42
43 impl $name {
44 fn wire_name(&self) -> &str {
45 match self {
46 $( Self::$variant => $wire_name, )+
47 Self::Unknown(value) => value,
48 }
49 }
50 }
51
52 impl Serialize for $name {
53 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
54 where
55 S: serde::Serializer,
56 {
57 serializer.serialize_str(self.wire_name())
58 }
59 }
60
61 impl<'de> Deserialize<'de> for $name {
62 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63 where
64 D: serde::Deserializer<'de>,
65 {
66 let value = String::deserialize(deserializer)?;
67 Ok(match value.as_str() {
68 $( $wire_name => Self::$variant, )+
69 _ => Self::Unknown(value),
70 })
71 }
72 }
73 };
74}
75
76#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
78pub struct ConsumerIdentity {
79 pub module_id: String,
80 pub launch_nonce: String,
81}
82
83impl std::fmt::Debug for ConsumerIdentity {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_struct("ConsumerIdentity")
90 .field("module_id", &self.module_id)
91 .field(
92 "launch_nonce",
93 &format_args!("<{} bytes redacted>", self.launch_nonce.len()),
94 )
95 .finish()
96 }
97}
98
99pub mod ops {
110 pub const SERVER: &str = "server.";
111 pub const CATALOG: &str = "catalog.";
112 pub const ROUTE: &str = "route.";
113 pub const SUPERVISOR: &str = "supervisor.";
114 pub const CONFIG: &str = "config.";
115
116 pub const SERVER_DESCRIBE: &str = "server.describe";
117 pub const CATALOG_LIST: &str = "catalog.list";
118 pub const ROUTE_OPEN: &str = "route.open";
119 pub const ROUTE_POLL: &str = "route.poll";
120 pub const ROUTE_CLOSING: &str = "route.closing";
121 pub const ROUTE_CLOSED: &str = "route.closed";
122 pub const SUPERVISOR_LIST: &str = "supervisor.list";
123 pub const SUPERVISOR_RESTART: &str = "supervisor.restart";
124 pub const SUPERVISOR_SWAP: &str = "supervisor.swap";
125 pub const SUPERVISOR_RELOAD: &str = "supervisor.reload";
126 pub const SUPERVISOR_RESCAN: &str = "supervisor.rescan";
127 pub const SUPERVISOR_RELEASE_RESERVED: &str = "supervisor.release_reserved";
128 pub const SUPERVISOR_SET_ENABLED: &str = "supervisor.set_enabled";
129 pub const SUPERVISOR_HEALTH_PROBE: &str = "supervisor.health_probe";
130 pub const SUPERVISOR_HEALTH: &str = "supervisor.health";
131 pub const SUPERVISOR_STDERR_TAIL: &str = "supervisor.stderr_tail";
132 pub const SUPERVISOR_TERMINALS: &str = "supervisor.terminals";
133 pub const SUPERVISOR_ROUTES: &str = "supervisor.routes";
134 pub const SUPERVISOR_PROVENANCE: &str = "supervisor.provenance";
135 pub const SUPERVISOR_SPAWN_SNAPSHOT: &str = "supervisor.spawn_snapshot";
136 pub const SUPERVISOR_SPAWN_SUBSCRIBE: &str = "supervisor.spawn_subscribe";
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
141#[serde(tag = "op")]
142#[allow(clippy::large_enum_variant)]
145pub enum ClientControlRequest {
146 #[serde(rename = "server.describe")]
147 ServerDescribe {},
148 #[serde(rename = "catalog.list")]
149 CatalogList {
150 #[serde(default)]
155 module_id: Option<String>,
156 },
157 #[serde(rename = "route.open")]
158 RouteOpen {
159 target: RouteTarget,
160 identity: BindIdentity,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
170 consumer_identity: Option<ConsumerIdentity>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
179 consumer_capabilities: Option<Vec<String>>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
191 role_versions: Option<BTreeMap<String, String>>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
194 admission_facts: Option<serde_json::Value>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
202 scope: Option<ScopeSelector>,
203 },
204 #[serde(rename = "route.poll")]
205 RoutePoll {
206 route_channel: u16,
207 route_epoch: u32,
208 kind: PollKind,
209 },
210 #[serde(rename = "supervisor.list")]
211 SupervisorList {},
212 #[serde(rename = "supervisor.spawn_snapshot")]
214 SupervisorSpawnSnapshot {},
215 #[serde(rename = "supervisor.spawn_subscribe")]
231 SupervisorSpawnSubscribe {
232 #[serde(default, skip_serializing_if = "Option::is_none")]
233 since: Option<SpawnCursor>,
234 },
235 #[serde(rename = "supervisor.restart")]
236 SupervisorRestart {
237 module_id: String,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
248 drain_timeout_ms: Option<u64>,
249 },
250 #[serde(rename = "supervisor.swap")]
265 SupervisorSwap {
266 module_id: String,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
270 ready_timeout_ms: Option<u64>,
271 },
272 #[serde(rename = "supervisor.reload")]
273 SupervisorReload { module_id: String },
274 #[serde(rename = "supervisor.rescan")]
275 SupervisorRescan {
276 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
296 preview: bool,
297 },
298 #[serde(rename = "supervisor.release_reserved")]
302 SupervisorReleaseReserved { module_id: String },
303 #[serde(rename = "supervisor.set_enabled")]
304 SupervisorSetEnabled { module_id: String, enabled: bool },
305 #[serde(rename = "supervisor.health_probe")]
306 SupervisorHealthProbe { module_id: String },
307 #[serde(rename = "supervisor.health")]
308 SupervisorHealth {},
309 #[serde(rename = "supervisor.routes")]
322 SupervisorRoutes {
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 module_id: Option<String>,
325 },
326 #[serde(rename = "supervisor.provenance")]
329 SupervisorProvenance {
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 module_id: Option<String>,
332 },
333 #[serde(rename = "supervisor.stderr_tail")]
341 SupervisorStderrTail {
342 module_id: String,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
344 max_lines: Option<u32>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 max_bytes: Option<u32>,
347 },
348 #[serde(rename = "supervisor.terminals")]
361 SupervisorTerminals { module_id: String },
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
366#[serde(tag = "op")]
367pub enum ClientControlResponse {
368 #[serde(rename = "server.describe")]
369 ServerDescribe {
370 protocol_ver: u8,
371 subc_ops: Vec<String>,
372 capabilities: Vec<String>,
373 connected_clients: u64,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 counters: Option<serde_json::Value>,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
383 build_git_sha: Option<String>,
384 #[serde(default, skip_serializing_if = "Option::is_none")]
390 build_lock_digest: Option<String>,
391 #[serde(default, skip_serializing_if = "Vec::is_empty")]
395 capability_requirements: Vec<CapabilityRequirementStatus>,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
401 machine_id: Option<String>,
402 },
403 #[serde(rename = "catalog.list")]
404 CatalogList {
405 generation: u64,
406 modules: Vec<CatalogEntry>,
407 subc_ops: Vec<String>,
408 },
409 #[serde(rename = "route.open")]
410 RouteOpen {
411 route_channel: u16,
412 route_epoch: u32,
413 },
414 #[serde(rename = "route.poll")]
415 RoutePoll {
416 route_channel: u16,
417 route_epoch: u32,
418 status: Option<String>,
419 live: Option<bool>,
420 },
421 #[serde(rename = "supervisor.list")]
422 SupervisorList {
423 generation: u64,
424 modules: Vec<SupervisorEntry>,
425 },
426 #[serde(rename = "supervisor.spawn_snapshot")]
427 SupervisorSpawnSnapshot {
428 #[serde(flatten)]
429 snapshot: SpawnSnapshot,
430 },
431 #[serde(rename = "supervisor.ack")]
432 SupervisorAck { module_id: String, applied: bool },
433 #[serde(rename = "supervisor.rescan")]
434 SupervisorRescan {
435 #[serde(flatten)]
436 result: SupervisorRescanResult,
437 },
438 #[serde(rename = "supervisor.health_probe")]
439 SupervisorHealthProbe {
440 module_id: String,
441 status: HealthStatus,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 detail: Option<String>,
444 #[serde(default, skip_serializing_if = "Option::is_none")]
445 metrics: Option<serde_json::Value>,
446 },
447 #[serde(rename = "supervisor.health")]
448 SupervisorHealth {
449 generation: u64,
450 modules: Vec<SupervisorHealthEntry>,
451 },
452 #[serde(rename = "supervisor.routes")]
453 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
454 #[serde(rename = "supervisor.provenance")]
455 SupervisorProvenance {
456 daemon: SupervisorDaemonProvenance,
457 modules: Vec<SupervisorModuleProvenance>,
458 },
459 #[serde(rename = "supervisor.stderr_tail")]
460 SupervisorStderrTail {
461 module_id: String,
462 #[serde(flatten)]
463 tail: StderrTail,
464 },
465 #[serde(rename = "supervisor.terminals")]
466 SupervisorTerminals {
467 module_id: String,
468 #[serde(flatten)]
469 terminals: TerminalHistory,
470 },
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
478#[serde(tag = "op")]
479pub enum ClientControlPush {
480 #[serde(rename = "route.closing")]
481 RouteClosing {
482 module_id: String,
483 #[serde(default)]
486 channels: Vec<u16>,
487 reason: RouteCloseReason,
488 },
489 #[serde(rename = "route.closed")]
490 RouteClosed {
491 module_id: String,
492 #[serde(default)]
495 channels: Vec<u16>,
496 reason: RouteCloseReason,
497 drained: bool,
499 abandoned: u32,
502 #[serde(default)]
504 excluded_subscriptions: u32,
505 #[serde(default, skip_serializing_if = "Option::is_none")]
511 terminal: Option<bool>,
512 },
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
517pub struct SpawnCursor {
518 pub daemon_incarnation: String,
519 pub seq: u64,
520}
521
522#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
524pub struct LiveSpawn {
525 pub module_id: String,
526 pub spawn_generation: u64,
527 pub pid: u32,
528 pub spawned_at_ms: u64,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
533pub struct SpawnSnapshot {
534 pub cursor: SpawnCursor,
535 pub ring_bound: u64,
537 pub live: Vec<LiveSpawn>,
538}
539
540#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
542#[serde(rename_all = "snake_case")]
543pub enum SpawnEventKind {
544 Spawned,
545 Exited,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
554pub struct SpawnEvent {
555 pub cursor: SpawnCursor,
556 pub kind: SpawnEventKind,
557 pub module_id: String,
558 pub spawn_generation: u64,
559 pub pid: u32,
560 #[serde(default, skip_serializing_if = "Option::is_none")]
561 pub exit_code: Option<i32>,
562 #[serde(default, skip_serializing_if = "Option::is_none")]
563 pub exit_signal: Option<i32>,
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
568pub struct StderrTail {
569 pub capture: StderrCaptureState,
570 pub entries: Vec<StderrTailEntry>,
571 #[serde(default, skip_serializing_if = "is_zero_u64")]
580 pub dropped_lines: u64,
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
585pub struct SupervisorRouteModule {
586 pub module_id: String,
587 pub routes: Vec<SupervisorRoute>,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
592pub struct SupervisorRoute {
593 pub consumer: SupervisorRouteConsumer,
594 pub age_ms: u64,
596 pub draining: bool,
599 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub drain_reason: Option<RouteCloseReason>,
606}
607
608#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
610pub struct SupervisorModuleProvenance {
611 pub module_id: String,
612 pub module_declared: ModuleDeclaredProvenance,
613 pub daemon_observed: SupervisorObservedProcess,
614}
615
616#[derive(Debug, Clone, PartialEq)]
618pub enum ModuleDeclaredProvenance {
619 Reported {
620 build: ManifestProvenance,
621 },
622 Unverifiable,
623 Unknown {
626 tag: String,
627 body: OrderedJsonObject,
628 },
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
636pub struct SupervisorObservedProcess {
637 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub pid: Option<u32>,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub spawned_at_ms: Option<u64>,
641 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub spawned_from: Option<PathBuf>,
643 pub running_image: RunningImageAgreement,
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
649pub struct PendingReloadVerdict {
650 pub path: ReloadPathAgreement,
651 pub image: RunningImageAgreement,
652}
653
654#[derive(Debug, Clone, PartialEq)]
656pub enum ReloadPathAgreement {
657 Match,
658 Mismatch {
659 configured: PathBuf,
660 spawned_from: PathBuf,
661 },
662 Unavailable {
663 reason: ReloadPathUnavailableReason,
664 },
665 Unknown {
666 tag: String,
667 body: OrderedJsonObject,
668 },
669}
670
671open_string_enum! {
672 ReloadPathUnavailableReason {
674 NotRunning => "not_running",
675 SpawnedPathUnavailable => "spawned_path_unavailable",
676 }
677}
678
679#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
681pub struct SupervisorDaemonProvenance {
682 pub daemon_build: DaemonBuildProvenance,
683 pub daemon_observed: DaemonObservedProcess,
684}
685
686#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
688pub struct DaemonBuildProvenance {
689 #[serde(default, skip_serializing_if = "Option::is_none")]
690 pub build_git_sha: Option<String>,
691 #[serde(default, skip_serializing_if = "Option::is_none")]
692 pub build_lock_digest: Option<String>,
693}
694
695#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
697pub struct DaemonObservedProcess {
698 #[serde(default, skip_serializing_if = "Option::is_none")]
699 pub pid: Option<u32>,
700 #[serde(default, skip_serializing_if = "Option::is_none")]
705 pub started_at_ms: Option<u64>,
706 pub running_image: RunningImageAgreement,
707}
708
709#[derive(Debug, Clone, PartialEq)]
711pub enum RunningImageAgreement {
712 Match {
713 evidence: RunningImageEvidence,
714 },
715 Mismatch {
716 running: RunningImageEvidence,
717 disk: RunningImageEvidence,
718 },
719 Unavailable {
720 reason: RunningImageUnavailableReason,
721 },
722 Unknown {
725 tag: String,
726 body: OrderedJsonObject,
727 },
728}
729
730#[derive(Debug, Clone, PartialEq)]
732pub enum RunningImageEvidence {
733 LinuxProcSha256 {
734 digest: String,
735 },
736 MacosSpawnInode {
737 device: u64,
738 inode: u64,
739 },
740 Unknown {
743 tag: String,
744 body: OrderedJsonObject,
745 },
746}
747
748open_string_enum! {
749 RunningImageUnavailableReason {
751 NotRunning => "not_running",
752 UnsupportedPlatform => "unsupported_platform",
753 RunningExecutableUnreadable => "running_executable_unreadable",
754 SpawnedPathUnreadable => "spawned_path_unreadable",
755 HashFailed => "hash_failed",
756 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
757 }
758}
759
760#[derive(Debug, Clone, PartialEq)]
766pub enum SupervisorRouteConsumer {
767 Reserved {
768 module_id: String,
769 },
770 Direct {
771 connection_id: u64,
772 },
773 Unknown {
776 tag: String,
777 body: OrderedJsonObject,
778 },
779}
780
781#[derive(Debug, Clone, PartialEq)]
788pub enum StderrCaptureState {
789 Captured,
792 Incomplete { reason: String },
794 NotCaptured { reason: String },
796 Unknown {
799 tag: String,
800 body: OrderedJsonObject,
801 },
802}
803
804#[derive(Debug, Clone, PartialEq)]
805pub enum StderrTailEntry {
806 Line {
807 text: String,
808 truncated: bool,
813 at_ms: Option<u64>,
817 },
818 ProcessStart,
823 Unknown {
826 tag: String,
827 body: OrderedJsonObject,
828 },
829}
830
831#[derive(Debug, Serialize, Deserialize)]
832#[serde(tag = "status", rename_all = "snake_case")]
833enum ModuleDeclaredProvenanceWire {
834 Reported { build: ManifestProvenance },
835 Unverifiable,
836}
837
838#[derive(Debug, Serialize, Deserialize)]
839#[serde(tag = "status", rename_all = "snake_case")]
840enum RunningImageAgreementWire {
841 Match {
842 evidence: RunningImageEvidence,
843 },
844 Mismatch {
845 running: RunningImageEvidence,
846 disk: RunningImageEvidence,
847 },
848 Unavailable {
849 reason: RunningImageUnavailableReason,
850 },
851}
852
853#[derive(Debug, Serialize, Deserialize)]
854#[serde(tag = "status", rename_all = "snake_case")]
855enum ReloadPathAgreementWire {
856 Match,
857 Mismatch {
858 configured: PathBuf,
859 spawned_from: PathBuf,
860 },
861 Unavailable {
862 reason: ReloadPathUnavailableReason,
863 },
864}
865
866#[derive(Debug, Serialize, Deserialize)]
867#[serde(tag = "method", rename_all = "snake_case")]
868enum RunningImageEvidenceWire {
869 LinuxProcSha256 { digest: String },
870 MacosSpawnInode { device: u64, inode: u64 },
871}
872
873#[derive(Debug, Serialize, Deserialize)]
874#[serde(tag = "kind", rename_all = "snake_case")]
875enum SupervisorRouteConsumerWire {
876 Reserved { module_id: String },
877 Direct { connection_id: u64 },
878}
879
880#[derive(Debug, Serialize, Deserialize)]
881#[serde(tag = "state", rename_all = "snake_case")]
882enum StderrCaptureStateWire {
883 Captured,
884 Incomplete { reason: String },
885 NotCaptured { reason: String },
886}
887
888#[derive(Debug, Serialize, Deserialize)]
889#[serde(tag = "status", rename_all = "snake_case")]
890enum ChildResourceUsageWire {
891 Measured(ChildResourceReading),
892 Unavailable {
893 reason: ChildResourceUnavailableReason,
894 },
895}
896
897#[derive(Debug, Serialize, Deserialize)]
898#[serde(tag = "kind", rename_all = "snake_case")]
899enum StderrTailEntryWire {
900 Line {
901 text: String,
902 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
903 truncated: bool,
904 #[serde(default, skip_serializing_if = "Option::is_none")]
907 at_ms: Option<u64>,
908 },
909 ProcessStart,
910}
911
912#[derive(Debug, Clone, PartialEq)]
914pub enum OrderedJsonValue {
915 Null,
916 Bool(bool),
917 Number(serde_json::Number),
918 String(String),
919 Array(Vec<Self>),
920 Object(OrderedJsonObject),
921}
922
923#[derive(Debug, Clone, PartialEq)]
925pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
926
927impl OrderedJsonObject {
928 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
930 &self.0
931 }
932
933 fn into_value(self) -> serde_json::Value {
934 serde_json::Value::Object(
935 self.0
936 .into_iter()
937 .map(|(key, value)| (key, value.into_value()))
938 .collect(),
939 )
940 }
941}
942
943impl OrderedJsonValue {
944 fn into_value(self) -> serde_json::Value {
945 match self {
946 Self::Null => serde_json::Value::Null,
947 Self::Bool(value) => serde_json::Value::Bool(value),
948 Self::Number(value) => serde_json::Value::Number(value),
949 Self::String(value) => serde_json::Value::String(value),
950 Self::Array(values) => {
951 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
952 }
953 Self::Object(value) => value.into_value(),
954 }
955 }
956}
957
958impl Serialize for OrderedJsonValue {
959 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
960 where
961 S: Serializer,
962 {
963 match self {
964 Self::Null => serializer.serialize_unit(),
965 Self::Bool(value) => serializer.serialize_bool(*value),
966 Self::Number(value) => value.serialize(serializer),
967 Self::String(value) => serializer.serialize_str(value),
968 Self::Array(values) => values.serialize(serializer),
969 Self::Object(value) => value.serialize(serializer),
970 }
971 }
972}
973
974impl<'de> Deserialize<'de> for OrderedJsonValue {
975 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
976 where
977 D: Deserializer<'de>,
978 {
979 struct OrderedValueVisitor;
980
981 impl<'de> Visitor<'de> for OrderedValueVisitor {
982 type Value = OrderedJsonValue;
983
984 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
985 formatter.write_str("a JSON value with ordered object members")
986 }
987
988 fn visit_unit<E>(self) -> Result<Self::Value, E>
989 where
990 E: serde::de::Error,
991 {
992 Ok(OrderedJsonValue::Null)
993 }
994
995 fn visit_none<E>(self) -> Result<Self::Value, E>
996 where
997 E: serde::de::Error,
998 {
999 Ok(OrderedJsonValue::Null)
1000 }
1001
1002 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1003 where
1004 D: Deserializer<'de>,
1005 {
1006 OrderedJsonValue::deserialize(deserializer)
1007 }
1008
1009 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
1010 where
1011 E: serde::de::Error,
1012 {
1013 Ok(OrderedJsonValue::Bool(value))
1014 }
1015
1016 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
1017 where
1018 E: serde::de::Error,
1019 {
1020 Ok(OrderedJsonValue::Number(value.into()))
1021 }
1022
1023 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
1024 where
1025 E: serde::de::Error,
1026 {
1027 Ok(OrderedJsonValue::Number(value.into()))
1028 }
1029
1030 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1031 where
1032 E: serde::de::Error,
1033 {
1034 serde_json::Number::from_f64(value)
1035 .map(OrderedJsonValue::Number)
1036 .ok_or_else(|| E::custom("non-finite JSON number"))
1037 }
1038
1039 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1040 where
1041 E: serde::de::Error,
1042 {
1043 Ok(OrderedJsonValue::String(value.to_owned()))
1044 }
1045
1046 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
1047 where
1048 E: serde::de::Error,
1049 {
1050 Ok(OrderedJsonValue::String(value))
1051 }
1052
1053 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1054 where
1055 A: SeqAccess<'de>,
1056 {
1057 let mut values = Vec::new();
1058 while let Some(value) = sequence.next_element()? {
1059 values.push(value);
1060 }
1061 Ok(OrderedJsonValue::Array(values))
1062 }
1063
1064 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1065 where
1066 A: MapAccess<'de>,
1067 {
1068 let mut entries = Vec::new();
1069 while let Some((key, value)) = map.next_entry()? {
1070 entries.push((key, value));
1071 }
1072 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
1073 }
1074 }
1075
1076 deserializer.deserialize_any(OrderedValueVisitor)
1077 }
1078}
1079
1080impl Serialize for OrderedJsonObject {
1081 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1082 where
1083 S: Serializer,
1084 {
1085 let mut map = serializer.serialize_map(Some(self.0.len()))?;
1086 for (key, value) in &self.0 {
1087 map.serialize_entry(key, value)?;
1088 }
1089 map.end()
1090 }
1091}
1092
1093impl<'de> Deserialize<'de> for OrderedJsonObject {
1094 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1095 where
1096 D: Deserializer<'de>,
1097 {
1098 struct OrderedObjectVisitor;
1099
1100 impl<'de> Visitor<'de> for OrderedObjectVisitor {
1101 type Value = OrderedJsonObject;
1102
1103 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1104 formatter.write_str("an object with ordered JSON members")
1105 }
1106
1107 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1108 where
1109 A: MapAccess<'de>,
1110 {
1111 let mut entries = Vec::new();
1112 while let Some((key, value)) = map.next_entry()? {
1113 entries.push((key, value));
1114 }
1115 Ok(OrderedJsonObject(entries))
1116 }
1117 }
1118
1119 deserializer.deserialize_map(OrderedObjectVisitor)
1120 }
1121}
1122
1123fn read_tagged<'de, D>(
1124 deserializer: D,
1125 field: &'static str,
1126) -> Result<(String, OrderedJsonObject), D::Error>
1127where
1128 D: Deserializer<'de>,
1129{
1130 let body = OrderedJsonObject::deserialize(deserializer)?;
1131 let mut tag = None;
1132 for (key, value) in body.as_entries() {
1133 if key != field {
1134 continue;
1135 }
1136 if tag.is_some() {
1137 return Err(D::Error::custom(format!(
1138 "tagged object has duplicate `{field}` field"
1139 )));
1140 }
1141 let OrderedJsonValue::String(value) = value else {
1142 return Err(D::Error::custom(format!(
1143 "tagged object has no string `{field}` field"
1144 )));
1145 };
1146 tag = Some(value);
1147 }
1148 let Some(tag) = tag else {
1149 return Err(D::Error::custom(format!(
1150 "tagged object has no string `{field}` field"
1151 )));
1152 };
1153 Ok((tag.to_string(), body))
1154}
1155
1156fn read_ordered_tagged(
1157 value: OrderedJsonValue,
1158 field: &'static str,
1159) -> Result<(String, OrderedJsonObject), String> {
1160 let OrderedJsonValue::Object(body) = value else {
1161 return Err(format!("expected tagged object with `{field}` field"));
1162 };
1163 let mut tag = None;
1164 for (key, value) in body.as_entries() {
1165 if key != field {
1166 continue;
1167 }
1168 if tag.is_some() {
1169 return Err(format!("tagged object has duplicate `{field}` field"));
1170 }
1171 let OrderedJsonValue::String(value) = value else {
1172 return Err(format!("tagged object has no string `{field}` field"));
1173 };
1174 tag = Some(value);
1175 }
1176 let Some(tag) = tag else {
1177 return Err(format!("tagged object has no string `{field}` field"));
1178 };
1179 Ok((tag.to_string(), body))
1180}
1181
1182fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1183 body.as_entries()
1184 .iter()
1185 .find_map(|(key, value)| (key == field).then_some(value))
1186}
1187
1188fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1189 match ordered_field(body, field) {
1190 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1191 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1192 None => Err(format!("tagged object has no `{field}` field")),
1193 }
1194}
1195
1196fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1197 let (tag, body) = read_ordered_tagged(value, "method")?;
1198 match tag.as_str() {
1199 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1200 digest: ordered_string(&body, "digest")?,
1201 }),
1202 "macos_spawn_inode" => {
1203 let device = ordered_field(&body, "device")
1204 .and_then(|value| match value {
1205 OrderedJsonValue::Number(number) => number.as_u64(),
1206 _ => None,
1207 })
1208 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1209 let inode = ordered_field(&body, "inode")
1210 .and_then(|value| match value {
1211 OrderedJsonValue::Number(number) => number.as_u64(),
1212 _ => None,
1213 })
1214 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
1215 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1216 }
1217 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1218 }
1219}
1220
1221impl Serialize for ModuleDeclaredProvenance {
1222 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1223 where
1224 S: Serializer,
1225 {
1226 match self {
1227 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1228 build: build.clone(),
1229 }
1230 .serialize(serializer),
1231 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1232 Self::Unknown { body, .. } => body.serialize(serializer),
1233 }
1234 }
1235}
1236
1237impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1238 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1239 where
1240 D: serde::Deserializer<'de>,
1241 {
1242 let (tag, value) = read_tagged(deserializer, "status")?;
1243 match tag.as_str() {
1244 "reported" => match serde_json::from_value(value.into_value())
1245 .map_err(D::Error::custom)?
1246 {
1247 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1248 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1249 },
1250 "unverifiable" => {
1251 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1252 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1253 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1254 }
1255 }
1256 _ => Ok(Self::Unknown { tag, body: value }),
1257 }
1258 }
1259}
1260
1261impl Serialize for RunningImageAgreement {
1262 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1263 where
1264 S: Serializer,
1265 {
1266 match self {
1267 Self::Match { evidence } => RunningImageAgreementWire::Match {
1268 evidence: evidence.clone(),
1269 }
1270 .serialize(serializer),
1271 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1272 running: running.clone(),
1273 disk: disk.clone(),
1274 }
1275 .serialize(serializer),
1276 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1277 reason: reason.clone(),
1278 }
1279 .serialize(serializer),
1280 Self::Unknown { body, .. } => body.serialize(serializer),
1281 }
1282 }
1283}
1284
1285impl Serialize for ReloadPathAgreement {
1286 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1287 where
1288 S: Serializer,
1289 {
1290 match self {
1291 Self::Match => ReloadPathAgreementWire::Match.serialize(serializer),
1292 Self::Mismatch {
1293 configured,
1294 spawned_from,
1295 } => ReloadPathAgreementWire::Mismatch {
1296 configured: configured.clone(),
1297 spawned_from: spawned_from.clone(),
1298 }
1299 .serialize(serializer),
1300 Self::Unavailable { reason } => ReloadPathAgreementWire::Unavailable {
1301 reason: reason.clone(),
1302 }
1303 .serialize(serializer),
1304 Self::Unknown { body, .. } => body.serialize(serializer),
1305 }
1306 }
1307}
1308
1309impl<'de> Deserialize<'de> for ReloadPathAgreement {
1310 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1311 where
1312 D: Deserializer<'de>,
1313 {
1314 let (tag, body) = read_tagged(deserializer, "status")?;
1315 match tag.as_str() {
1316 "match" => Ok(Self::Match),
1317 "mismatch" => {
1318 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1319 ReloadPathAgreementWire::Mismatch {
1320 configured,
1321 spawned_from,
1322 } => Ok(Self::Mismatch {
1323 configured,
1324 spawned_from,
1325 }),
1326 _ => unreachable!(),
1327 }
1328 }
1329 "unavailable" => match serde_json::from_value(body.into_value())
1330 .map_err(D::Error::custom)?
1331 {
1332 ReloadPathAgreementWire::Unavailable { reason } => Ok(Self::Unavailable { reason }),
1333 _ => unreachable!(),
1334 },
1335 _ => Ok(Self::Unknown { tag, body }),
1336 }
1337 }
1338}
1339
1340impl<'de> Deserialize<'de> for RunningImageAgreement {
1341 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1342 where
1343 D: serde::Deserializer<'de>,
1344 {
1345 let (tag, value) = read_tagged(deserializer, "status")?;
1346 match tag.as_str() {
1347 "match" => Ok(Self::Match {
1348 evidence: decode_running_image_evidence(
1349 ordered_field(&value, "evidence")
1350 .cloned()
1351 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1352 )
1353 .map_err(D::Error::custom)?,
1354 }),
1355 "mismatch" => Ok(Self::Mismatch {
1356 running: decode_running_image_evidence(
1357 ordered_field(&value, "running")
1358 .cloned()
1359 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1360 )
1361 .map_err(D::Error::custom)?,
1362 disk: decode_running_image_evidence(
1363 ordered_field(&value, "disk")
1364 .cloned()
1365 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1366 )
1367 .map_err(D::Error::custom)?,
1368 }),
1369 "unavailable" => Ok(Self::Unavailable {
1370 reason: serde_json::from_value(
1371 ordered_field(&value, "reason")
1372 .cloned()
1373 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1374 .into_value(),
1375 )
1376 .map_err(D::Error::custom)?,
1377 }),
1378 _ => Ok(Self::Unknown { tag, body: value }),
1379 }
1380 }
1381}
1382
1383impl Serialize for ChildResourceUsage {
1384 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1385 where
1386 S: Serializer,
1387 {
1388 match self {
1389 Self::Measured(reading) => {
1390 ChildResourceUsageWire::Measured(reading.clone()).serialize(serializer)
1391 }
1392 Self::Unavailable { reason } => ChildResourceUsageWire::Unavailable {
1393 reason: reason.clone(),
1394 }
1395 .serialize(serializer),
1396 Self::Unknown { body, .. } => body.serialize(serializer),
1397 }
1398 }
1399}
1400
1401impl<'de> Deserialize<'de> for ChildResourceUsage {
1402 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1403 where
1404 D: Deserializer<'de>,
1405 {
1406 let (tag, body) = read_tagged(deserializer, "status")?;
1407 match tag.as_str() {
1408 "measured" | "unavailable" => {
1409 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1410 ChildResourceUsageWire::Measured(reading) => Ok(Self::Measured(reading)),
1411 ChildResourceUsageWire::Unavailable { reason } => {
1412 Ok(Self::Unavailable { reason })
1413 }
1414 }
1415 }
1416 _ => Ok(Self::Unknown { tag, body }),
1417 }
1418 }
1419}
1420
1421impl Serialize for RunningImageEvidence {
1422 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1423 where
1424 S: Serializer,
1425 {
1426 match self {
1427 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1428 digest: digest.clone(),
1429 }
1430 .serialize(serializer),
1431 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1432 device: *device,
1433 inode: *inode,
1434 }
1435 .serialize(serializer),
1436 Self::Unknown { body, .. } => body.serialize(serializer),
1437 }
1438 }
1439}
1440
1441impl<'de> Deserialize<'de> for RunningImageEvidence {
1442 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1443 where
1444 D: serde::Deserializer<'de>,
1445 {
1446 let (tag, value) = read_tagged(deserializer, "method")?;
1447 match tag.as_str() {
1448 "linux_proc_sha256" => {
1449 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1450 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1451 Ok(Self::LinuxProcSha256 { digest })
1452 }
1453 _ => unreachable!(),
1454 }
1455 }
1456 "macos_spawn_inode" => {
1457 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1458 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1459 Ok(Self::MacosSpawnInode { device, inode })
1460 }
1461 _ => unreachable!(),
1462 }
1463 }
1464 _ => Ok(Self::Unknown { tag, body: value }),
1465 }
1466 }
1467}
1468
1469impl Serialize for SupervisorRouteConsumer {
1470 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1471 where
1472 S: Serializer,
1473 {
1474 match self {
1475 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1476 module_id: module_id.clone(),
1477 }
1478 .serialize(serializer),
1479 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1480 connection_id: *connection_id,
1481 }
1482 .serialize(serializer),
1483 Self::Unknown { body, .. } => body.serialize(serializer),
1484 }
1485 }
1486}
1487
1488impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1489 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1490 where
1491 D: serde::Deserializer<'de>,
1492 {
1493 let (tag, value) = read_tagged(deserializer, "kind")?;
1494 match tag.as_str() {
1495 "reserved" => {
1496 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1497 SupervisorRouteConsumerWire::Reserved { module_id } => {
1498 Ok(Self::Reserved { module_id })
1499 }
1500 _ => unreachable!(),
1501 }
1502 }
1503 "direct" => {
1504 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1505 SupervisorRouteConsumerWire::Direct { connection_id } => {
1506 Ok(Self::Direct { connection_id })
1507 }
1508 _ => unreachable!(),
1509 }
1510 }
1511 _ => Ok(Self::Unknown { tag, body: value }),
1512 }
1513 }
1514}
1515
1516impl Serialize for StderrCaptureState {
1517 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1518 where
1519 S: Serializer,
1520 {
1521 match self {
1522 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1523 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1524 reason: reason.clone(),
1525 }
1526 .serialize(serializer),
1527 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1528 reason: reason.clone(),
1529 }
1530 .serialize(serializer),
1531 Self::Unknown { body, .. } => body.serialize(serializer),
1532 }
1533 }
1534}
1535
1536impl<'de> Deserialize<'de> for StderrCaptureState {
1537 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1538 where
1539 D: serde::Deserializer<'de>,
1540 {
1541 let (tag, value) = read_tagged(deserializer, "state")?;
1542 match tag.as_str() {
1543 "captured" => {
1544 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1545 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1546 _ => unreachable!(),
1547 }
1548 }
1549 "incomplete" => match serde_json::from_value(value.into_value())
1550 .map_err(D::Error::custom)?
1551 {
1552 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1553 _ => unreachable!(),
1554 },
1555 "not_captured" => match serde_json::from_value(value.into_value())
1556 .map_err(D::Error::custom)?
1557 {
1558 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1559 _ => unreachable!(),
1560 },
1561 _ => Ok(Self::Unknown { tag, body: value }),
1562 }
1563 }
1564}
1565
1566impl Serialize for StderrTailEntry {
1567 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1568 where
1569 S: Serializer,
1570 {
1571 match self {
1572 Self::Line {
1573 text,
1574 truncated,
1575 at_ms,
1576 } => StderrTailEntryWire::Line {
1577 text: text.clone(),
1578 truncated: *truncated,
1579 at_ms: *at_ms,
1580 }
1581 .serialize(serializer),
1582 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1583 Self::Unknown { body, .. } => body.serialize(serializer),
1584 }
1585 }
1586}
1587
1588impl<'de> Deserialize<'de> for StderrTailEntry {
1589 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1590 where
1591 D: serde::Deserializer<'de>,
1592 {
1593 let (tag, value) = read_tagged(deserializer, "kind")?;
1594 match tag.as_str() {
1595 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1596 StderrTailEntryWire::Line {
1597 text,
1598 truncated,
1599 at_ms,
1600 } => Ok(Self::Line {
1601 text,
1602 truncated,
1603 at_ms,
1604 }),
1605 _ => unreachable!(),
1606 },
1607 "process_start" => {
1608 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1609 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1610 _ => unreachable!(),
1611 }
1612 }
1613 _ => Ok(Self::Unknown { tag, body: value }),
1614 }
1615 }
1616}
1617
1618fn is_zero_u64(value: &u64) -> bool {
1619 *value == 0
1620}
1621
1622fn default_true() -> bool {
1623 true
1624}
1625
1626#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1628pub struct TerminalHistory {
1629 pub daemon_started_at_ms: u64,
1631 pub entries: Vec<TerminalEntry>,
1632 #[serde(default, skip_serializing_if = "is_zero_u64")]
1635 pub dropped: u64,
1636 #[serde(default, skip_serializing_if = "is_zero_u64")]
1639 pub journal_skipped_lines: u64,
1640 #[serde(default, skip_serializing_if = "is_zero_u64")]
1642 pub journal_read_errors: u64,
1643 #[serde(default, skip_serializing_if = "is_zero_u64")]
1645 pub journal_write_failures: u64,
1646}
1647
1648#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1650pub struct TerminalEntry {
1651 #[serde(default, skip_serializing_if = "Option::is_none")]
1654 pub daemon_incarnation: Option<String>,
1655 #[serde(default, skip_serializing_if = "Option::is_none")]
1656 pub exit_code: Option<i32>,
1657 #[serde(default, skip_serializing_if = "Option::is_none")]
1658 pub exit_signal: Option<i32>,
1659 pub at_ms: u64,
1660 pub disposition: TerminalDisposition,
1661 #[serde(default, skip_serializing_if = "Option::is_none")]
1665 pub exit_kind: Option<TerminalExitKind>,
1666 #[serde(default, skip_serializing_if = "Option::is_none")]
1673 pub disposition_detail: Option<String>,
1674}
1675
1676#[derive(Debug, Clone, PartialEq, Eq)]
1681pub enum TerminalExitKind {
1682 Clean,
1683 Crash,
1684 DeliberateSeverance,
1685 Unknown(String),
1686}
1687
1688impl TerminalExitKind {
1689 fn wire_name(&self) -> &str {
1690 match self {
1691 Self::Clean => "clean",
1692 Self::Crash => "crash",
1693 Self::DeliberateSeverance => "deliberate_severance",
1694 Self::Unknown(value) => value,
1695 }
1696 }
1697}
1698
1699impl Serialize for TerminalExitKind {
1700 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1701 where
1702 S: serde::Serializer,
1703 {
1704 serializer.serialize_str(self.wire_name())
1705 }
1706}
1707
1708impl<'de> Deserialize<'de> for TerminalExitKind {
1709 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1710 where
1711 D: serde::Deserializer<'de>,
1712 {
1713 let value = String::deserialize(deserializer)?;
1714 Ok(match value.as_str() {
1715 "clean" => Self::Clean,
1716 "crash" => Self::Crash,
1717 "deliberate_severance" => Self::DeliberateSeverance,
1718 _ => Self::Unknown(value),
1719 })
1720 }
1721}
1722
1723open_string_enum! {
1724 TerminalDisposition {
1726 Stopped => "stopped",
1727 Disabled => "disabled",
1728 Failed => "failed",
1729 Restarting => "restarting",
1730 DaemonShutdown => "daemon_shutdown",
1735 }
1736}
1737
1738#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1739#[serde(rename_all = "snake_case")]
1740pub enum PollKind {
1741 Status,
1742 Liveness,
1743}
1744
1745#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1757#[non_exhaustive]
1758pub struct CatalogEntry {
1759 pub module_id: String,
1760 #[serde(default = "default_true")]
1771 pub ready: bool,
1772 #[serde(default, skip_serializing_if = "Option::is_none")]
1776 pub not_ready: Option<NotReadyReason>,
1777 #[serde(default, skip_serializing_if = "Option::is_none")]
1798 pub module_version: Option<String>,
1799 pub roles: Vec<ProviderRole>,
1800 pub control_ops: Vec<String>,
1801 #[serde(default, skip_serializing_if = "Option::is_none")]
1806 pub capabilities: Option<CapabilityDeclarations>,
1807 #[serde(default, skip_serializing_if = "Option::is_none")]
1810 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1811 #[serde(default, skip_serializing_if = "Option::is_none")]
1815 pub events: Option<Vec<EventDeclaration>>,
1816}
1817
1818impl CatalogEntry {
1819 #[must_use]
1821 pub fn new(
1822 module_id: impl Into<String>,
1823 roles: Vec<ProviderRole>,
1824 control_ops: Vec<String>,
1825 ) -> Self {
1826 Self {
1827 module_id: module_id.into(),
1828 ready: default_true(),
1829 not_ready: None,
1830 module_version: None,
1831 roles,
1832 control_ops,
1833 capabilities: None,
1834 self_signals: None,
1835 events: None,
1836 }
1837 }
1838
1839 #[must_use]
1841 pub fn with_ready(mut self, ready: bool) -> Self {
1842 self.ready = ready;
1843 self
1844 }
1845
1846 #[must_use]
1848 pub fn with_not_ready(mut self, not_ready: Option<NotReadyReason>) -> Self {
1849 self.not_ready = not_ready;
1850 self
1851 }
1852
1853 #[must_use]
1855 pub fn with_module_version(mut self, module_version: Option<String>) -> Self {
1856 self.module_version = module_version;
1857 self
1858 }
1859
1860 #[must_use]
1862 pub fn with_capabilities(mut self, capabilities: Option<CapabilityDeclarations>) -> Self {
1863 self.capabilities = capabilities;
1864 self
1865 }
1866
1867 #[must_use]
1869 pub fn with_self_signals(mut self, self_signals: Option<Vec<SelfSignalDeclaration>>) -> Self {
1870 self.self_signals = self_signals;
1871 self
1872 }
1873
1874 #[must_use]
1876 pub fn with_events(mut self, events: Option<Vec<EventDeclaration>>) -> Self {
1877 self.events = events;
1878 self
1879 }
1880}
1881
1882#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1884pub struct NotReadyReason {
1885 pub reason: String,
1890 #[serde(default, skip_serializing_if = "Option::is_none")]
1893 pub capability: Option<String>,
1894}
1895
1896impl NotReadyReason {
1897 pub const DECLARED_NOT_READY: &'static str = "declared_not_ready";
1898 pub const REQUIRED_CAPABILITY_UNPROVIDED: &'static str = "required_capability_unprovided";
1899}
1900
1901#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1902pub struct CapabilityRequirementStatus {
1903 pub consumer: String,
1904 pub capability: String,
1905 pub need: String,
1906 pub verdict: String,
1907 pub episode_seq: u64,
1908 pub config_satisfiable: bool,
1909 pub runtime_available: bool,
1910 pub detail: String,
1911}
1912
1913#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1914pub struct SupervisorRescanResult {
1915 pub added: Vec<String>,
1916 pub removed: Vec<String>,
1917 pub changed_pending_reload: Vec<String>,
1918 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1931 pub enabled_changes: Vec<String>,
1932 pub unchanged: u32,
1933 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1941 pub preview: bool,
1942 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1960 pub restart_required: Vec<String>,
1961 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1965 pub capability_warnings: Vec<String>,
1966}
1967
1968#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1984#[serde(rename_all = "snake_case")]
1985pub enum ModuleProtocol {
1986 #[default]
1990 Subc,
1991 None,
2000}
2001
2002#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2014#[non_exhaustive]
2015pub struct SupervisorEntry {
2016 pub module_id: String,
2017 pub state: String,
2018 pub enabled: bool,
2019 pub live: bool,
2029 #[serde(default)]
2033 pub protocol: ModuleProtocol,
2034 #[serde(default, skip_serializing_if = "Option::is_none")]
2037 pub launch_nonce_env: Option<bool>,
2038 pub health: SupervisorHealthStatus,
2039 #[serde(default, skip_serializing_if = "Option::is_none")]
2042 pub pending_reload: Option<PendingReloadVerdict>,
2043 #[serde(default)]
2049 pub last_probe_ms: Option<u64>,
2050 #[serde(default, skip_serializing_if = "Option::is_none")]
2054 pub last_exit_code: Option<i32>,
2055 #[serde(default, skip_serializing_if = "Option::is_none")]
2059 pub last_exit_signal: Option<i32>,
2060 #[serde(default, skip_serializing_if = "Option::is_none")]
2064 pub last_exit_ms: Option<u64>,
2065 #[serde(default, skip_serializing_if = "Option::is_none")]
2068 pub last_exit_kind: Option<TerminalExitKind>,
2069 #[serde(default, skip_serializing_if = "Option::is_none")]
2086 pub restart_count: Option<u32>,
2087 #[serde(default, skip_serializing_if = "Option::is_none")]
2090 pub max_restarts: Option<u32>,
2091 #[serde(default, skip_serializing_if = "Option::is_none")]
2094 pub lifetime_restarts: Option<u32>,
2095 #[serde(default, skip_serializing_if = "Option::is_none")]
2099 pub spawn_generation: Option<u64>,
2100 #[serde(default, skip_serializing_if = "Option::is_none")]
2110 pub restart_window_secs: Option<u64>,
2111 #[serde(default, skip_serializing_if = "Option::is_none")]
2115 pub drain_timeout_ms: Option<u64>,
2116 #[serde(default, skip_serializing_if = "Option::is_none")]
2119 pub restart_backoff_ms: Option<u64>,
2120 #[serde(default, skip_serializing_if = "Option::is_none")]
2123 pub restart_max_backoff_ms: Option<u64>,
2124 #[serde(default, skip_serializing_if = "Option::is_none")]
2137 pub resources: Option<ChildResourceUsage>,
2138}
2139
2140impl SupervisorEntry {
2141 #[must_use]
2143 pub fn new(
2144 module_id: impl Into<String>,
2145 state: impl Into<String>,
2146 enabled: bool,
2147 live: bool,
2148 health: SupervisorHealthStatus,
2149 ) -> Self {
2150 Self {
2151 module_id: module_id.into(),
2152 state: state.into(),
2153 enabled,
2154 live,
2155 protocol: ModuleProtocol::default(),
2156 launch_nonce_env: None,
2157 health,
2158 pending_reload: None,
2159 last_probe_ms: None,
2160 last_exit_code: None,
2161 last_exit_signal: None,
2162 last_exit_ms: None,
2163 last_exit_kind: None,
2164 restart_count: None,
2165 max_restarts: None,
2166 lifetime_restarts: None,
2167 spawn_generation: None,
2168 restart_window_secs: None,
2169 drain_timeout_ms: None,
2170 restart_backoff_ms: None,
2171 restart_max_backoff_ms: None,
2172 resources: None,
2173 }
2174 }
2175
2176 #[must_use]
2178 pub fn with_protocol(mut self, protocol: ModuleProtocol) -> Self {
2179 self.protocol = protocol;
2180 self
2181 }
2182
2183 #[must_use]
2185 pub fn with_launch_nonce_env(mut self, launch_nonce_env: Option<bool>) -> Self {
2186 self.launch_nonce_env = launch_nonce_env;
2187 self
2188 }
2189
2190 #[must_use]
2192 pub fn with_pending_reload(mut self, pending_reload: Option<PendingReloadVerdict>) -> Self {
2193 self.pending_reload = pending_reload;
2194 self
2195 }
2196
2197 #[must_use]
2199 pub fn with_last_probe_ms(mut self, last_probe_ms: Option<u64>) -> Self {
2200 self.last_probe_ms = last_probe_ms;
2201 self
2202 }
2203
2204 #[must_use]
2206 pub fn with_last_exit_code(mut self, last_exit_code: Option<i32>) -> Self {
2207 self.last_exit_code = last_exit_code;
2208 self
2209 }
2210
2211 #[must_use]
2213 pub fn with_last_exit_signal(mut self, last_exit_signal: Option<i32>) -> Self {
2214 self.last_exit_signal = last_exit_signal;
2215 self
2216 }
2217
2218 #[must_use]
2220 pub fn with_last_exit_ms(mut self, last_exit_ms: Option<u64>) -> Self {
2221 self.last_exit_ms = last_exit_ms;
2222 self
2223 }
2224
2225 #[must_use]
2227 pub fn with_last_exit_kind(mut self, last_exit_kind: Option<TerminalExitKind>) -> Self {
2228 self.last_exit_kind = last_exit_kind;
2229 self
2230 }
2231
2232 #[must_use]
2234 pub fn with_restart_count(mut self, restart_count: Option<u32>) -> Self {
2235 self.restart_count = restart_count;
2236 self
2237 }
2238
2239 #[must_use]
2241 pub fn with_max_restarts(mut self, max_restarts: Option<u32>) -> Self {
2242 self.max_restarts = max_restarts;
2243 self
2244 }
2245
2246 #[must_use]
2248 pub fn with_lifetime_restarts(mut self, lifetime_restarts: Option<u32>) -> Self {
2249 self.lifetime_restarts = lifetime_restarts;
2250 self
2251 }
2252
2253 #[must_use]
2255 pub fn with_spawn_generation(mut self, spawn_generation: Option<u64>) -> Self {
2256 self.spawn_generation = spawn_generation;
2257 self
2258 }
2259
2260 #[must_use]
2262 pub fn with_restart_window_secs(mut self, restart_window_secs: Option<u64>) -> Self {
2263 self.restart_window_secs = restart_window_secs;
2264 self
2265 }
2266
2267 #[must_use]
2269 pub fn with_drain_timeout_ms(mut self, drain_timeout_ms: Option<u64>) -> Self {
2270 self.drain_timeout_ms = drain_timeout_ms;
2271 self
2272 }
2273
2274 #[must_use]
2276 pub fn with_restart_backoff_ms(mut self, restart_backoff_ms: Option<u64>) -> Self {
2277 self.restart_backoff_ms = restart_backoff_ms;
2278 self
2279 }
2280
2281 #[must_use]
2283 pub fn with_restart_max_backoff_ms(mut self, restart_max_backoff_ms: Option<u64>) -> Self {
2284 self.restart_max_backoff_ms = restart_max_backoff_ms;
2285 self
2286 }
2287
2288 #[must_use]
2290 pub fn with_resources(mut self, resources: Option<ChildResourceUsage>) -> Self {
2291 self.resources = resources;
2292 self
2293 }
2294}
2295
2296#[derive(Debug, Clone, PartialEq)]
2299pub enum ChildResourceUsage {
2300 Measured(ChildResourceReading),
2301 Unavailable {
2302 reason: ChildResourceUnavailableReason,
2303 },
2304 Unknown {
2307 tag: String,
2308 body: OrderedJsonObject,
2309 },
2310}
2311
2312#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2314pub struct ChildResourceReading {
2315 pub memory_bytes: u64,
2318 pub memory_kind: ChildMemoryKind,
2319 #[serde(default, skip_serializing_if = "Option::is_none")]
2322 pub swap_bytes: Option<u64>,
2323 pub cpu_user_ms: u64,
2327 pub cpu_system_ms: u64,
2330}
2331
2332open_string_enum! {
2333 ChildMemoryKind {
2335 PhysFootprint => "phys_footprint",
2339 ResidentSet => "resident_set",
2342 }
2343}
2344
2345open_string_enum! {
2346 ChildResourceUnavailableReason {
2348 NotRunning => "not_running",
2350 UnsupportedPlatform => "unsupported_platform",
2352 Unreadable => "unreadable",
2355 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
2358 }
2359}
2360
2361#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2362#[serde(rename_all = "snake_case")]
2363pub enum SupervisorHealthStatus {
2364 Ok,
2365 Degraded,
2366 Failing,
2367 Unresponsive,
2368 Unknown,
2369}
2370
2371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2372pub struct SupervisorHealthEntry {
2373 pub module_id: String,
2374 pub status: SupervisorHealthStatus,
2375 #[serde(default, skip_serializing_if = "Option::is_none")]
2381 pub detail: Option<String>,
2382 #[serde(default, skip_serializing_if = "Option::is_none")]
2387 pub metrics: Option<serde_json::Value>,
2388 pub consecutive_failures: u32,
2389 #[serde(default)]
2392 pub late_answer_count: u64,
2393 #[serde(default, skip_serializing_if = "Option::is_none")]
2395 pub last_late_answer_latency_ms: Option<u64>,
2396 #[serde(default)]
2401 pub last_action: Option<String>,
2402 #[serde(default)]
2405 pub last_action_ms: Option<u64>,
2406 #[serde(default, skip_serializing_if = "Option::is_none")]
2419 pub last_probe_ms: Option<u64>,
2420}
2421
2422#[cfg(test)]
2423mod tests {
2424 use super::*;
2425 use subc_protocol::{BindIdentity, RouteTarget};
2426
2427 #[test]
2428 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
2429 let entry = TerminalEntry {
2430 daemon_incarnation: Some("daemon-before-restart".into()),
2431 exit_code: Some(1),
2432 exit_signal: None,
2433 at_ms: 1_700_000_000_123,
2434 disposition: TerminalDisposition::Restarting,
2435 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
2436 disposition_detail: None,
2437 };
2438 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
2439 assert_eq!(
2440 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
2441 ["exit_kind"],
2442 "deliberate_severance"
2443 );
2444
2445 #[derive(serde::Deserialize)]
2446 struct LegacyTerminalEntry {
2447 exit_code: Option<i32>,
2448 exit_signal: Option<i32>,
2449 at_ms: u64,
2450 disposition: TerminalDisposition,
2451 }
2452
2453 let decoded: LegacyTerminalEntry =
2454 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
2455 assert_eq!(decoded.exit_code, Some(1));
2456 assert_eq!(decoded.exit_signal, None);
2457 assert_eq!(decoded.at_ms, 1_700_000_000_123);
2458 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
2459
2460 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
2461 let future: TerminalEntry =
2462 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
2463 assert_eq!(
2464 future.exit_kind,
2465 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
2466 );
2467 }
2468
2469 #[test]
2470 fn terminal_incarnation_is_optional_for_older_daemons() {
2471 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
2472 "at_ms": 123,
2473 "disposition": "stopped"
2474 }))
2475 .unwrap();
2476 let encoded = serde_json::to_value(&entry).unwrap();
2477 assert_eq!(
2478 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
2479 (None, None)
2480 );
2481 }
2482
2483 #[test]
2484 fn route_poll_uses_kind_field() {
2485 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
2486 route_channel: 7,
2487 route_epoch: 11,
2488 kind: PollKind::Status,
2489 })
2490 .unwrap();
2491
2492 assert_eq!(body["op"], "route.poll");
2493 assert_eq!(body["route_epoch"], 11);
2494 assert_eq!(body["kind"], "status");
2495 assert!(body.get("op").is_some());
2496 }
2497
2498 #[test]
2499 fn route_open_is_internally_tagged() {
2500 let request = ClientControlRequest::RouteOpen {
2501 target: RouteTarget::ToolProvider {
2502 module_id: "aft".to_string(),
2503 },
2504 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
2505 consumer_identity: None,
2506 consumer_capabilities: None,
2507 role_versions: None,
2508 admission_facts: None,
2509 scope: None,
2510 };
2511
2512 let body = serde_json::to_value(request).unwrap();
2513 assert_eq!(body["op"], "route.open");
2514 assert_eq!(body["target"]["kind"], "tool_provider");
2515 assert!(body.get("consumer_identity").is_none());
2516 assert!(body.get("consumer_capabilities").is_none());
2517 assert!(body.get("role_versions").is_none());
2518 }
2519
2520 #[test]
2521 fn route_open_without_optional_fields_still_decodes() {
2522 let body = serde_json::json!({
2523 "op": "route.open",
2524 "target": { "kind": "tool_provider", "module_id": "aft" },
2525 "identity": {
2526 "project_root": "/tmp/project",
2527 "harness": "opencode",
2528 "session": "session-1"
2529 }
2530 });
2531
2532 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
2533 let ClientControlRequest::RouteOpen {
2534 consumer_identity,
2535 consumer_capabilities,
2536 role_versions,
2537 admission_facts,
2538 ..
2539 } = decoded
2540 else {
2541 panic!("decoded wrong request variant");
2542 };
2543 assert_eq!(consumer_identity, None);
2544 assert_eq!(consumer_capabilities, None);
2545 assert_eq!(role_versions, None);
2546 assert_eq!(admission_facts, None);
2547 }
2548
2549 #[test]
2550 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
2551 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
2552 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
2553 match decoded {
2554 ClientControlPush::RouteClosed {
2555 excluded_subscriptions,
2556 terminal,
2557 ..
2558 } => {
2559 assert_eq!(excluded_subscriptions, 0);
2560 assert_eq!(terminal, None);
2561 }
2562 other => panic!("unexpected push: {other:?}"),
2563 }
2564 assert!(!serde_json::to_string(&decoded)
2565 .unwrap()
2566 .contains("terminal"));
2567 }
2568
2569 #[test]
2570 fn old_route_closed_decoder_ignores_new_terminal_field() {
2571 #[derive(serde::Deserialize)]
2572 #[serde(tag = "op")]
2573 enum LegacyClientControlPush {
2574 #[serde(rename = "route.closed")]
2575 RouteClosed {
2576 module_id: String,
2577 reason: RouteCloseReason,
2578 drained: bool,
2579 abandoned: u32,
2580 },
2581 }
2582
2583 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
2584 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
2585 match decoded {
2586 LegacyClientControlPush::RouteClosed {
2587 module_id,
2588 reason,
2589 drained,
2590 abandoned,
2591 } => {
2592 assert_eq!(module_id, "aft-tools");
2593 assert_eq!(reason, RouteCloseReason::Crash);
2594 assert!(!drained);
2595 assert_eq!(abandoned, 0);
2596 }
2597 }
2598 }
2599
2600 #[test]
2601 fn supervisor_routes_is_a_control_plane_request() {
2602 let body = serde_json::json!({
2603 "op": "supervisor.routes",
2604 "module_id": "aft"
2605 });
2606
2607 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
2608 assert_eq!(serde_json::to_value(request).unwrap(), body);
2609 }
2610
2611 #[test]
2612 fn diagnostic_string_enums_retain_unknown_wire_values() {
2613 let reason: RunningImageUnavailableReason =
2614 serde_json::from_str("\"future_reason\"").unwrap();
2615 let disposition: TerminalDisposition =
2616 serde_json::from_str("\"future_disposition\"").unwrap();
2617
2618 assert_eq!(
2619 reason,
2620 RunningImageUnavailableReason::Unknown("future_reason".to_string())
2621 );
2622 assert_eq!(
2623 disposition,
2624 TerminalDisposition::Unknown("future_disposition".to_string())
2625 );
2626 }
2627
2628 #[test]
2629 fn diagnostic_string_enums_preserve_existing_wire_names() {
2630 let names = [
2631 (RunningImageUnavailableReason::NotRunning, "not_running"),
2632 (
2633 RunningImageUnavailableReason::UnsupportedPlatform,
2634 "unsupported_platform",
2635 ),
2636 (
2637 RunningImageUnavailableReason::RunningExecutableUnreadable,
2638 "running_executable_unreadable",
2639 ),
2640 (
2641 RunningImageUnavailableReason::SpawnedPathUnreadable,
2642 "spawned_path_unreadable",
2643 ),
2644 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2645 (
2646 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2647 "process_identity_unconfirmed",
2648 ),
2649 ];
2650 for (value, expected) in names {
2651 let wire = serde_json::to_string(&value).unwrap();
2652 assert_eq!(wire, format!("\"{expected}\""));
2653 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2654 assert_eq!(decoded, value);
2655 }
2656
2657 for (value, expected) in [
2658 (TerminalDisposition::Stopped, "stopped"),
2659 (TerminalDisposition::Disabled, "disabled"),
2660 (TerminalDisposition::Failed, "failed"),
2661 (TerminalDisposition::Restarting, "restarting"),
2662 (TerminalDisposition::DaemonShutdown, "daemon_shutdown"),
2663 ] {
2664 let wire = serde_json::to_string(&value).unwrap();
2665 assert_eq!(wire, format!("\"{expected}\""));
2666 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2667 assert_eq!(decoded, value);
2668 }
2669 }
2670
2671 #[test]
2672 fn diagnostic_string_enums_reject_non_string_bodies() {
2673 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2674 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2675 }
2676
2677 #[test]
2678 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2679 let body = serde_json::json!({
2680 "op": "supervisor.provenance",
2681 "daemon": {
2682 "daemon_build": {},
2683 "daemon_observed": {
2684 "running_image": {
2685 "status": "unavailable",
2686 "reason": "not_running"
2687 }
2688 }
2689 },
2690 "modules": [
2691 {
2692 "module_id": "future",
2693 "module_declared": { "status": "unverifiable" },
2694 "daemon_observed": {
2695 "running_image": {
2696 "status": "unavailable",
2697 "reason": "future_reason"
2698 }
2699 }
2700 },
2701 {
2702 "module_id": "healthy-a",
2703 "module_declared": { "status": "unverifiable" },
2704 "daemon_observed": {
2705 "running_image": {
2706 "status": "match",
2707 "evidence": {
2708 "method": "linux_proc_sha256",
2709 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2710 }
2711 }
2712 }
2713 },
2714 {
2715 "module_id": "healthy-b",
2716 "module_declared": { "status": "unverifiable" },
2717 "daemon_observed": {
2718 "running_image": {
2719 "status": "unavailable",
2720 "reason": "unsupported_platform"
2721 }
2722 }
2723 }
2724 ]
2725 });
2726
2727 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2728 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2729 panic!("decoded wrong response variant");
2730 };
2731 assert_eq!(modules.len(), 3);
2732 assert_eq!(modules[0].module_id, "future");
2733 assert_eq!(
2734 modules[0].daemon_observed.running_image,
2735 RunningImageAgreement::Unavailable {
2736 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2737 }
2738 );
2739 assert_eq!(modules[1].module_id, "healthy-a");
2740 assert_eq!(modules[2].module_id, "healthy-b");
2741 }
2742
2743 #[test]
2744 fn tagged_unknown_values_retain_tag_and_body() {
2745 macro_rules! assert_unknown_round_trip {
2746 ($ty:ident, $field:literal, $value:expr) => {
2747 let value = $value;
2748 let wire = serde_json::to_string(&value).unwrap();
2749 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2750 match decoded {
2751 $ty::Unknown { tag, body } => {
2752 assert_eq!(tag, value[$field].as_str().unwrap());
2753 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2754 }
2755 _ => panic!("decoded known variant"),
2756 }
2757 };
2758 }
2759
2760 assert_unknown_round_trip!(
2761 ModuleDeclaredProvenance,
2762 "status",
2763 serde_json::json!({"status": "future", "build": {"version": 7}})
2764 );
2765 assert_unknown_round_trip!(
2766 RunningImageAgreement,
2767 "status",
2768 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2769 );
2770 assert_unknown_round_trip!(
2771 RunningImageEvidence,
2772 "method",
2773 serde_json::json!({"method": "future", "digest": "abc"})
2774 );
2775 assert_unknown_round_trip!(
2776 SupervisorRouteConsumer,
2777 "kind",
2778 serde_json::json!({"kind": "future", "module_id": "m"})
2779 );
2780 assert_unknown_round_trip!(
2781 StderrCaptureState,
2782 "state",
2783 serde_json::json!({"state": "future", "reason": "because"})
2784 );
2785 assert_unknown_round_trip!(
2786 StderrTailEntry,
2787 "kind",
2788 serde_json::json!({"kind": "future", "text": "line"})
2789 );
2790 assert_unknown_round_trip!(
2791 ChildResourceUsage,
2792 "status",
2793 serde_json::json!({"status": "future", "memory_bytes": 1})
2794 );
2795 }
2796
2797 #[test]
2798 fn child_resource_usage_round_trips_both_known_states() {
2799 let measured = ChildResourceUsage::Measured(ChildResourceReading {
2800 memory_bytes: 0,
2801 memory_kind: ChildMemoryKind::ResidentSet,
2802 swap_bytes: Some(0),
2803 cpu_user_ms: 0,
2804 cpu_system_ms: 0,
2805 });
2806 let wire = serde_json::to_value(&measured).unwrap();
2807 assert_eq!(
2808 wire,
2809 serde_json::json!({
2810 "status": "measured",
2811 "memory_bytes": 0,
2812 "memory_kind": "resident_set",
2813 "swap_bytes": 0,
2814 "cpu_user_ms": 0,
2815 "cpu_system_ms": 0
2816 })
2817 );
2818 assert_eq!(
2819 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2820 measured
2821 );
2822
2823 let unavailable = ChildResourceUsage::Unavailable {
2824 reason: ChildResourceUnavailableReason::NotRunning,
2825 };
2826 let wire = serde_json::to_value(&unavailable).unwrap();
2827 assert_eq!(
2828 wire,
2829 serde_json::json!({"status": "unavailable", "reason": "not_running"})
2830 );
2831 assert_eq!(
2832 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2833 unavailable
2834 );
2835 }
2836
2837 #[test]
2838 fn a_stderr_line_decodes_with_and_without_its_capture_time() {
2839 let stamped: StderrTailEntry = serde_json::from_str(
2843 r#"{"kind":"line","text":"boom","truncated":true,"at_ms":1789801440685}"#,
2844 )
2845 .unwrap();
2846 assert_eq!(
2847 stamped,
2848 StderrTailEntry::Line {
2849 text: "boom".to_string(),
2850 truncated: true,
2851 at_ms: Some(1_789_801_440_685),
2852 }
2853 );
2854 let unstamped: StderrTailEntry =
2855 serde_json::from_str(r#"{"kind":"line","text":"boom"}"#).unwrap();
2856 assert_eq!(
2857 unstamped,
2858 StderrTailEntry::Line {
2859 text: "boom".to_string(),
2860 truncated: false,
2861 at_ms: None,
2862 }
2863 );
2864 assert_eq!(
2867 serde_json::to_string(&unstamped).unwrap(),
2868 r#"{"kind":"line","text":"boom"}"#
2869 );
2870 assert_eq!(
2871 serde_json::to_value(&stamped).unwrap()["at_ms"],
2872 serde_json::json!(1_789_801_440_685u64)
2873 );
2874 }
2875
2876 #[test]
2877 fn tagged_unknown_values_round_trip_the_original_json() {
2878 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2879 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2880 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2881 }
2882
2883 #[test]
2884 fn tagged_unknown_values_round_trip_trailing_tag() {
2885 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2886 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2887 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2888
2889 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2890 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2891 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2892 }
2893
2894 #[test]
2895 fn tagged_unknown_values_round_trip_middle_tag() {
2896 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2897 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2898 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2899
2900 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2901 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2902 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2903 }
2904
2905 #[test]
2906 fn tagged_unknown_values_round_trip_deep_payload() {
2907 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2908 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2909 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2910
2911 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2912 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2913 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2914 }
2915
2916 #[test]
2917 fn tagged_unknown_values_reject_non_object_bodies() {
2918 for wire in ["42", r#""future""#, "[]"] {
2919 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2920 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2921 }
2922 }
2923
2924 #[test]
2925 fn duplicate_discriminators_reject_without_panicking() {
2926 assert_eq!(
2927 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2928 .unwrap(),
2929 ModuleDeclaredProvenance::Unverifiable
2930 );
2931 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2932 .unwrap()
2933 {
2934 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2935 _ => panic!("future discriminator decoded as a known variant"),
2936 }
2937
2938 let wires = [
2939 r#"{"status":"reported","status":"unverifiable"}"#,
2940 r#"{"status":"unverifiable","status":"reported"}"#,
2941 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2942 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2943 ];
2944
2945 for wire in wires {
2946 let result =
2947 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2948 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2949 assert!(
2950 result.unwrap().is_err(),
2951 "duplicate discriminator decoded: {wire}"
2952 );
2953 }
2954
2955 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2956 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2957 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2958 assert!(
2959 result.unwrap().is_err(),
2960 "duplicate discriminator decoded: {wire}"
2961 );
2962 }
2963
2964 #[test]
2965 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2966 let known_wire =
2967 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2968 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2969 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2970
2971 for wire in [
2972 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2973 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2974 ] {
2975 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2976 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2977 }
2978
2979 for wire in [
2980 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2981 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2982 ] {
2983 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2984 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2985 }
2986
2987 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2988 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2989 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2990
2991 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2992 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2993 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2994 }
2995
2996 #[test]
2997 fn tagged_unknown_member_does_not_discard_known_siblings() {
2998 let body = serde_json::json!({
2999 "modules": [{
3000 "module_id": "target",
3001 "routes": [
3002 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
3003 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
3004 ]
3005 }]
3006 });
3007 let decoded: ClientControlResponse = serde_json::from_value(
3008 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
3009 )
3010 .unwrap();
3011 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
3012 panic!("decoded wrong response variant");
3013 };
3014 assert_eq!(modules[0].routes.len(), 2);
3015 assert_eq!(
3016 modules[0].routes[1].consumer,
3017 SupervisorRouteConsumer::Direct { connection_id: 7 }
3018 );
3019 }
3020}
3021
3022#[cfg(test)]
3023mod launch_nonce_redaction_tests {
3024 use super::*;
3025
3026 const NONCE: &str = "nonce-f00dfeed1234abcd";
3027
3028 fn identity() -> ConsumerIdentity {
3029 ConsumerIdentity {
3030 module_id: "wernicke".to_string(),
3031 launch_nonce: NONCE.to_string(),
3032 }
3033 }
3034
3035 #[test]
3036 fn consumer_identity_debug_names_the_module_and_never_the_nonce() {
3037 let printed = format!("{:?}", identity());
3038 assert!(printed.contains("wernicke"), "{printed}");
3039 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
3040 }
3041
3042 #[test]
3043 fn route_open_request_debug_never_prints_the_nonce() {
3044 let request = ClientControlRequest::RouteOpen {
3045 target: subc_protocol::RouteTarget::ToolProvider {
3046 module_id: "broca".to_string(),
3047 },
3048 identity: subc_protocol::BindIdentity::new(
3049 PathBuf::from("/tmp/project"),
3050 "test".to_string(),
3051 "session".to_string(),
3052 ),
3053 consumer_identity: Some(identity()),
3054 consumer_capabilities: None,
3055 role_versions: None,
3056 admission_facts: None,
3057 scope: None,
3058 };
3059 let printed = format!("{request:?}");
3060 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
3061 }
3062}