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
29mod activity;
30pub use activity::*;
31
32macro_rules! open_string_enum {
33 (
34 $(#[$meta:meta])*
35 $name:ident {
36 $( $(#[$variant_meta:meta])* $variant:ident => $wire_name:literal ),+ $(,)?
37 }
38 ) => {
39 $(#[$meta])*
40 #[derive(Debug, Clone, PartialEq, Eq)]
41 pub enum $name {
42 $( $(#[$variant_meta])* $variant, )+
43 Unknown(String),
44 }
45
46 impl $name {
47 fn wire_name(&self) -> &str {
48 match self {
49 $( Self::$variant => $wire_name, )+
50 Self::Unknown(value) => value,
51 }
52 }
53 }
54
55 impl Serialize for $name {
56 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
57 where
58 S: serde::Serializer,
59 {
60 serializer.serialize_str(self.wire_name())
61 }
62 }
63
64 impl<'de> Deserialize<'de> for $name {
65 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66 where
67 D: serde::Deserializer<'de>,
68 {
69 let value = String::deserialize(deserializer)?;
70 Ok(match value.as_str() {
71 $( $wire_name => Self::$variant, )+
72 _ => Self::Unknown(value),
73 })
74 }
75 }
76 };
77}
78
79#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
81pub struct ConsumerIdentity {
82 pub module_id: String,
83 pub launch_nonce: String,
84}
85
86impl std::fmt::Debug for ConsumerIdentity {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("ConsumerIdentity")
93 .field("module_id", &self.module_id)
94 .field(
95 "launch_nonce",
96 &format_args!("<{} bytes redacted>", self.launch_nonce.len()),
97 )
98 .finish()
99 }
100}
101
102pub mod ops {
113 pub const SERVER: &str = "server.";
114 pub const CATALOG: &str = "catalog.";
115 pub const ROUTE: &str = "route.";
116 pub const SUPERVISOR: &str = "supervisor.";
117 pub const CONFIG: &str = "config.";
118
119 pub const SERVER_DESCRIBE: &str = "server.describe";
120 pub const CATALOG_LIST: &str = "catalog.list";
121 pub const ROUTE_OPEN: &str = "route.open";
122 pub const ROUTE_POLL: &str = "route.poll";
123 pub const ROUTE_CLOSING: &str = "route.closing";
124 pub const ROUTE_CLOSED: &str = "route.closed";
125 pub const SUPERVISOR_LIST: &str = "supervisor.list";
126 pub const SUPERVISOR_RESTART: &str = "supervisor.restart";
127 pub const SUPERVISOR_SWAP: &str = "supervisor.swap";
128 pub const SUPERVISOR_RELOAD: &str = "supervisor.reload";
129 pub const SUPERVISOR_RESCAN: &str = "supervisor.rescan";
130 pub const SUPERVISOR_RELEASE_RESERVED: &str = "supervisor.release_reserved";
131 pub const SUPERVISOR_SET_ENABLED: &str = "supervisor.set_enabled";
132 pub const SUPERVISOR_HEALTH_PROBE: &str = "supervisor.health_probe";
133 pub const SUPERVISOR_HEALTH: &str = "supervisor.health";
134 pub const SUPERVISOR_STDERR_TAIL: &str = "supervisor.stderr_tail";
135 pub const SUPERVISOR_TERMINALS: &str = "supervisor.terminals";
136 pub const SUPERVISOR_ROUTES: &str = "supervisor.routes";
137 pub const SUPERVISOR_PROVENANCE: &str = "supervisor.provenance";
138 pub const SUPERVISOR_SPAWN_SNAPSHOT: &str = "supervisor.spawn_snapshot";
139 pub const SUPERVISOR_SPAWN_SUBSCRIBE: &str = "supervisor.spawn_subscribe";
140 pub const ACTIVITY_RECORD: &str = "activity.record";
141 pub const ACTIVITY_QUERY: &str = "activity.query";
142 pub const ACTIVITY_SUBSCRIBE: &str = "activity.subscribe";
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147#[serde(tag = "op")]
148#[allow(clippy::large_enum_variant)]
151pub enum ClientControlRequest {
152 #[serde(rename = "server.describe")]
153 ServerDescribe {},
154 #[serde(rename = "catalog.list")]
155 CatalogList {
156 #[serde(default)]
161 module_id: Option<String>,
162 },
163 #[serde(rename = "route.open")]
164 RouteOpen {
165 target: RouteTarget,
166 identity: BindIdentity,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
176 consumer_identity: Option<ConsumerIdentity>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
185 consumer_capabilities: Option<Vec<String>>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
197 role_versions: Option<BTreeMap<String, String>>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 admission_facts: Option<serde_json::Value>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
208 scope: Option<ScopeSelector>,
209 },
210 #[serde(rename = "route.poll")]
211 RoutePoll {
212 route_channel: u16,
213 route_epoch: u32,
214 kind: PollKind,
215 },
216 #[serde(rename = "supervisor.list")]
217 SupervisorList {},
218 #[serde(rename = "supervisor.spawn_snapshot")]
220 SupervisorSpawnSnapshot {},
221 #[serde(rename = "supervisor.spawn_subscribe")]
237 SupervisorSpawnSubscribe {
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 since: Option<SpawnCursor>,
240 },
241 #[serde(rename = "supervisor.restart")]
242 SupervisorRestart {
243 module_id: String,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
254 drain_timeout_ms: Option<u64>,
255 },
256 #[serde(rename = "supervisor.swap")]
271 SupervisorSwap {
272 module_id: String,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
276 ready_timeout_ms: Option<u64>,
277 },
278 #[serde(rename = "supervisor.reload")]
279 SupervisorReload { module_id: String },
280 #[serde(rename = "supervisor.rescan")]
281 SupervisorRescan {
282 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
302 preview: bool,
303 },
304 #[serde(rename = "supervisor.release_reserved")]
308 SupervisorReleaseReserved { module_id: String },
309 #[serde(rename = "supervisor.set_enabled")]
310 SupervisorSetEnabled { module_id: String, enabled: bool },
311 #[serde(rename = "supervisor.health_probe")]
312 SupervisorHealthProbe { module_id: String },
313 #[serde(rename = "supervisor.health")]
314 SupervisorHealth {},
315 #[serde(rename = "supervisor.routes")]
328 SupervisorRoutes {
329 #[serde(default, skip_serializing_if = "Option::is_none")]
330 module_id: Option<String>,
331 },
332 #[serde(rename = "supervisor.provenance")]
335 SupervisorProvenance {
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 module_id: Option<String>,
338 },
339 #[serde(rename = "supervisor.stderr_tail")]
347 SupervisorStderrTail {
348 module_id: String,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
350 max_lines: Option<u32>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 max_bytes: Option<u32>,
353 },
354 #[serde(rename = "supervisor.terminals")]
367 SupervisorTerminals { module_id: String },
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
372#[serde(tag = "op")]
373pub enum ClientControlResponse {
374 #[serde(rename = "server.describe")]
375 ServerDescribe {
376 protocol_ver: u8,
377 subc_ops: Vec<String>,
378 capabilities: Vec<String>,
379 connected_clients: u64,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
381 counters: Option<serde_json::Value>,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
389 build_git_sha: Option<String>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
396 build_lock_digest: Option<String>,
397 #[serde(default, skip_serializing_if = "Vec::is_empty")]
401 capability_requirements: Vec<CapabilityRequirementStatus>,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
407 machine_id: Option<String>,
408 },
409 #[serde(rename = "catalog.list")]
410 CatalogList {
411 generation: u64,
412 modules: Vec<CatalogEntry>,
413 subc_ops: Vec<String>,
414 },
415 #[serde(rename = "route.open")]
416 RouteOpen {
417 route_channel: u16,
418 route_epoch: u32,
419 },
420 #[serde(rename = "route.poll")]
421 RoutePoll {
422 route_channel: u16,
423 route_epoch: u32,
424 status: Option<String>,
425 live: Option<bool>,
426 },
427 #[serde(rename = "supervisor.list")]
428 SupervisorList {
429 generation: u64,
430 modules: Vec<SupervisorEntry>,
431 },
432 #[serde(rename = "supervisor.spawn_snapshot")]
433 SupervisorSpawnSnapshot {
434 #[serde(flatten)]
435 snapshot: SpawnSnapshot,
436 },
437 #[serde(rename = "supervisor.ack")]
438 SupervisorAck { module_id: String, applied: bool },
439 #[serde(rename = "supervisor.rescan")]
440 SupervisorRescan {
441 #[serde(flatten)]
442 result: SupervisorRescanResult,
443 },
444 #[serde(rename = "supervisor.health_probe")]
445 SupervisorHealthProbe {
446 module_id: String,
447 status: HealthStatus,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
449 detail: Option<String>,
450 #[serde(default, skip_serializing_if = "Option::is_none")]
451 metrics: Option<serde_json::Value>,
452 },
453 #[serde(rename = "supervisor.health")]
454 SupervisorHealth {
455 generation: u64,
456 modules: Vec<SupervisorHealthEntry>,
457 },
458 #[serde(rename = "supervisor.routes")]
459 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
460 #[serde(rename = "supervisor.provenance")]
461 SupervisorProvenance {
462 daemon: SupervisorDaemonProvenance,
463 modules: Vec<SupervisorModuleProvenance>,
464 },
465 #[serde(rename = "supervisor.stderr_tail")]
466 SupervisorStderrTail {
467 module_id: String,
468 #[serde(flatten)]
469 tail: StderrTail,
470 },
471 #[serde(rename = "supervisor.terminals")]
472 SupervisorTerminals {
473 module_id: String,
474 #[serde(flatten)]
475 terminals: TerminalHistory,
476 },
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
484#[serde(tag = "op")]
485pub enum ClientControlPush {
486 #[serde(rename = "route.closing")]
487 RouteClosing {
488 module_id: String,
489 #[serde(default)]
492 channels: Vec<u16>,
493 reason: RouteCloseReason,
494 },
495 #[serde(rename = "route.closed")]
496 RouteClosed {
497 module_id: String,
498 #[serde(default)]
501 channels: Vec<u16>,
502 reason: RouteCloseReason,
503 drained: bool,
505 abandoned: u32,
508 #[serde(default)]
510 excluded_subscriptions: u32,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
517 terminal: Option<bool>,
518 },
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
523pub struct SpawnCursor {
524 pub daemon_incarnation: String,
525 pub seq: u64,
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
530pub struct LiveSpawn {
531 pub module_id: String,
532 pub spawn_generation: u64,
533 pub pid: u32,
534 pub spawned_at_ms: u64,
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
539pub struct SpawnSnapshot {
540 pub cursor: SpawnCursor,
541 pub ring_bound: u64,
543 pub live: Vec<LiveSpawn>,
544}
545
546#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
548#[serde(rename_all = "snake_case")]
549pub enum SpawnEventKind {
550 Spawned,
551 Exited,
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
560pub struct SpawnEvent {
561 pub cursor: SpawnCursor,
562 pub kind: SpawnEventKind,
563 pub module_id: String,
564 pub spawn_generation: u64,
565 pub pid: u32,
566 #[serde(default, skip_serializing_if = "Option::is_none")]
567 pub exit_code: Option<i32>,
568 #[serde(default, skip_serializing_if = "Option::is_none")]
569 pub exit_signal: Option<i32>,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
574pub struct StderrTail {
575 pub capture: StderrCaptureState,
576 pub entries: Vec<StderrTailEntry>,
577 #[serde(default, skip_serializing_if = "is_zero_u64")]
586 pub dropped_lines: u64,
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
591pub struct SupervisorRouteModule {
592 pub module_id: String,
593 pub routes: Vec<SupervisorRoute>,
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
598pub struct SupervisorRoute {
599 pub consumer: SupervisorRouteConsumer,
600 pub age_ms: u64,
602 pub draining: bool,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
611 pub drain_reason: Option<RouteCloseReason>,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
616pub struct SupervisorModuleProvenance {
617 pub module_id: String,
618 pub module_declared: ModuleDeclaredProvenance,
619 pub daemon_observed: SupervisorObservedProcess,
620}
621
622#[derive(Debug, Clone, PartialEq)]
624pub enum ModuleDeclaredProvenance {
625 Reported {
626 build: ManifestProvenance,
627 },
628 Unverifiable,
629 Unknown {
632 tag: String,
633 body: OrderedJsonObject,
634 },
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
642pub struct SupervisorObservedProcess {
643 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub launch_nonce_source: Option<String>,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
651 pub pid: Option<u32>,
652 #[serde(default, skip_serializing_if = "Option::is_none")]
653 pub spawned_at_ms: Option<u64>,
654 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub spawned_from: Option<PathBuf>,
656 pub running_image: RunningImageAgreement,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
662pub struct PendingReloadVerdict {
663 pub path: ReloadPathAgreement,
664 pub image: RunningImageAgreement,
665}
666
667#[derive(Debug, Clone, PartialEq)]
669pub enum ReloadPathAgreement {
670 Match,
671 Mismatch {
672 configured: PathBuf,
673 spawned_from: PathBuf,
674 },
675 Unavailable {
676 reason: ReloadPathUnavailableReason,
677 },
678 Unknown {
679 tag: String,
680 body: OrderedJsonObject,
681 },
682}
683
684open_string_enum! {
685 ReloadPathUnavailableReason {
687 NotRunning => "not_running",
688 SpawnedPathUnavailable => "spawned_path_unavailable",
689 }
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
694pub struct SupervisorDaemonProvenance {
695 pub daemon_build: DaemonBuildProvenance,
696 pub daemon_observed: DaemonObservedProcess,
697}
698
699#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
701pub struct DaemonBuildProvenance {
702 #[serde(default, skip_serializing_if = "Option::is_none")]
703 pub build_git_sha: Option<String>,
704 #[serde(default, skip_serializing_if = "Option::is_none")]
705 pub build_lock_digest: Option<String>,
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
710pub struct DaemonObservedProcess {
711 #[serde(default, skip_serializing_if = "Option::is_none")]
712 pub pid: Option<u32>,
713 #[serde(default, skip_serializing_if = "Option::is_none")]
718 pub started_at_ms: Option<u64>,
719 pub running_image: RunningImageAgreement,
720}
721
722#[derive(Debug, Clone, PartialEq)]
724pub enum RunningImageAgreement {
725 Match {
726 evidence: RunningImageEvidence,
727 },
728 Mismatch {
729 running: RunningImageEvidence,
730 disk: RunningImageEvidence,
731 },
732 Unavailable {
733 reason: RunningImageUnavailableReason,
734 },
735 Unknown {
738 tag: String,
739 body: OrderedJsonObject,
740 },
741}
742
743#[derive(Debug, Clone, PartialEq)]
745pub enum RunningImageEvidence {
746 LinuxProcSha256 {
747 digest: String,
748 },
749 MacosSpawnInode {
750 device: u64,
751 inode: u64,
752 },
753 WindowsSpawnFileId {
755 volume_serial_number: u64,
756 file_id: [u8; 16],
757 },
758 Unknown {
761 tag: String,
762 body: OrderedJsonObject,
763 },
764}
765
766open_string_enum! {
767 RunningImageUnavailableReason {
769 NotRunning => "not_running",
770 UnsupportedPlatform => "unsupported_platform",
771 RunningExecutableUnreadable => "running_executable_unreadable",
772 RunningExecutablePermissionDenied => "running_executable_permission_denied",
773 ProcessHidden => "process_hidden",
774 SpawnedPathUnreadable => "spawned_path_unreadable",
775 HashFailed => "hash_failed",
776 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
777 }
778}
779
780#[derive(Debug, Clone, PartialEq)]
786pub enum SupervisorRouteConsumer {
787 Reserved {
788 module_id: String,
789 },
790 Direct {
791 connection_id: u64,
792 },
793 Unknown {
796 tag: String,
797 body: OrderedJsonObject,
798 },
799}
800
801#[derive(Debug, Clone, PartialEq)]
808pub enum StderrCaptureState {
809 Captured,
812 Incomplete { reason: String },
814 NotCaptured { reason: String },
816 Unknown {
819 tag: String,
820 body: OrderedJsonObject,
821 },
822}
823
824#[derive(Debug, Clone, PartialEq)]
825pub enum StderrTailEntry {
826 Line {
827 text: String,
828 truncated: bool,
833 at_ms: Option<u64>,
837 },
838 ProcessStart,
843 Unknown {
846 tag: String,
847 body: OrderedJsonObject,
848 },
849}
850
851#[derive(Debug, Serialize, Deserialize)]
852#[serde(tag = "status", rename_all = "snake_case")]
853enum ModuleDeclaredProvenanceWire {
854 Reported { build: ManifestProvenance },
855 Unverifiable,
856}
857
858#[derive(Debug, Serialize, Deserialize)]
859#[serde(tag = "status", rename_all = "snake_case")]
860enum RunningImageAgreementWire {
861 Match {
862 evidence: RunningImageEvidence,
863 },
864 Mismatch {
865 running: RunningImageEvidence,
866 disk: RunningImageEvidence,
867 },
868 Unavailable {
869 reason: RunningImageUnavailableReason,
870 },
871}
872
873#[derive(Debug, Serialize, Deserialize)]
874#[serde(tag = "status", rename_all = "snake_case")]
875enum ReloadPathAgreementWire {
876 Match,
877 Mismatch {
878 configured: PathBuf,
879 spawned_from: PathBuf,
880 },
881 Unavailable {
882 reason: ReloadPathUnavailableReason,
883 },
884}
885
886#[derive(Debug, Serialize, Deserialize)]
887#[serde(tag = "method", rename_all = "snake_case")]
888enum RunningImageEvidenceWire {
889 LinuxProcSha256 {
890 digest: String,
891 },
892 MacosSpawnInode {
893 device: u64,
894 inode: u64,
895 },
896 WindowsSpawnFileId {
897 volume_serial_number: u64,
898 file_id: [u8; 16],
899 },
900}
901
902#[derive(Debug, Serialize, Deserialize)]
903#[serde(tag = "kind", rename_all = "snake_case")]
904enum SupervisorRouteConsumerWire {
905 Reserved { module_id: String },
906 Direct { connection_id: u64 },
907}
908
909#[derive(Debug, Serialize, Deserialize)]
910#[serde(tag = "state", rename_all = "snake_case")]
911enum StderrCaptureStateWire {
912 Captured,
913 Incomplete { reason: String },
914 NotCaptured { reason: String },
915}
916
917#[derive(Debug, Serialize, Deserialize)]
918#[serde(tag = "status", rename_all = "snake_case")]
919enum ChildResourceUsageWire {
920 Measured(ChildResourceReading),
921 Unavailable {
922 reason: ChildResourceUnavailableReason,
923 },
924}
925
926#[derive(Debug, Serialize, Deserialize)]
927#[serde(tag = "kind", rename_all = "snake_case")]
928enum StderrTailEntryWire {
929 Line {
930 text: String,
931 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
932 truncated: bool,
933 #[serde(default, skip_serializing_if = "Option::is_none")]
936 at_ms: Option<u64>,
937 },
938 ProcessStart,
939}
940
941#[derive(Debug, Clone, PartialEq)]
943pub enum OrderedJsonValue {
944 Null,
945 Bool(bool),
946 Number(serde_json::Number),
947 String(String),
948 Array(Vec<Self>),
949 Object(OrderedJsonObject),
950}
951
952#[derive(Debug, Clone, PartialEq)]
954pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
955
956impl OrderedJsonObject {
957 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
959 &self.0
960 }
961
962 fn into_value(self) -> serde_json::Value {
963 serde_json::Value::Object(
964 self.0
965 .into_iter()
966 .map(|(key, value)| (key, value.into_value()))
967 .collect(),
968 )
969 }
970}
971
972impl OrderedJsonValue {
973 fn into_value(self) -> serde_json::Value {
974 match self {
975 Self::Null => serde_json::Value::Null,
976 Self::Bool(value) => serde_json::Value::Bool(value),
977 Self::Number(value) => serde_json::Value::Number(value),
978 Self::String(value) => serde_json::Value::String(value),
979 Self::Array(values) => {
980 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
981 }
982 Self::Object(value) => value.into_value(),
983 }
984 }
985}
986
987impl Serialize for OrderedJsonValue {
988 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
989 where
990 S: Serializer,
991 {
992 match self {
993 Self::Null => serializer.serialize_unit(),
994 Self::Bool(value) => serializer.serialize_bool(*value),
995 Self::Number(value) => value.serialize(serializer),
996 Self::String(value) => serializer.serialize_str(value),
997 Self::Array(values) => values.serialize(serializer),
998 Self::Object(value) => value.serialize(serializer),
999 }
1000 }
1001}
1002
1003impl<'de> Deserialize<'de> for OrderedJsonValue {
1004 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1005 where
1006 D: Deserializer<'de>,
1007 {
1008 struct OrderedValueVisitor;
1009
1010 impl<'de> Visitor<'de> for OrderedValueVisitor {
1011 type Value = OrderedJsonValue;
1012
1013 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1014 formatter.write_str("a JSON value with ordered object members")
1015 }
1016
1017 fn visit_unit<E>(self) -> Result<Self::Value, E>
1018 where
1019 E: serde::de::Error,
1020 {
1021 Ok(OrderedJsonValue::Null)
1022 }
1023
1024 fn visit_none<E>(self) -> Result<Self::Value, E>
1025 where
1026 E: serde::de::Error,
1027 {
1028 Ok(OrderedJsonValue::Null)
1029 }
1030
1031 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1032 where
1033 D: Deserializer<'de>,
1034 {
1035 OrderedJsonValue::deserialize(deserializer)
1036 }
1037
1038 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
1039 where
1040 E: serde::de::Error,
1041 {
1042 Ok(OrderedJsonValue::Bool(value))
1043 }
1044
1045 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
1046 where
1047 E: serde::de::Error,
1048 {
1049 Ok(OrderedJsonValue::Number(value.into()))
1050 }
1051
1052 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
1053 where
1054 E: serde::de::Error,
1055 {
1056 Ok(OrderedJsonValue::Number(value.into()))
1057 }
1058
1059 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1060 where
1061 E: serde::de::Error,
1062 {
1063 serde_json::Number::from_f64(value)
1064 .map(OrderedJsonValue::Number)
1065 .ok_or_else(|| E::custom("non-finite JSON number"))
1066 }
1067
1068 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1069 where
1070 E: serde::de::Error,
1071 {
1072 Ok(OrderedJsonValue::String(value.to_owned()))
1073 }
1074
1075 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
1076 where
1077 E: serde::de::Error,
1078 {
1079 Ok(OrderedJsonValue::String(value))
1080 }
1081
1082 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1083 where
1084 A: SeqAccess<'de>,
1085 {
1086 let mut values = Vec::new();
1087 while let Some(value) = sequence.next_element()? {
1088 values.push(value);
1089 }
1090 Ok(OrderedJsonValue::Array(values))
1091 }
1092
1093 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1094 where
1095 A: MapAccess<'de>,
1096 {
1097 let mut entries = Vec::new();
1098 while let Some((key, value)) = map.next_entry()? {
1099 entries.push((key, value));
1100 }
1101 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
1102 }
1103 }
1104
1105 deserializer.deserialize_any(OrderedValueVisitor)
1106 }
1107}
1108
1109impl Serialize for OrderedJsonObject {
1110 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1111 where
1112 S: Serializer,
1113 {
1114 let mut map = serializer.serialize_map(Some(self.0.len()))?;
1115 for (key, value) in &self.0 {
1116 map.serialize_entry(key, value)?;
1117 }
1118 map.end()
1119 }
1120}
1121
1122impl<'de> Deserialize<'de> for OrderedJsonObject {
1123 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1124 where
1125 D: Deserializer<'de>,
1126 {
1127 struct OrderedObjectVisitor;
1128
1129 impl<'de> Visitor<'de> for OrderedObjectVisitor {
1130 type Value = OrderedJsonObject;
1131
1132 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1133 formatter.write_str("an object with ordered JSON members")
1134 }
1135
1136 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1137 where
1138 A: MapAccess<'de>,
1139 {
1140 let mut entries = Vec::new();
1141 while let Some((key, value)) = map.next_entry()? {
1142 entries.push((key, value));
1143 }
1144 Ok(OrderedJsonObject(entries))
1145 }
1146 }
1147
1148 deserializer.deserialize_map(OrderedObjectVisitor)
1149 }
1150}
1151
1152fn read_tagged<'de, D>(
1153 deserializer: D,
1154 field: &'static str,
1155) -> Result<(String, OrderedJsonObject), D::Error>
1156where
1157 D: Deserializer<'de>,
1158{
1159 let body = OrderedJsonObject::deserialize(deserializer)?;
1160 let mut tag = None;
1161 for (key, value) in body.as_entries() {
1162 if key != field {
1163 continue;
1164 }
1165 if tag.is_some() {
1166 return Err(D::Error::custom(format!(
1167 "tagged object has duplicate `{field}` field"
1168 )));
1169 }
1170 let OrderedJsonValue::String(value) = value else {
1171 return Err(D::Error::custom(format!(
1172 "tagged object has no string `{field}` field"
1173 )));
1174 };
1175 tag = Some(value);
1176 }
1177 let Some(tag) = tag else {
1178 return Err(D::Error::custom(format!(
1179 "tagged object has no string `{field}` field"
1180 )));
1181 };
1182 Ok((tag.to_string(), body))
1183}
1184
1185fn read_ordered_tagged(
1186 value: OrderedJsonValue,
1187 field: &'static str,
1188) -> Result<(String, OrderedJsonObject), String> {
1189 let OrderedJsonValue::Object(body) = value else {
1190 return Err(format!("expected tagged object with `{field}` field"));
1191 };
1192 let mut tag = None;
1193 for (key, value) in body.as_entries() {
1194 if key != field {
1195 continue;
1196 }
1197 if tag.is_some() {
1198 return Err(format!("tagged object has duplicate `{field}` field"));
1199 }
1200 let OrderedJsonValue::String(value) = value else {
1201 return Err(format!("tagged object has no string `{field}` field"));
1202 };
1203 tag = Some(value);
1204 }
1205 let Some(tag) = tag else {
1206 return Err(format!("tagged object has no string `{field}` field"));
1207 };
1208 Ok((tag.to_string(), body))
1209}
1210
1211fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1212 body.as_entries()
1213 .iter()
1214 .find_map(|(key, value)| (key == field).then_some(value))
1215}
1216
1217fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1218 match ordered_field(body, field) {
1219 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1220 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1221 None => Err(format!("tagged object has no `{field}` field")),
1222 }
1223}
1224
1225fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1226 let (tag, body) = read_ordered_tagged(value, "method")?;
1227 match tag.as_str() {
1228 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1229 digest: ordered_string(&body, "digest")?,
1230 }),
1231 "macos_spawn_inode" => {
1232 let device = ordered_field(&body, "device")
1233 .and_then(|value| match value {
1234 OrderedJsonValue::Number(number) => number.as_u64(),
1235 _ => None,
1236 })
1237 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1238 let inode = ordered_field(&body, "inode")
1239 .and_then(|value| match value {
1240 OrderedJsonValue::Number(number) => number.as_u64(),
1241 _ => None,
1242 })
1243 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
1244 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1245 }
1246 "windows_spawn_file_id" => {
1247 let wire: RunningImageEvidenceWire = serde_json::from_value(
1248 serde_json::to_value(&body).map_err(|error| error.to_string())?,
1249 )
1250 .map_err(|error| error.to_string())?;
1251 match wire {
1252 RunningImageEvidenceWire::WindowsSpawnFileId {
1253 volume_serial_number,
1254 file_id,
1255 } => Ok(RunningImageEvidence::WindowsSpawnFileId {
1256 volume_serial_number,
1257 file_id,
1258 }),
1259 _ => unreachable!(),
1260 }
1261 }
1262 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1263 }
1264}
1265
1266impl Serialize for ModuleDeclaredProvenance {
1267 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1268 where
1269 S: Serializer,
1270 {
1271 match self {
1272 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1273 build: build.clone(),
1274 }
1275 .serialize(serializer),
1276 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1277 Self::Unknown { body, .. } => body.serialize(serializer),
1278 }
1279 }
1280}
1281
1282impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1283 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1284 where
1285 D: serde::Deserializer<'de>,
1286 {
1287 let (tag, value) = read_tagged(deserializer, "status")?;
1288 match tag.as_str() {
1289 "reported" => match serde_json::from_value(value.into_value())
1290 .map_err(D::Error::custom)?
1291 {
1292 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1293 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1294 },
1295 "unverifiable" => {
1296 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1297 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1298 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1299 }
1300 }
1301 _ => Ok(Self::Unknown { tag, body: value }),
1302 }
1303 }
1304}
1305
1306impl Serialize for RunningImageAgreement {
1307 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1308 where
1309 S: Serializer,
1310 {
1311 match self {
1312 Self::Match { evidence } => RunningImageAgreementWire::Match {
1313 evidence: evidence.clone(),
1314 }
1315 .serialize(serializer),
1316 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1317 running: running.clone(),
1318 disk: disk.clone(),
1319 }
1320 .serialize(serializer),
1321 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1322 reason: reason.clone(),
1323 }
1324 .serialize(serializer),
1325 Self::Unknown { body, .. } => body.serialize(serializer),
1326 }
1327 }
1328}
1329
1330impl Serialize for ReloadPathAgreement {
1331 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1332 where
1333 S: Serializer,
1334 {
1335 match self {
1336 Self::Match => ReloadPathAgreementWire::Match.serialize(serializer),
1337 Self::Mismatch {
1338 configured,
1339 spawned_from,
1340 } => ReloadPathAgreementWire::Mismatch {
1341 configured: configured.clone(),
1342 spawned_from: spawned_from.clone(),
1343 }
1344 .serialize(serializer),
1345 Self::Unavailable { reason } => ReloadPathAgreementWire::Unavailable {
1346 reason: reason.clone(),
1347 }
1348 .serialize(serializer),
1349 Self::Unknown { body, .. } => body.serialize(serializer),
1350 }
1351 }
1352}
1353
1354impl<'de> Deserialize<'de> for ReloadPathAgreement {
1355 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1356 where
1357 D: Deserializer<'de>,
1358 {
1359 let (tag, body) = read_tagged(deserializer, "status")?;
1360 match tag.as_str() {
1361 "match" => Ok(Self::Match),
1362 "mismatch" => {
1363 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1364 ReloadPathAgreementWire::Mismatch {
1365 configured,
1366 spawned_from,
1367 } => Ok(Self::Mismatch {
1368 configured,
1369 spawned_from,
1370 }),
1371 _ => unreachable!(),
1372 }
1373 }
1374 "unavailable" => match serde_json::from_value(body.into_value())
1375 .map_err(D::Error::custom)?
1376 {
1377 ReloadPathAgreementWire::Unavailable { reason } => Ok(Self::Unavailable { reason }),
1378 _ => unreachable!(),
1379 },
1380 _ => Ok(Self::Unknown { tag, body }),
1381 }
1382 }
1383}
1384
1385impl<'de> Deserialize<'de> for RunningImageAgreement {
1386 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1387 where
1388 D: serde::Deserializer<'de>,
1389 {
1390 let (tag, value) = read_tagged(deserializer, "status")?;
1391 match tag.as_str() {
1392 "match" => Ok(Self::Match {
1393 evidence: decode_running_image_evidence(
1394 ordered_field(&value, "evidence")
1395 .cloned()
1396 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1397 )
1398 .map_err(D::Error::custom)?,
1399 }),
1400 "mismatch" => Ok(Self::Mismatch {
1401 running: decode_running_image_evidence(
1402 ordered_field(&value, "running")
1403 .cloned()
1404 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1405 )
1406 .map_err(D::Error::custom)?,
1407 disk: decode_running_image_evidence(
1408 ordered_field(&value, "disk")
1409 .cloned()
1410 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1411 )
1412 .map_err(D::Error::custom)?,
1413 }),
1414 "unavailable" => Ok(Self::Unavailable {
1415 reason: serde_json::from_value(
1416 ordered_field(&value, "reason")
1417 .cloned()
1418 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1419 .into_value(),
1420 )
1421 .map_err(D::Error::custom)?,
1422 }),
1423 _ => Ok(Self::Unknown { tag, body: value }),
1424 }
1425 }
1426}
1427
1428impl Serialize for ChildResourceUsage {
1429 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1430 where
1431 S: Serializer,
1432 {
1433 match self {
1434 Self::Measured(reading) => {
1435 ChildResourceUsageWire::Measured(reading.clone()).serialize(serializer)
1436 }
1437 Self::Unavailable { reason } => ChildResourceUsageWire::Unavailable {
1438 reason: reason.clone(),
1439 }
1440 .serialize(serializer),
1441 Self::Unknown { body, .. } => body.serialize(serializer),
1442 }
1443 }
1444}
1445
1446impl<'de> Deserialize<'de> for ChildResourceUsage {
1447 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1448 where
1449 D: Deserializer<'de>,
1450 {
1451 let (tag, body) = read_tagged(deserializer, "status")?;
1452 match tag.as_str() {
1453 "measured" | "unavailable" => {
1454 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1455 ChildResourceUsageWire::Measured(reading) => Ok(Self::Measured(reading)),
1456 ChildResourceUsageWire::Unavailable { reason } => {
1457 Ok(Self::Unavailable { reason })
1458 }
1459 }
1460 }
1461 _ => Ok(Self::Unknown { tag, body }),
1462 }
1463 }
1464}
1465
1466impl Serialize for RunningImageEvidence {
1467 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1468 where
1469 S: Serializer,
1470 {
1471 match self {
1472 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1473 digest: digest.clone(),
1474 }
1475 .serialize(serializer),
1476 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1477 device: *device,
1478 inode: *inode,
1479 }
1480 .serialize(serializer),
1481 Self::WindowsSpawnFileId {
1482 volume_serial_number,
1483 file_id,
1484 } => RunningImageEvidenceWire::WindowsSpawnFileId {
1485 volume_serial_number: *volume_serial_number,
1486 file_id: *file_id,
1487 }
1488 .serialize(serializer),
1489 Self::Unknown { body, .. } => body.serialize(serializer),
1490 }
1491 }
1492}
1493
1494impl<'de> Deserialize<'de> for RunningImageEvidence {
1495 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1496 where
1497 D: serde::Deserializer<'de>,
1498 {
1499 let (tag, value) = read_tagged(deserializer, "method")?;
1500 match tag.as_str() {
1501 "linux_proc_sha256" => {
1502 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1503 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1504 Ok(Self::LinuxProcSha256 { digest })
1505 }
1506 _ => unreachable!(),
1507 }
1508 }
1509 "macos_spawn_inode" => {
1510 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1511 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1512 Ok(Self::MacosSpawnInode { device, inode })
1513 }
1514 _ => unreachable!(),
1515 }
1516 }
1517 "windows_spawn_file_id" => {
1518 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1519 RunningImageEvidenceWire::WindowsSpawnFileId {
1520 volume_serial_number,
1521 file_id,
1522 } => Ok(Self::WindowsSpawnFileId {
1523 volume_serial_number,
1524 file_id,
1525 }),
1526 _ => unreachable!(),
1527 }
1528 }
1529 _ => Ok(Self::Unknown { tag, body: value }),
1530 }
1531 }
1532}
1533
1534impl Serialize for SupervisorRouteConsumer {
1535 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1536 where
1537 S: Serializer,
1538 {
1539 match self {
1540 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1541 module_id: module_id.clone(),
1542 }
1543 .serialize(serializer),
1544 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1545 connection_id: *connection_id,
1546 }
1547 .serialize(serializer),
1548 Self::Unknown { body, .. } => body.serialize(serializer),
1549 }
1550 }
1551}
1552
1553impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1554 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1555 where
1556 D: serde::Deserializer<'de>,
1557 {
1558 let (tag, value) = read_tagged(deserializer, "kind")?;
1559 match tag.as_str() {
1560 "reserved" => {
1561 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1562 SupervisorRouteConsumerWire::Reserved { module_id } => {
1563 Ok(Self::Reserved { module_id })
1564 }
1565 _ => unreachable!(),
1566 }
1567 }
1568 "direct" => {
1569 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1570 SupervisorRouteConsumerWire::Direct { connection_id } => {
1571 Ok(Self::Direct { connection_id })
1572 }
1573 _ => unreachable!(),
1574 }
1575 }
1576 _ => Ok(Self::Unknown { tag, body: value }),
1577 }
1578 }
1579}
1580
1581impl Serialize for StderrCaptureState {
1582 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1583 where
1584 S: Serializer,
1585 {
1586 match self {
1587 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1588 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1589 reason: reason.clone(),
1590 }
1591 .serialize(serializer),
1592 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1593 reason: reason.clone(),
1594 }
1595 .serialize(serializer),
1596 Self::Unknown { body, .. } => body.serialize(serializer),
1597 }
1598 }
1599}
1600
1601impl<'de> Deserialize<'de> for StderrCaptureState {
1602 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1603 where
1604 D: serde::Deserializer<'de>,
1605 {
1606 let (tag, value) = read_tagged(deserializer, "state")?;
1607 match tag.as_str() {
1608 "captured" => {
1609 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1610 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1611 _ => unreachable!(),
1612 }
1613 }
1614 "incomplete" => match serde_json::from_value(value.into_value())
1615 .map_err(D::Error::custom)?
1616 {
1617 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1618 _ => unreachable!(),
1619 },
1620 "not_captured" => match serde_json::from_value(value.into_value())
1621 .map_err(D::Error::custom)?
1622 {
1623 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1624 _ => unreachable!(),
1625 },
1626 _ => Ok(Self::Unknown { tag, body: value }),
1627 }
1628 }
1629}
1630
1631impl Serialize for StderrTailEntry {
1632 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1633 where
1634 S: Serializer,
1635 {
1636 match self {
1637 Self::Line {
1638 text,
1639 truncated,
1640 at_ms,
1641 } => StderrTailEntryWire::Line {
1642 text: text.clone(),
1643 truncated: *truncated,
1644 at_ms: *at_ms,
1645 }
1646 .serialize(serializer),
1647 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1648 Self::Unknown { body, .. } => body.serialize(serializer),
1649 }
1650 }
1651}
1652
1653impl<'de> Deserialize<'de> for StderrTailEntry {
1654 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1655 where
1656 D: serde::Deserializer<'de>,
1657 {
1658 let (tag, value) = read_tagged(deserializer, "kind")?;
1659 match tag.as_str() {
1660 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1661 StderrTailEntryWire::Line {
1662 text,
1663 truncated,
1664 at_ms,
1665 } => Ok(Self::Line {
1666 text,
1667 truncated,
1668 at_ms,
1669 }),
1670 _ => unreachable!(),
1671 },
1672 "process_start" => {
1673 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1674 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1675 _ => unreachable!(),
1676 }
1677 }
1678 _ => Ok(Self::Unknown { tag, body: value }),
1679 }
1680 }
1681}
1682
1683fn is_zero_u64(value: &u64) -> bool {
1684 *value == 0
1685}
1686
1687fn default_true() -> bool {
1688 true
1689}
1690
1691#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1693pub struct TerminalHistory {
1694 pub daemon_started_at_ms: u64,
1696 pub entries: Vec<TerminalEntry>,
1697 #[serde(default, skip_serializing_if = "is_zero_u64")]
1700 pub dropped: u64,
1701 #[serde(default, skip_serializing_if = "is_zero_u64")]
1704 pub journal_skipped_lines: u64,
1705 #[serde(default, skip_serializing_if = "is_zero_u64")]
1707 pub journal_read_errors: u64,
1708 #[serde(default, skip_serializing_if = "is_zero_u64")]
1710 pub journal_write_failures: u64,
1711}
1712
1713#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1715pub struct TerminalEntry {
1716 #[serde(default, skip_serializing_if = "Option::is_none")]
1719 pub daemon_incarnation: Option<String>,
1720 #[serde(default, skip_serializing_if = "Option::is_none")]
1721 pub exit_code: Option<i32>,
1722 #[serde(default, skip_serializing_if = "Option::is_none")]
1723 pub exit_signal: Option<i32>,
1724 pub at_ms: u64,
1725 pub disposition: TerminalDisposition,
1726 #[serde(default, skip_serializing_if = "Option::is_none")]
1730 pub exit_kind: Option<TerminalExitKind>,
1731 #[serde(default, skip_serializing_if = "Option::is_none")]
1738 pub disposition_detail: Option<String>,
1739}
1740
1741#[derive(Debug, Clone, PartialEq, Eq)]
1746pub enum TerminalExitKind {
1747 Clean,
1748 Crash,
1749 DeliberateSeverance,
1750 Unknown(String),
1751}
1752
1753impl TerminalExitKind {
1754 fn wire_name(&self) -> &str {
1755 match self {
1756 Self::Clean => "clean",
1757 Self::Crash => "crash",
1758 Self::DeliberateSeverance => "deliberate_severance",
1759 Self::Unknown(value) => value,
1760 }
1761 }
1762}
1763
1764impl Serialize for TerminalExitKind {
1765 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1766 where
1767 S: serde::Serializer,
1768 {
1769 serializer.serialize_str(self.wire_name())
1770 }
1771}
1772
1773impl<'de> Deserialize<'de> for TerminalExitKind {
1774 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1775 where
1776 D: serde::Deserializer<'de>,
1777 {
1778 let value = String::deserialize(deserializer)?;
1779 Ok(match value.as_str() {
1780 "clean" => Self::Clean,
1781 "crash" => Self::Crash,
1782 "deliberate_severance" => Self::DeliberateSeverance,
1783 _ => Self::Unknown(value),
1784 })
1785 }
1786}
1787
1788open_string_enum! {
1789 TerminalDisposition {
1791 Stopped => "stopped",
1792 Disabled => "disabled",
1793 Failed => "failed",
1794 Restarting => "restarting",
1795 DaemonShutdown => "daemon_shutdown",
1800 }
1801}
1802
1803#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1804#[serde(rename_all = "snake_case")]
1805pub enum PollKind {
1806 Status,
1807 Liveness,
1808}
1809
1810#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1822#[non_exhaustive]
1823pub struct CatalogEntry {
1824 pub module_id: String,
1825 #[serde(default = "default_true")]
1836 pub ready: bool,
1837 #[serde(default, skip_serializing_if = "Option::is_none")]
1841 pub not_ready: Option<NotReadyReason>,
1842 #[serde(default, skip_serializing_if = "Option::is_none")]
1863 pub module_version: Option<String>,
1864 pub roles: Vec<ProviderRole>,
1865 pub control_ops: Vec<String>,
1866 #[serde(default, skip_serializing_if = "Option::is_none")]
1871 pub capabilities: Option<CapabilityDeclarations>,
1872 #[serde(default, skip_serializing_if = "Option::is_none")]
1875 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1876 #[serde(default, skip_serializing_if = "Option::is_none")]
1880 pub events: Option<Vec<EventDeclaration>>,
1881}
1882
1883impl CatalogEntry {
1884 #[must_use]
1886 pub fn new(
1887 module_id: impl Into<String>,
1888 roles: Vec<ProviderRole>,
1889 control_ops: Vec<String>,
1890 ) -> Self {
1891 Self {
1892 module_id: module_id.into(),
1893 ready: default_true(),
1894 not_ready: None,
1895 module_version: None,
1896 roles,
1897 control_ops,
1898 capabilities: None,
1899 self_signals: None,
1900 events: None,
1901 }
1902 }
1903
1904 #[must_use]
1906 pub fn with_ready(mut self, ready: bool) -> Self {
1907 self.ready = ready;
1908 self
1909 }
1910
1911 #[must_use]
1913 pub fn with_not_ready(mut self, not_ready: Option<NotReadyReason>) -> Self {
1914 self.not_ready = not_ready;
1915 self
1916 }
1917
1918 #[must_use]
1920 pub fn with_module_version(mut self, module_version: Option<String>) -> Self {
1921 self.module_version = module_version;
1922 self
1923 }
1924
1925 #[must_use]
1927 pub fn with_capabilities(mut self, capabilities: Option<CapabilityDeclarations>) -> Self {
1928 self.capabilities = capabilities;
1929 self
1930 }
1931
1932 #[must_use]
1934 pub fn with_self_signals(mut self, self_signals: Option<Vec<SelfSignalDeclaration>>) -> Self {
1935 self.self_signals = self_signals;
1936 self
1937 }
1938
1939 #[must_use]
1941 pub fn with_events(mut self, events: Option<Vec<EventDeclaration>>) -> Self {
1942 self.events = events;
1943 self
1944 }
1945}
1946
1947#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1949pub struct NotReadyReason {
1950 pub reason: String,
1955 #[serde(default, skip_serializing_if = "Option::is_none")]
1958 pub capability: Option<String>,
1959}
1960
1961impl NotReadyReason {
1962 pub const DECLARED_NOT_READY: &'static str = "declared_not_ready";
1963 pub const REQUIRED_CAPABILITY_UNPROVIDED: &'static str = "required_capability_unprovided";
1964}
1965
1966#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1967pub struct CapabilityRequirementStatus {
1968 pub consumer: String,
1969 pub capability: String,
1970 pub need: String,
1971 pub verdict: String,
1972 pub episode_seq: u64,
1973 pub config_satisfiable: bool,
1974 pub runtime_available: bool,
1975 pub detail: String,
1976}
1977
1978#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1979pub struct SupervisorRescanResult {
1980 pub added: Vec<String>,
1981 pub removed: Vec<String>,
1982 pub changed_pending_reload: Vec<String>,
1983 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1996 pub enabled_changes: Vec<String>,
1997 pub unchanged: u32,
1998 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2006 pub preview: bool,
2007 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2025 pub restart_required: Vec<String>,
2026 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2030 pub capability_warnings: Vec<String>,
2031}
2032
2033#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
2049#[serde(rename_all = "snake_case")]
2050pub enum ModuleProtocol {
2051 #[default]
2055 Subc,
2056 None,
2065}
2066
2067#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2079#[non_exhaustive]
2080pub struct SupervisorEntry {
2081 #[serde(default, skip_serializing_if = "Option::is_none")]
2084 pub waiting_for_previous_pid: Option<u32>,
2085 pub module_id: String,
2086 pub state: String,
2087 pub enabled: bool,
2088 pub live: bool,
2098 #[serde(default)]
2102 pub protocol: ModuleProtocol,
2103 #[serde(default, skip_serializing_if = "Option::is_none")]
2106 pub launch_nonce_env: Option<bool>,
2107 #[serde(default, skip_serializing_if = "Option::is_none")]
2113 pub launch_nonce_source: Option<String>,
2114 pub health: SupervisorHealthStatus,
2115 #[serde(default, skip_serializing_if = "Option::is_none")]
2118 pub pending_reload: Option<PendingReloadVerdict>,
2119 #[serde(default)]
2125 pub last_probe_ms: Option<u64>,
2126 #[serde(default, skip_serializing_if = "Option::is_none")]
2130 pub last_exit_code: Option<i32>,
2131 #[serde(default, skip_serializing_if = "Option::is_none")]
2135 pub last_exit_signal: Option<i32>,
2136 #[serde(default, skip_serializing_if = "Option::is_none")]
2140 pub last_exit_ms: Option<u64>,
2141 #[serde(default, skip_serializing_if = "Option::is_none")]
2144 pub last_exit_kind: Option<TerminalExitKind>,
2145 #[serde(default, skip_serializing_if = "Option::is_none")]
2162 pub restart_count: Option<u32>,
2163 #[serde(default, skip_serializing_if = "Option::is_none")]
2166 pub max_restarts: Option<u32>,
2167 #[serde(default, skip_serializing_if = "Option::is_none")]
2170 pub lifetime_restarts: Option<u32>,
2171 #[serde(default, skip_serializing_if = "Option::is_none")]
2175 pub spawn_generation: Option<u64>,
2176 #[serde(default, skip_serializing_if = "Option::is_none")]
2186 pub restart_window_secs: Option<u64>,
2187 #[serde(default, skip_serializing_if = "Option::is_none")]
2191 pub drain_timeout_ms: Option<u64>,
2192 #[serde(default, skip_serializing_if = "Option::is_none")]
2195 pub restart_backoff_ms: Option<u64>,
2196 #[serde(default, skip_serializing_if = "Option::is_none")]
2199 pub restart_max_backoff_ms: Option<u64>,
2200 #[serde(default, skip_serializing_if = "Option::is_none")]
2213 pub resources: Option<ChildResourceUsage>,
2214}
2215
2216impl SupervisorEntry {
2217 #[must_use]
2219 pub fn new(
2220 module_id: impl Into<String>,
2221 state: impl Into<String>,
2222 enabled: bool,
2223 live: bool,
2224 health: SupervisorHealthStatus,
2225 ) -> Self {
2226 Self {
2227 module_id: module_id.into(),
2228 state: state.into(),
2229 waiting_for_previous_pid: None,
2230 enabled,
2231 live,
2232 protocol: ModuleProtocol::default(),
2233 launch_nonce_env: None,
2234 launch_nonce_source: None,
2235 health,
2236 pending_reload: None,
2237 last_probe_ms: None,
2238 last_exit_code: None,
2239 last_exit_signal: None,
2240 last_exit_ms: None,
2241 last_exit_kind: None,
2242 restart_count: None,
2243 max_restarts: None,
2244 lifetime_restarts: None,
2245 spawn_generation: None,
2246 restart_window_secs: None,
2247 drain_timeout_ms: None,
2248 restart_backoff_ms: None,
2249 restart_max_backoff_ms: None,
2250 resources: None,
2251 }
2252 }
2253
2254 #[must_use]
2256 pub fn with_protocol(mut self, protocol: ModuleProtocol) -> Self {
2257 self.protocol = protocol;
2258 self
2259 }
2260
2261 #[must_use]
2263 pub fn with_launch_nonce_env(mut self, launch_nonce_env: Option<bool>) -> Self {
2264 self.launch_nonce_env = launch_nonce_env;
2265 self
2266 }
2267
2268 pub fn with_launch_nonce_source(mut self, source: Option<String>) -> Self {
2269 self.launch_nonce_source = source;
2270 self
2271 }
2272
2273 #[must_use]
2274 pub fn with_waiting_for_previous_pid(mut self, pid: Option<u32>) -> Self {
2275 self.waiting_for_previous_pid = pid;
2276 self
2277 }
2278
2279 #[must_use]
2281 pub fn with_pending_reload(mut self, pending_reload: Option<PendingReloadVerdict>) -> Self {
2282 self.pending_reload = pending_reload;
2283 self
2284 }
2285
2286 #[must_use]
2288 pub fn with_last_probe_ms(mut self, last_probe_ms: Option<u64>) -> Self {
2289 self.last_probe_ms = last_probe_ms;
2290 self
2291 }
2292
2293 #[must_use]
2295 pub fn with_last_exit_code(mut self, last_exit_code: Option<i32>) -> Self {
2296 self.last_exit_code = last_exit_code;
2297 self
2298 }
2299
2300 #[must_use]
2302 pub fn with_last_exit_signal(mut self, last_exit_signal: Option<i32>) -> Self {
2303 self.last_exit_signal = last_exit_signal;
2304 self
2305 }
2306
2307 #[must_use]
2309 pub fn with_last_exit_ms(mut self, last_exit_ms: Option<u64>) -> Self {
2310 self.last_exit_ms = last_exit_ms;
2311 self
2312 }
2313
2314 #[must_use]
2316 pub fn with_last_exit_kind(mut self, last_exit_kind: Option<TerminalExitKind>) -> Self {
2317 self.last_exit_kind = last_exit_kind;
2318 self
2319 }
2320
2321 #[must_use]
2323 pub fn with_restart_count(mut self, restart_count: Option<u32>) -> Self {
2324 self.restart_count = restart_count;
2325 self
2326 }
2327
2328 #[must_use]
2330 pub fn with_max_restarts(mut self, max_restarts: Option<u32>) -> Self {
2331 self.max_restarts = max_restarts;
2332 self
2333 }
2334
2335 #[must_use]
2337 pub fn with_lifetime_restarts(mut self, lifetime_restarts: Option<u32>) -> Self {
2338 self.lifetime_restarts = lifetime_restarts;
2339 self
2340 }
2341
2342 #[must_use]
2344 pub fn with_spawn_generation(mut self, spawn_generation: Option<u64>) -> Self {
2345 self.spawn_generation = spawn_generation;
2346 self
2347 }
2348
2349 #[must_use]
2351 pub fn with_restart_window_secs(mut self, restart_window_secs: Option<u64>) -> Self {
2352 self.restart_window_secs = restart_window_secs;
2353 self
2354 }
2355
2356 #[must_use]
2358 pub fn with_drain_timeout_ms(mut self, drain_timeout_ms: Option<u64>) -> Self {
2359 self.drain_timeout_ms = drain_timeout_ms;
2360 self
2361 }
2362
2363 #[must_use]
2365 pub fn with_restart_backoff_ms(mut self, restart_backoff_ms: Option<u64>) -> Self {
2366 self.restart_backoff_ms = restart_backoff_ms;
2367 self
2368 }
2369
2370 #[must_use]
2372 pub fn with_restart_max_backoff_ms(mut self, restart_max_backoff_ms: Option<u64>) -> Self {
2373 self.restart_max_backoff_ms = restart_max_backoff_ms;
2374 self
2375 }
2376
2377 #[must_use]
2379 pub fn with_resources(mut self, resources: Option<ChildResourceUsage>) -> Self {
2380 self.resources = resources;
2381 self
2382 }
2383}
2384
2385#[derive(Debug, Clone, PartialEq)]
2388pub enum ChildResourceUsage {
2389 Measured(ChildResourceReading),
2390 Unavailable {
2391 reason: ChildResourceUnavailableReason,
2392 },
2393 Unknown {
2396 tag: String,
2397 body: OrderedJsonObject,
2398 },
2399}
2400
2401#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2403pub struct ChildResourceReading {
2404 pub memory_bytes: u64,
2407 pub memory_kind: ChildMemoryKind,
2408 #[serde(default, skip_serializing_if = "Option::is_none")]
2411 pub swap_bytes: Option<u64>,
2412 pub cpu_user_ms: u64,
2416 pub cpu_system_ms: u64,
2419}
2420
2421open_string_enum! {
2422 ChildMemoryKind {
2424 PhysFootprint => "phys_footprint",
2428 ResidentSet => "resident_set",
2431 WindowsWorkingSet => "windows_working_set",
2434 }
2435}
2436
2437open_string_enum! {
2438 ChildResourceUnavailableReason {
2440 NotRunning => "not_running",
2442 UnsupportedPlatform => "unsupported_platform",
2444 Unreadable => "unreadable",
2447 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
2450 }
2451}
2452
2453#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2454#[serde(rename_all = "snake_case")]
2455pub enum SupervisorHealthStatus {
2456 Ok,
2457 Degraded,
2458 Failing,
2459 Unresponsive,
2460 Unknown,
2461}
2462
2463#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2464pub struct SupervisorHealthEntry {
2465 pub module_id: String,
2466 pub status: SupervisorHealthStatus,
2467 #[serde(default, skip_serializing_if = "Option::is_none")]
2473 pub detail: Option<String>,
2474 #[serde(default, skip_serializing_if = "Option::is_none")]
2479 pub metrics: Option<serde_json::Value>,
2480 pub consecutive_failures: u32,
2481 #[serde(default)]
2484 pub late_answer_count: u64,
2485 #[serde(default, skip_serializing_if = "Option::is_none")]
2487 pub last_late_answer_latency_ms: Option<u64>,
2488 #[serde(default)]
2493 pub last_action: Option<String>,
2494 #[serde(default)]
2497 pub last_action_ms: Option<u64>,
2498 #[serde(default, skip_serializing_if = "Option::is_none")]
2511 pub last_probe_ms: Option<u64>,
2512}
2513
2514#[cfg(test)]
2515mod tests {
2516 #[test]
2517 fn windows_running_image_evidence_round_trips_all_sixteen_id_bytes() {
2518 let evidence = super::RunningImageEvidence::WindowsSpawnFileId {
2519 volume_serial_number: u64::MAX,
2520 file_id: [
2521 0, 1, 2, 3, 4, 5, 6, 7, 128, 129, 130, 131, 252, 253, 254, 255,
2522 ],
2523 };
2524 let agreement = super::RunningImageAgreement::Match { evidence };
2525 let encoded = serde_json::to_string(&agreement).unwrap();
2526 assert!(encoded.contains("windows_spawn_file_id"));
2527 assert_eq!(
2528 serde_json::from_str::<super::RunningImageAgreement>(&encoded).unwrap(),
2529 agreement
2530 );
2531 }
2532
2533 #[test]
2534 fn unix_running_image_evidence_keeps_original_wire_bytes() {
2535 assert_eq!(
2536 serde_json::to_string(&super::RunningImageEvidence::MacosSpawnInode {
2537 device: 7,
2538 inode: 11
2539 })
2540 .unwrap(),
2541 "{\"method\":\"macos_spawn_inode\",\"device\":7,\"inode\":11}"
2542 );
2543 assert_eq!(
2544 serde_json::to_string(&super::RunningImageEvidence::LinuxProcSha256 {
2545 digest: "abc".to_owned()
2546 })
2547 .unwrap(),
2548 "{\"method\":\"linux_proc_sha256\",\"digest\":\"abc\"}"
2549 );
2550 }
2551 use super::*;
2552 use subc_protocol::{BindIdentity, RouteTarget};
2553
2554 #[test]
2555 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
2556 let entry = TerminalEntry {
2557 daemon_incarnation: Some("daemon-before-restart".into()),
2558 exit_code: Some(1),
2559 exit_signal: None,
2560 at_ms: 1_700_000_000_123,
2561 disposition: TerminalDisposition::Restarting,
2562 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
2563 disposition_detail: None,
2564 };
2565 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
2566 assert_eq!(
2567 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
2568 ["exit_kind"],
2569 "deliberate_severance"
2570 );
2571
2572 #[derive(serde::Deserialize)]
2573 struct LegacyTerminalEntry {
2574 exit_code: Option<i32>,
2575 exit_signal: Option<i32>,
2576 at_ms: u64,
2577 disposition: TerminalDisposition,
2578 }
2579
2580 let decoded: LegacyTerminalEntry =
2581 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
2582 assert_eq!(decoded.exit_code, Some(1));
2583 assert_eq!(decoded.exit_signal, None);
2584 assert_eq!(decoded.at_ms, 1_700_000_000_123);
2585 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
2586
2587 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
2588 let future: TerminalEntry =
2589 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
2590 assert_eq!(
2591 future.exit_kind,
2592 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
2593 );
2594 }
2595
2596 #[test]
2597 fn terminal_incarnation_is_optional_for_older_daemons() {
2598 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
2599 "at_ms": 123,
2600 "disposition": "stopped"
2601 }))
2602 .unwrap();
2603 let encoded = serde_json::to_value(&entry).unwrap();
2604 assert_eq!(
2605 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
2606 (None, None)
2607 );
2608 }
2609
2610 #[test]
2611 fn route_poll_uses_kind_field() {
2612 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
2613 route_channel: 7,
2614 route_epoch: 11,
2615 kind: PollKind::Status,
2616 })
2617 .unwrap();
2618
2619 assert_eq!(body["op"], "route.poll");
2620 assert_eq!(body["route_epoch"], 11);
2621 assert_eq!(body["kind"], "status");
2622 assert!(body.get("op").is_some());
2623 }
2624
2625 #[test]
2626 fn route_open_is_internally_tagged() {
2627 let request = ClientControlRequest::RouteOpen {
2628 target: RouteTarget::ToolProvider {
2629 module_id: "aft".to_string(),
2630 },
2631 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
2632 consumer_identity: None,
2633 consumer_capabilities: None,
2634 role_versions: None,
2635 admission_facts: None,
2636 scope: None,
2637 };
2638
2639 let body = serde_json::to_value(request).unwrap();
2640 assert_eq!(body["op"], "route.open");
2641 assert_eq!(body["target"]["kind"], "tool_provider");
2642 assert!(body.get("consumer_identity").is_none());
2643 assert!(body.get("consumer_capabilities").is_none());
2644 assert!(body.get("role_versions").is_none());
2645 }
2646
2647 #[test]
2648 fn route_open_without_optional_fields_still_decodes() {
2649 let body = serde_json::json!({
2650 "op": "route.open",
2651 "target": { "kind": "tool_provider", "module_id": "aft" },
2652 "identity": {
2653 "project_root": "/tmp/project",
2654 "harness": "opencode",
2655 "session": "session-1"
2656 }
2657 });
2658
2659 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
2660 let ClientControlRequest::RouteOpen {
2661 consumer_identity,
2662 consumer_capabilities,
2663 role_versions,
2664 admission_facts,
2665 ..
2666 } = decoded
2667 else {
2668 panic!("decoded wrong request variant");
2669 };
2670 assert_eq!(consumer_identity, None);
2671 assert_eq!(consumer_capabilities, None);
2672 assert_eq!(role_versions, None);
2673 assert_eq!(admission_facts, None);
2674 }
2675
2676 #[test]
2677 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
2678 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
2679 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
2680 match decoded {
2681 ClientControlPush::RouteClosed {
2682 excluded_subscriptions,
2683 terminal,
2684 ..
2685 } => {
2686 assert_eq!(excluded_subscriptions, 0);
2687 assert_eq!(terminal, None);
2688 }
2689 other => panic!("unexpected push: {other:?}"),
2690 }
2691 assert!(!serde_json::to_string(&decoded)
2692 .unwrap()
2693 .contains("terminal"));
2694 }
2695
2696 #[test]
2697 fn old_route_closed_decoder_ignores_new_terminal_field() {
2698 #[derive(serde::Deserialize)]
2699 #[serde(tag = "op")]
2700 enum LegacyClientControlPush {
2701 #[serde(rename = "route.closed")]
2702 RouteClosed {
2703 module_id: String,
2704 reason: RouteCloseReason,
2705 drained: bool,
2706 abandoned: u32,
2707 },
2708 }
2709
2710 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
2711 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
2712 match decoded {
2713 LegacyClientControlPush::RouteClosed {
2714 module_id,
2715 reason,
2716 drained,
2717 abandoned,
2718 } => {
2719 assert_eq!(module_id, "aft-tools");
2720 assert_eq!(reason, RouteCloseReason::Crash);
2721 assert!(!drained);
2722 assert_eq!(abandoned, 0);
2723 }
2724 }
2725 }
2726
2727 #[test]
2728 fn supervisor_routes_is_a_control_plane_request() {
2729 let body = serde_json::json!({
2730 "op": "supervisor.routes",
2731 "module_id": "aft"
2732 });
2733
2734 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
2735 assert_eq!(serde_json::to_value(request).unwrap(), body);
2736 }
2737
2738 #[test]
2739 fn diagnostic_string_enums_retain_unknown_wire_values() {
2740 let reason: RunningImageUnavailableReason =
2741 serde_json::from_str("\"future_reason\"").unwrap();
2742 let disposition: TerminalDisposition =
2743 serde_json::from_str("\"future_disposition\"").unwrap();
2744
2745 assert_eq!(
2746 reason,
2747 RunningImageUnavailableReason::Unknown("future_reason".to_string())
2748 );
2749 assert_eq!(
2750 disposition,
2751 TerminalDisposition::Unknown("future_disposition".to_string())
2752 );
2753 }
2754
2755 #[test]
2756 fn diagnostic_string_enums_preserve_existing_wire_names() {
2757 let names = [
2758 (RunningImageUnavailableReason::NotRunning, "not_running"),
2759 (
2760 RunningImageUnavailableReason::UnsupportedPlatform,
2761 "unsupported_platform",
2762 ),
2763 (
2764 RunningImageUnavailableReason::RunningExecutableUnreadable,
2765 "running_executable_unreadable",
2766 ),
2767 (
2768 RunningImageUnavailableReason::RunningExecutablePermissionDenied,
2769 "running_executable_permission_denied",
2770 ),
2771 (
2772 RunningImageUnavailableReason::ProcessHidden,
2773 "process_hidden",
2774 ),
2775 (
2776 RunningImageUnavailableReason::SpawnedPathUnreadable,
2777 "spawned_path_unreadable",
2778 ),
2779 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2780 (
2781 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2782 "process_identity_unconfirmed",
2783 ),
2784 ];
2785 for (value, expected) in names {
2786 let wire = serde_json::to_string(&value).unwrap();
2787 assert_eq!(wire, format!("\"{expected}\""));
2788 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2789 assert_eq!(decoded, value);
2790 }
2791
2792 for (value, expected) in [
2793 (TerminalDisposition::Stopped, "stopped"),
2794 (TerminalDisposition::Disabled, "disabled"),
2795 (TerminalDisposition::Failed, "failed"),
2796 (TerminalDisposition::Restarting, "restarting"),
2797 (TerminalDisposition::DaemonShutdown, "daemon_shutdown"),
2798 ] {
2799 let wire = serde_json::to_string(&value).unwrap();
2800 assert_eq!(wire, format!("\"{expected}\""));
2801 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2802 assert_eq!(decoded, value);
2803 }
2804 }
2805
2806 #[test]
2807 fn supervisor_waiting_previous_pid_is_optional_and_round_trips() {
2808 let entry = SupervisorEntry::new(
2809 "waiting-module",
2810 "starting",
2811 true,
2812 false,
2813 SupervisorHealthStatus::Unknown,
2814 );
2815 let old_wire = serde_json::to_value(&entry).unwrap();
2816 assert!(old_wire.get("waiting_for_previous_pid").is_none());
2817 let decoded: SupervisorEntry = serde_json::from_value(old_wire).unwrap();
2818 assert_eq!(decoded.waiting_for_previous_pid, None);
2819 let entry = entry.with_waiting_for_previous_pid(Some(42));
2820 let wire = serde_json::to_value(&entry).unwrap();
2821 assert_eq!(wire["waiting_for_previous_pid"], 42);
2822 let decoded: SupervisorEntry = serde_json::from_value(wire).unwrap();
2823 assert_eq!(decoded.waiting_for_previous_pid, Some(42));
2824 }
2825
2826 #[test]
2827 fn diagnostic_string_enums_reject_non_string_bodies() {
2828 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2829 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2830 }
2831
2832 #[test]
2833 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2834 let body = serde_json::json!({
2835 "op": "supervisor.provenance",
2836 "daemon": {
2837 "daemon_build": {},
2838 "daemon_observed": {
2839 "running_image": {
2840 "status": "unavailable",
2841 "reason": "not_running"
2842 }
2843 }
2844 },
2845 "modules": [
2846 {
2847 "module_id": "future",
2848 "module_declared": { "status": "unverifiable" },
2849 "daemon_observed": {
2850 "running_image": {
2851 "status": "unavailable",
2852 "reason": "future_reason"
2853 }
2854 }
2855 },
2856 {
2857 "module_id": "healthy-a",
2858 "module_declared": { "status": "unverifiable" },
2859 "daemon_observed": {
2860 "running_image": {
2861 "status": "match",
2862 "evidence": {
2863 "method": "linux_proc_sha256",
2864 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2865 }
2866 }
2867 }
2868 },
2869 {
2870 "module_id": "healthy-b",
2871 "module_declared": { "status": "unverifiable" },
2872 "daemon_observed": {
2873 "running_image": {
2874 "status": "unavailable",
2875 "reason": "unsupported_platform"
2876 }
2877 }
2878 }
2879 ]
2880 });
2881
2882 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2883 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2884 panic!("decoded wrong response variant");
2885 };
2886 assert_eq!(modules.len(), 3);
2887 assert_eq!(modules[0].module_id, "future");
2888 assert_eq!(
2889 modules[0].daemon_observed.running_image,
2890 RunningImageAgreement::Unavailable {
2891 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2892 }
2893 );
2894 assert_eq!(modules[1].module_id, "healthy-a");
2895 assert_eq!(modules[2].module_id, "healthy-b");
2896 }
2897
2898 #[test]
2899 fn tagged_unknown_values_retain_tag_and_body() {
2900 macro_rules! assert_unknown_round_trip {
2901 ($ty:ident, $field:literal, $value:expr) => {
2902 let value = $value;
2903 let wire = serde_json::to_string(&value).unwrap();
2904 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2905 match decoded {
2906 $ty::Unknown { tag, body } => {
2907 assert_eq!(tag, value[$field].as_str().unwrap());
2908 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2909 }
2910 _ => panic!("decoded known variant"),
2911 }
2912 };
2913 }
2914
2915 assert_unknown_round_trip!(
2916 ModuleDeclaredProvenance,
2917 "status",
2918 serde_json::json!({"status": "future", "build": {"version": 7}})
2919 );
2920 assert_unknown_round_trip!(
2921 RunningImageAgreement,
2922 "status",
2923 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2924 );
2925 assert_unknown_round_trip!(
2926 RunningImageEvidence,
2927 "method",
2928 serde_json::json!({"method": "future", "digest": "abc"})
2929 );
2930 assert_unknown_round_trip!(
2931 SupervisorRouteConsumer,
2932 "kind",
2933 serde_json::json!({"kind": "future", "module_id": "m"})
2934 );
2935 assert_unknown_round_trip!(
2936 StderrCaptureState,
2937 "state",
2938 serde_json::json!({"state": "future", "reason": "because"})
2939 );
2940 assert_unknown_round_trip!(
2941 StderrTailEntry,
2942 "kind",
2943 serde_json::json!({"kind": "future", "text": "line"})
2944 );
2945 assert_unknown_round_trip!(
2946 ChildResourceUsage,
2947 "status",
2948 serde_json::json!({"status": "future", "memory_bytes": 1})
2949 );
2950 }
2951
2952 #[test]
2953 fn child_resource_usage_round_trips_both_known_states() {
2954 let measured = ChildResourceUsage::Measured(ChildResourceReading {
2955 memory_bytes: 0,
2956 memory_kind: ChildMemoryKind::ResidentSet,
2957 swap_bytes: Some(0),
2958 cpu_user_ms: 0,
2959 cpu_system_ms: 0,
2960 });
2961 let wire = serde_json::to_value(&measured).unwrap();
2962 assert_eq!(
2963 wire,
2964 serde_json::json!({
2965 "status": "measured",
2966 "memory_bytes": 0,
2967 "memory_kind": "resident_set",
2968 "swap_bytes": 0,
2969 "cpu_user_ms": 0,
2970 "cpu_system_ms": 0
2971 })
2972 );
2973 assert_eq!(
2974 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2975 measured
2976 );
2977
2978 let unavailable = ChildResourceUsage::Unavailable {
2979 reason: ChildResourceUnavailableReason::NotRunning,
2980 };
2981 let wire = serde_json::to_value(&unavailable).unwrap();
2982 assert_eq!(
2983 wire,
2984 serde_json::json!({"status": "unavailable", "reason": "not_running"})
2985 );
2986 assert_eq!(
2987 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2988 unavailable
2989 );
2990 }
2991
2992 #[test]
2993 fn a_stderr_line_decodes_with_and_without_its_capture_time() {
2994 let stamped: StderrTailEntry = serde_json::from_str(
2998 r#"{"kind":"line","text":"boom","truncated":true,"at_ms":1789801440685}"#,
2999 )
3000 .unwrap();
3001 assert_eq!(
3002 stamped,
3003 StderrTailEntry::Line {
3004 text: "boom".to_string(),
3005 truncated: true,
3006 at_ms: Some(1_789_801_440_685),
3007 }
3008 );
3009 let unstamped: StderrTailEntry =
3010 serde_json::from_str(r#"{"kind":"line","text":"boom"}"#).unwrap();
3011 assert_eq!(
3012 unstamped,
3013 StderrTailEntry::Line {
3014 text: "boom".to_string(),
3015 truncated: false,
3016 at_ms: None,
3017 }
3018 );
3019 assert_eq!(
3022 serde_json::to_string(&unstamped).unwrap(),
3023 r#"{"kind":"line","text":"boom"}"#
3024 );
3025 assert_eq!(
3026 serde_json::to_value(&stamped).unwrap()["at_ms"],
3027 serde_json::json!(1_789_801_440_685u64)
3028 );
3029 }
3030
3031 #[test]
3032 fn tagged_unknown_values_round_trip_the_original_json() {
3033 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
3034 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
3035 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
3036 }
3037
3038 #[test]
3039 fn tagged_unknown_values_round_trip_trailing_tag() {
3040 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
3041 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
3042 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
3043
3044 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
3045 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
3046 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
3047 }
3048
3049 #[test]
3050 fn tagged_unknown_values_round_trip_middle_tag() {
3051 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
3052 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
3053 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
3054
3055 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
3056 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
3057 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
3058 }
3059
3060 #[test]
3061 fn tagged_unknown_values_round_trip_deep_payload() {
3062 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
3063 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
3064 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
3065
3066 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
3067 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
3068 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
3069 }
3070
3071 #[test]
3072 fn tagged_unknown_values_reject_non_object_bodies() {
3073 for wire in ["42", r#""future""#, "[]"] {
3074 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
3075 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
3076 }
3077 }
3078
3079 #[test]
3080 fn duplicate_discriminators_reject_without_panicking() {
3081 assert_eq!(
3082 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
3083 .unwrap(),
3084 ModuleDeclaredProvenance::Unverifiable
3085 );
3086 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
3087 .unwrap()
3088 {
3089 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
3090 _ => panic!("future discriminator decoded as a known variant"),
3091 }
3092
3093 let wires = [
3094 r#"{"status":"reported","status":"unverifiable"}"#,
3095 r#"{"status":"unverifiable","status":"reported"}"#,
3096 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
3097 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
3098 ];
3099
3100 for wire in wires {
3101 let result =
3102 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
3103 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
3104 assert!(
3105 result.unwrap().is_err(),
3106 "duplicate discriminator decoded: {wire}"
3107 );
3108 }
3109
3110 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
3111 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
3112 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
3113 assert!(
3114 result.unwrap().is_err(),
3115 "duplicate discriminator decoded: {wire}"
3116 );
3117 }
3118
3119 #[test]
3120 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
3121 let known_wire =
3122 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
3123 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
3124 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
3125
3126 for wire in [
3127 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
3128 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
3129 ] {
3130 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
3131 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
3132 }
3133
3134 for wire in [
3135 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
3136 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
3137 ] {
3138 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
3139 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
3140 }
3141
3142 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
3143 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
3144 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
3145
3146 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
3147 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
3148 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
3149 }
3150
3151 #[test]
3152 fn tagged_unknown_member_does_not_discard_known_siblings() {
3153 let body = serde_json::json!({
3154 "modules": [{
3155 "module_id": "target",
3156 "routes": [
3157 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
3158 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
3159 ]
3160 }]
3161 });
3162 let decoded: ClientControlResponse = serde_json::from_value(
3163 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
3164 )
3165 .unwrap();
3166 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
3167 panic!("decoded wrong response variant");
3168 };
3169 assert_eq!(modules[0].routes.len(), 2);
3170 assert_eq!(
3171 modules[0].routes[1].consumer,
3172 SupervisorRouteConsumer::Direct { connection_id: 7 }
3173 );
3174 }
3175}
3176
3177#[cfg(test)]
3178mod launch_nonce_redaction_tests {
3179 use super::*;
3180
3181 const NONCE: &str = "nonce-f00dfeed1234abcd";
3182
3183 fn identity() -> ConsumerIdentity {
3184 ConsumerIdentity {
3185 module_id: "wernicke".to_string(),
3186 launch_nonce: NONCE.to_string(),
3187 }
3188 }
3189
3190 #[test]
3191 fn consumer_identity_debug_names_the_module_and_never_the_nonce() {
3192 let printed = format!("{:?}", identity());
3193 assert!(printed.contains("wernicke"), "{printed}");
3194 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
3195 }
3196
3197 #[test]
3198 fn route_open_request_debug_never_prints_the_nonce() {
3199 let request = ClientControlRequest::RouteOpen {
3200 target: subc_protocol::RouteTarget::ToolProvider {
3201 module_id: "broca".to_string(),
3202 },
3203 identity: subc_protocol::BindIdentity::new(
3204 PathBuf::from("/tmp/project"),
3205 "test".to_string(),
3206 "session".to_string(),
3207 ),
3208 consumer_identity: Some(identity()),
3209 consumer_capabilities: None,
3210 role_versions: None,
3211 admission_facts: None,
3212 scope: None,
3213 };
3214 let printed = format!("{request:?}");
3215 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
3216 }
3217}