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 SpawnedPathUnreadable => "spawned_path_unreadable",
773 HashFailed => "hash_failed",
774 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
775 }
776}
777
778#[derive(Debug, Clone, PartialEq)]
784pub enum SupervisorRouteConsumer {
785 Reserved {
786 module_id: String,
787 },
788 Direct {
789 connection_id: u64,
790 },
791 Unknown {
794 tag: String,
795 body: OrderedJsonObject,
796 },
797}
798
799#[derive(Debug, Clone, PartialEq)]
806pub enum StderrCaptureState {
807 Captured,
810 Incomplete { reason: String },
812 NotCaptured { reason: String },
814 Unknown {
817 tag: String,
818 body: OrderedJsonObject,
819 },
820}
821
822#[derive(Debug, Clone, PartialEq)]
823pub enum StderrTailEntry {
824 Line {
825 text: String,
826 truncated: bool,
831 at_ms: Option<u64>,
835 },
836 ProcessStart,
841 Unknown {
844 tag: String,
845 body: OrderedJsonObject,
846 },
847}
848
849#[derive(Debug, Serialize, Deserialize)]
850#[serde(tag = "status", rename_all = "snake_case")]
851enum ModuleDeclaredProvenanceWire {
852 Reported { build: ManifestProvenance },
853 Unverifiable,
854}
855
856#[derive(Debug, Serialize, Deserialize)]
857#[serde(tag = "status", rename_all = "snake_case")]
858enum RunningImageAgreementWire {
859 Match {
860 evidence: RunningImageEvidence,
861 },
862 Mismatch {
863 running: RunningImageEvidence,
864 disk: RunningImageEvidence,
865 },
866 Unavailable {
867 reason: RunningImageUnavailableReason,
868 },
869}
870
871#[derive(Debug, Serialize, Deserialize)]
872#[serde(tag = "status", rename_all = "snake_case")]
873enum ReloadPathAgreementWire {
874 Match,
875 Mismatch {
876 configured: PathBuf,
877 spawned_from: PathBuf,
878 },
879 Unavailable {
880 reason: ReloadPathUnavailableReason,
881 },
882}
883
884#[derive(Debug, Serialize, Deserialize)]
885#[serde(tag = "method", rename_all = "snake_case")]
886enum RunningImageEvidenceWire {
887 LinuxProcSha256 {
888 digest: String,
889 },
890 MacosSpawnInode {
891 device: u64,
892 inode: u64,
893 },
894 WindowsSpawnFileId {
895 volume_serial_number: u64,
896 file_id: [u8; 16],
897 },
898}
899
900#[derive(Debug, Serialize, Deserialize)]
901#[serde(tag = "kind", rename_all = "snake_case")]
902enum SupervisorRouteConsumerWire {
903 Reserved { module_id: String },
904 Direct { connection_id: u64 },
905}
906
907#[derive(Debug, Serialize, Deserialize)]
908#[serde(tag = "state", rename_all = "snake_case")]
909enum StderrCaptureStateWire {
910 Captured,
911 Incomplete { reason: String },
912 NotCaptured { reason: String },
913}
914
915#[derive(Debug, Serialize, Deserialize)]
916#[serde(tag = "status", rename_all = "snake_case")]
917enum ChildResourceUsageWire {
918 Measured(ChildResourceReading),
919 Unavailable {
920 reason: ChildResourceUnavailableReason,
921 },
922}
923
924#[derive(Debug, Serialize, Deserialize)]
925#[serde(tag = "kind", rename_all = "snake_case")]
926enum StderrTailEntryWire {
927 Line {
928 text: String,
929 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
930 truncated: bool,
931 #[serde(default, skip_serializing_if = "Option::is_none")]
934 at_ms: Option<u64>,
935 },
936 ProcessStart,
937}
938
939#[derive(Debug, Clone, PartialEq)]
941pub enum OrderedJsonValue {
942 Null,
943 Bool(bool),
944 Number(serde_json::Number),
945 String(String),
946 Array(Vec<Self>),
947 Object(OrderedJsonObject),
948}
949
950#[derive(Debug, Clone, PartialEq)]
952pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
953
954impl OrderedJsonObject {
955 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
957 &self.0
958 }
959
960 fn into_value(self) -> serde_json::Value {
961 serde_json::Value::Object(
962 self.0
963 .into_iter()
964 .map(|(key, value)| (key, value.into_value()))
965 .collect(),
966 )
967 }
968}
969
970impl OrderedJsonValue {
971 fn into_value(self) -> serde_json::Value {
972 match self {
973 Self::Null => serde_json::Value::Null,
974 Self::Bool(value) => serde_json::Value::Bool(value),
975 Self::Number(value) => serde_json::Value::Number(value),
976 Self::String(value) => serde_json::Value::String(value),
977 Self::Array(values) => {
978 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
979 }
980 Self::Object(value) => value.into_value(),
981 }
982 }
983}
984
985impl Serialize for OrderedJsonValue {
986 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
987 where
988 S: Serializer,
989 {
990 match self {
991 Self::Null => serializer.serialize_unit(),
992 Self::Bool(value) => serializer.serialize_bool(*value),
993 Self::Number(value) => value.serialize(serializer),
994 Self::String(value) => serializer.serialize_str(value),
995 Self::Array(values) => values.serialize(serializer),
996 Self::Object(value) => value.serialize(serializer),
997 }
998 }
999}
1000
1001impl<'de> Deserialize<'de> for OrderedJsonValue {
1002 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1003 where
1004 D: Deserializer<'de>,
1005 {
1006 struct OrderedValueVisitor;
1007
1008 impl<'de> Visitor<'de> for OrderedValueVisitor {
1009 type Value = OrderedJsonValue;
1010
1011 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1012 formatter.write_str("a JSON value with ordered object members")
1013 }
1014
1015 fn visit_unit<E>(self) -> Result<Self::Value, E>
1016 where
1017 E: serde::de::Error,
1018 {
1019 Ok(OrderedJsonValue::Null)
1020 }
1021
1022 fn visit_none<E>(self) -> Result<Self::Value, E>
1023 where
1024 E: serde::de::Error,
1025 {
1026 Ok(OrderedJsonValue::Null)
1027 }
1028
1029 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1030 where
1031 D: Deserializer<'de>,
1032 {
1033 OrderedJsonValue::deserialize(deserializer)
1034 }
1035
1036 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
1037 where
1038 E: serde::de::Error,
1039 {
1040 Ok(OrderedJsonValue::Bool(value))
1041 }
1042
1043 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
1044 where
1045 E: serde::de::Error,
1046 {
1047 Ok(OrderedJsonValue::Number(value.into()))
1048 }
1049
1050 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
1051 where
1052 E: serde::de::Error,
1053 {
1054 Ok(OrderedJsonValue::Number(value.into()))
1055 }
1056
1057 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1058 where
1059 E: serde::de::Error,
1060 {
1061 serde_json::Number::from_f64(value)
1062 .map(OrderedJsonValue::Number)
1063 .ok_or_else(|| E::custom("non-finite JSON number"))
1064 }
1065
1066 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1067 where
1068 E: serde::de::Error,
1069 {
1070 Ok(OrderedJsonValue::String(value.to_owned()))
1071 }
1072
1073 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
1074 where
1075 E: serde::de::Error,
1076 {
1077 Ok(OrderedJsonValue::String(value))
1078 }
1079
1080 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1081 where
1082 A: SeqAccess<'de>,
1083 {
1084 let mut values = Vec::new();
1085 while let Some(value) = sequence.next_element()? {
1086 values.push(value);
1087 }
1088 Ok(OrderedJsonValue::Array(values))
1089 }
1090
1091 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1092 where
1093 A: MapAccess<'de>,
1094 {
1095 let mut entries = Vec::new();
1096 while let Some((key, value)) = map.next_entry()? {
1097 entries.push((key, value));
1098 }
1099 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
1100 }
1101 }
1102
1103 deserializer.deserialize_any(OrderedValueVisitor)
1104 }
1105}
1106
1107impl Serialize for OrderedJsonObject {
1108 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1109 where
1110 S: Serializer,
1111 {
1112 let mut map = serializer.serialize_map(Some(self.0.len()))?;
1113 for (key, value) in &self.0 {
1114 map.serialize_entry(key, value)?;
1115 }
1116 map.end()
1117 }
1118}
1119
1120impl<'de> Deserialize<'de> for OrderedJsonObject {
1121 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1122 where
1123 D: Deserializer<'de>,
1124 {
1125 struct OrderedObjectVisitor;
1126
1127 impl<'de> Visitor<'de> for OrderedObjectVisitor {
1128 type Value = OrderedJsonObject;
1129
1130 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1131 formatter.write_str("an object with ordered JSON members")
1132 }
1133
1134 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1135 where
1136 A: MapAccess<'de>,
1137 {
1138 let mut entries = Vec::new();
1139 while let Some((key, value)) = map.next_entry()? {
1140 entries.push((key, value));
1141 }
1142 Ok(OrderedJsonObject(entries))
1143 }
1144 }
1145
1146 deserializer.deserialize_map(OrderedObjectVisitor)
1147 }
1148}
1149
1150fn read_tagged<'de, D>(
1151 deserializer: D,
1152 field: &'static str,
1153) -> Result<(String, OrderedJsonObject), D::Error>
1154where
1155 D: Deserializer<'de>,
1156{
1157 let body = OrderedJsonObject::deserialize(deserializer)?;
1158 let mut tag = None;
1159 for (key, value) in body.as_entries() {
1160 if key != field {
1161 continue;
1162 }
1163 if tag.is_some() {
1164 return Err(D::Error::custom(format!(
1165 "tagged object has duplicate `{field}` field"
1166 )));
1167 }
1168 let OrderedJsonValue::String(value) = value else {
1169 return Err(D::Error::custom(format!(
1170 "tagged object has no string `{field}` field"
1171 )));
1172 };
1173 tag = Some(value);
1174 }
1175 let Some(tag) = tag else {
1176 return Err(D::Error::custom(format!(
1177 "tagged object has no string `{field}` field"
1178 )));
1179 };
1180 Ok((tag.to_string(), body))
1181}
1182
1183fn read_ordered_tagged(
1184 value: OrderedJsonValue,
1185 field: &'static str,
1186) -> Result<(String, OrderedJsonObject), String> {
1187 let OrderedJsonValue::Object(body) = value else {
1188 return Err(format!("expected tagged object with `{field}` field"));
1189 };
1190 let mut tag = None;
1191 for (key, value) in body.as_entries() {
1192 if key != field {
1193 continue;
1194 }
1195 if tag.is_some() {
1196 return Err(format!("tagged object has duplicate `{field}` field"));
1197 }
1198 let OrderedJsonValue::String(value) = value else {
1199 return Err(format!("tagged object has no string `{field}` field"));
1200 };
1201 tag = Some(value);
1202 }
1203 let Some(tag) = tag else {
1204 return Err(format!("tagged object has no string `{field}` field"));
1205 };
1206 Ok((tag.to_string(), body))
1207}
1208
1209fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1210 body.as_entries()
1211 .iter()
1212 .find_map(|(key, value)| (key == field).then_some(value))
1213}
1214
1215fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1216 match ordered_field(body, field) {
1217 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1218 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1219 None => Err(format!("tagged object has no `{field}` field")),
1220 }
1221}
1222
1223fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1224 let (tag, body) = read_ordered_tagged(value, "method")?;
1225 match tag.as_str() {
1226 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1227 digest: ordered_string(&body, "digest")?,
1228 }),
1229 "macos_spawn_inode" => {
1230 let device = ordered_field(&body, "device")
1231 .and_then(|value| match value {
1232 OrderedJsonValue::Number(number) => number.as_u64(),
1233 _ => None,
1234 })
1235 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1236 let inode = ordered_field(&body, "inode")
1237 .and_then(|value| match value {
1238 OrderedJsonValue::Number(number) => number.as_u64(),
1239 _ => None,
1240 })
1241 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
1242 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1243 }
1244 "windows_spawn_file_id" => {
1245 let wire: RunningImageEvidenceWire = serde_json::from_value(
1246 serde_json::to_value(&body).map_err(|error| error.to_string())?,
1247 )
1248 .map_err(|error| error.to_string())?;
1249 match wire {
1250 RunningImageEvidenceWire::WindowsSpawnFileId {
1251 volume_serial_number,
1252 file_id,
1253 } => Ok(RunningImageEvidence::WindowsSpawnFileId {
1254 volume_serial_number,
1255 file_id,
1256 }),
1257 _ => unreachable!(),
1258 }
1259 }
1260 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1261 }
1262}
1263
1264impl Serialize for ModuleDeclaredProvenance {
1265 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1266 where
1267 S: Serializer,
1268 {
1269 match self {
1270 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1271 build: build.clone(),
1272 }
1273 .serialize(serializer),
1274 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1275 Self::Unknown { body, .. } => body.serialize(serializer),
1276 }
1277 }
1278}
1279
1280impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1281 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1282 where
1283 D: serde::Deserializer<'de>,
1284 {
1285 let (tag, value) = read_tagged(deserializer, "status")?;
1286 match tag.as_str() {
1287 "reported" => match serde_json::from_value(value.into_value())
1288 .map_err(D::Error::custom)?
1289 {
1290 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1291 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1292 },
1293 "unverifiable" => {
1294 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1295 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1296 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1297 }
1298 }
1299 _ => Ok(Self::Unknown { tag, body: value }),
1300 }
1301 }
1302}
1303
1304impl Serialize for RunningImageAgreement {
1305 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1306 where
1307 S: Serializer,
1308 {
1309 match self {
1310 Self::Match { evidence } => RunningImageAgreementWire::Match {
1311 evidence: evidence.clone(),
1312 }
1313 .serialize(serializer),
1314 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1315 running: running.clone(),
1316 disk: disk.clone(),
1317 }
1318 .serialize(serializer),
1319 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1320 reason: reason.clone(),
1321 }
1322 .serialize(serializer),
1323 Self::Unknown { body, .. } => body.serialize(serializer),
1324 }
1325 }
1326}
1327
1328impl Serialize for ReloadPathAgreement {
1329 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1330 where
1331 S: Serializer,
1332 {
1333 match self {
1334 Self::Match => ReloadPathAgreementWire::Match.serialize(serializer),
1335 Self::Mismatch {
1336 configured,
1337 spawned_from,
1338 } => ReloadPathAgreementWire::Mismatch {
1339 configured: configured.clone(),
1340 spawned_from: spawned_from.clone(),
1341 }
1342 .serialize(serializer),
1343 Self::Unavailable { reason } => ReloadPathAgreementWire::Unavailable {
1344 reason: reason.clone(),
1345 }
1346 .serialize(serializer),
1347 Self::Unknown { body, .. } => body.serialize(serializer),
1348 }
1349 }
1350}
1351
1352impl<'de> Deserialize<'de> for ReloadPathAgreement {
1353 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1354 where
1355 D: Deserializer<'de>,
1356 {
1357 let (tag, body) = read_tagged(deserializer, "status")?;
1358 match tag.as_str() {
1359 "match" => Ok(Self::Match),
1360 "mismatch" => {
1361 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1362 ReloadPathAgreementWire::Mismatch {
1363 configured,
1364 spawned_from,
1365 } => Ok(Self::Mismatch {
1366 configured,
1367 spawned_from,
1368 }),
1369 _ => unreachable!(),
1370 }
1371 }
1372 "unavailable" => match serde_json::from_value(body.into_value())
1373 .map_err(D::Error::custom)?
1374 {
1375 ReloadPathAgreementWire::Unavailable { reason } => Ok(Self::Unavailable { reason }),
1376 _ => unreachable!(),
1377 },
1378 _ => Ok(Self::Unknown { tag, body }),
1379 }
1380 }
1381}
1382
1383impl<'de> Deserialize<'de> for RunningImageAgreement {
1384 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1385 where
1386 D: serde::Deserializer<'de>,
1387 {
1388 let (tag, value) = read_tagged(deserializer, "status")?;
1389 match tag.as_str() {
1390 "match" => Ok(Self::Match {
1391 evidence: decode_running_image_evidence(
1392 ordered_field(&value, "evidence")
1393 .cloned()
1394 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1395 )
1396 .map_err(D::Error::custom)?,
1397 }),
1398 "mismatch" => Ok(Self::Mismatch {
1399 running: decode_running_image_evidence(
1400 ordered_field(&value, "running")
1401 .cloned()
1402 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1403 )
1404 .map_err(D::Error::custom)?,
1405 disk: decode_running_image_evidence(
1406 ordered_field(&value, "disk")
1407 .cloned()
1408 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1409 )
1410 .map_err(D::Error::custom)?,
1411 }),
1412 "unavailable" => Ok(Self::Unavailable {
1413 reason: serde_json::from_value(
1414 ordered_field(&value, "reason")
1415 .cloned()
1416 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1417 .into_value(),
1418 )
1419 .map_err(D::Error::custom)?,
1420 }),
1421 _ => Ok(Self::Unknown { tag, body: value }),
1422 }
1423 }
1424}
1425
1426impl Serialize for ChildResourceUsage {
1427 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1428 where
1429 S: Serializer,
1430 {
1431 match self {
1432 Self::Measured(reading) => {
1433 ChildResourceUsageWire::Measured(reading.clone()).serialize(serializer)
1434 }
1435 Self::Unavailable { reason } => ChildResourceUsageWire::Unavailable {
1436 reason: reason.clone(),
1437 }
1438 .serialize(serializer),
1439 Self::Unknown { body, .. } => body.serialize(serializer),
1440 }
1441 }
1442}
1443
1444impl<'de> Deserialize<'de> for ChildResourceUsage {
1445 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1446 where
1447 D: Deserializer<'de>,
1448 {
1449 let (tag, body) = read_tagged(deserializer, "status")?;
1450 match tag.as_str() {
1451 "measured" | "unavailable" => {
1452 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1453 ChildResourceUsageWire::Measured(reading) => Ok(Self::Measured(reading)),
1454 ChildResourceUsageWire::Unavailable { reason } => {
1455 Ok(Self::Unavailable { reason })
1456 }
1457 }
1458 }
1459 _ => Ok(Self::Unknown { tag, body }),
1460 }
1461 }
1462}
1463
1464impl Serialize for RunningImageEvidence {
1465 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1466 where
1467 S: Serializer,
1468 {
1469 match self {
1470 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1471 digest: digest.clone(),
1472 }
1473 .serialize(serializer),
1474 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1475 device: *device,
1476 inode: *inode,
1477 }
1478 .serialize(serializer),
1479 Self::WindowsSpawnFileId {
1480 volume_serial_number,
1481 file_id,
1482 } => RunningImageEvidenceWire::WindowsSpawnFileId {
1483 volume_serial_number: *volume_serial_number,
1484 file_id: *file_id,
1485 }
1486 .serialize(serializer),
1487 Self::Unknown { body, .. } => body.serialize(serializer),
1488 }
1489 }
1490}
1491
1492impl<'de> Deserialize<'de> for RunningImageEvidence {
1493 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1494 where
1495 D: serde::Deserializer<'de>,
1496 {
1497 let (tag, value) = read_tagged(deserializer, "method")?;
1498 match tag.as_str() {
1499 "linux_proc_sha256" => {
1500 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1501 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1502 Ok(Self::LinuxProcSha256 { digest })
1503 }
1504 _ => unreachable!(),
1505 }
1506 }
1507 "macos_spawn_inode" => {
1508 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1509 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1510 Ok(Self::MacosSpawnInode { device, inode })
1511 }
1512 _ => unreachable!(),
1513 }
1514 }
1515 "windows_spawn_file_id" => {
1516 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1517 RunningImageEvidenceWire::WindowsSpawnFileId {
1518 volume_serial_number,
1519 file_id,
1520 } => Ok(Self::WindowsSpawnFileId {
1521 volume_serial_number,
1522 file_id,
1523 }),
1524 _ => unreachable!(),
1525 }
1526 }
1527 _ => Ok(Self::Unknown { tag, body: value }),
1528 }
1529 }
1530}
1531
1532impl Serialize for SupervisorRouteConsumer {
1533 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1534 where
1535 S: Serializer,
1536 {
1537 match self {
1538 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1539 module_id: module_id.clone(),
1540 }
1541 .serialize(serializer),
1542 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1543 connection_id: *connection_id,
1544 }
1545 .serialize(serializer),
1546 Self::Unknown { body, .. } => body.serialize(serializer),
1547 }
1548 }
1549}
1550
1551impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1552 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1553 where
1554 D: serde::Deserializer<'de>,
1555 {
1556 let (tag, value) = read_tagged(deserializer, "kind")?;
1557 match tag.as_str() {
1558 "reserved" => {
1559 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1560 SupervisorRouteConsumerWire::Reserved { module_id } => {
1561 Ok(Self::Reserved { module_id })
1562 }
1563 _ => unreachable!(),
1564 }
1565 }
1566 "direct" => {
1567 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1568 SupervisorRouteConsumerWire::Direct { connection_id } => {
1569 Ok(Self::Direct { connection_id })
1570 }
1571 _ => unreachable!(),
1572 }
1573 }
1574 _ => Ok(Self::Unknown { tag, body: value }),
1575 }
1576 }
1577}
1578
1579impl Serialize for StderrCaptureState {
1580 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1581 where
1582 S: Serializer,
1583 {
1584 match self {
1585 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1586 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1587 reason: reason.clone(),
1588 }
1589 .serialize(serializer),
1590 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1591 reason: reason.clone(),
1592 }
1593 .serialize(serializer),
1594 Self::Unknown { body, .. } => body.serialize(serializer),
1595 }
1596 }
1597}
1598
1599impl<'de> Deserialize<'de> for StderrCaptureState {
1600 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1601 where
1602 D: serde::Deserializer<'de>,
1603 {
1604 let (tag, value) = read_tagged(deserializer, "state")?;
1605 match tag.as_str() {
1606 "captured" => {
1607 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1608 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1609 _ => unreachable!(),
1610 }
1611 }
1612 "incomplete" => match serde_json::from_value(value.into_value())
1613 .map_err(D::Error::custom)?
1614 {
1615 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1616 _ => unreachable!(),
1617 },
1618 "not_captured" => match serde_json::from_value(value.into_value())
1619 .map_err(D::Error::custom)?
1620 {
1621 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1622 _ => unreachable!(),
1623 },
1624 _ => Ok(Self::Unknown { tag, body: value }),
1625 }
1626 }
1627}
1628
1629impl Serialize for StderrTailEntry {
1630 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1631 where
1632 S: Serializer,
1633 {
1634 match self {
1635 Self::Line {
1636 text,
1637 truncated,
1638 at_ms,
1639 } => StderrTailEntryWire::Line {
1640 text: text.clone(),
1641 truncated: *truncated,
1642 at_ms: *at_ms,
1643 }
1644 .serialize(serializer),
1645 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1646 Self::Unknown { body, .. } => body.serialize(serializer),
1647 }
1648 }
1649}
1650
1651impl<'de> Deserialize<'de> for StderrTailEntry {
1652 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1653 where
1654 D: serde::Deserializer<'de>,
1655 {
1656 let (tag, value) = read_tagged(deserializer, "kind")?;
1657 match tag.as_str() {
1658 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1659 StderrTailEntryWire::Line {
1660 text,
1661 truncated,
1662 at_ms,
1663 } => Ok(Self::Line {
1664 text,
1665 truncated,
1666 at_ms,
1667 }),
1668 _ => unreachable!(),
1669 },
1670 "process_start" => {
1671 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1672 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1673 _ => unreachable!(),
1674 }
1675 }
1676 _ => Ok(Self::Unknown { tag, body: value }),
1677 }
1678 }
1679}
1680
1681fn is_zero_u64(value: &u64) -> bool {
1682 *value == 0
1683}
1684
1685fn default_true() -> bool {
1686 true
1687}
1688
1689#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1691pub struct TerminalHistory {
1692 pub daemon_started_at_ms: u64,
1694 pub entries: Vec<TerminalEntry>,
1695 #[serde(default, skip_serializing_if = "is_zero_u64")]
1698 pub dropped: u64,
1699 #[serde(default, skip_serializing_if = "is_zero_u64")]
1702 pub journal_skipped_lines: u64,
1703 #[serde(default, skip_serializing_if = "is_zero_u64")]
1705 pub journal_read_errors: u64,
1706 #[serde(default, skip_serializing_if = "is_zero_u64")]
1708 pub journal_write_failures: u64,
1709}
1710
1711#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1713pub struct TerminalEntry {
1714 #[serde(default, skip_serializing_if = "Option::is_none")]
1717 pub daemon_incarnation: Option<String>,
1718 #[serde(default, skip_serializing_if = "Option::is_none")]
1719 pub exit_code: Option<i32>,
1720 #[serde(default, skip_serializing_if = "Option::is_none")]
1721 pub exit_signal: Option<i32>,
1722 pub at_ms: u64,
1723 pub disposition: TerminalDisposition,
1724 #[serde(default, skip_serializing_if = "Option::is_none")]
1728 pub exit_kind: Option<TerminalExitKind>,
1729 #[serde(default, skip_serializing_if = "Option::is_none")]
1736 pub disposition_detail: Option<String>,
1737}
1738
1739#[derive(Debug, Clone, PartialEq, Eq)]
1744pub enum TerminalExitKind {
1745 Clean,
1746 Crash,
1747 DeliberateSeverance,
1748 Unknown(String),
1749}
1750
1751impl TerminalExitKind {
1752 fn wire_name(&self) -> &str {
1753 match self {
1754 Self::Clean => "clean",
1755 Self::Crash => "crash",
1756 Self::DeliberateSeverance => "deliberate_severance",
1757 Self::Unknown(value) => value,
1758 }
1759 }
1760}
1761
1762impl Serialize for TerminalExitKind {
1763 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1764 where
1765 S: serde::Serializer,
1766 {
1767 serializer.serialize_str(self.wire_name())
1768 }
1769}
1770
1771impl<'de> Deserialize<'de> for TerminalExitKind {
1772 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1773 where
1774 D: serde::Deserializer<'de>,
1775 {
1776 let value = String::deserialize(deserializer)?;
1777 Ok(match value.as_str() {
1778 "clean" => Self::Clean,
1779 "crash" => Self::Crash,
1780 "deliberate_severance" => Self::DeliberateSeverance,
1781 _ => Self::Unknown(value),
1782 })
1783 }
1784}
1785
1786open_string_enum! {
1787 TerminalDisposition {
1789 Stopped => "stopped",
1790 Disabled => "disabled",
1791 Failed => "failed",
1792 Restarting => "restarting",
1793 DaemonShutdown => "daemon_shutdown",
1798 }
1799}
1800
1801#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1802#[serde(rename_all = "snake_case")]
1803pub enum PollKind {
1804 Status,
1805 Liveness,
1806}
1807
1808#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1820#[non_exhaustive]
1821pub struct CatalogEntry {
1822 pub module_id: String,
1823 #[serde(default = "default_true")]
1834 pub ready: bool,
1835 #[serde(default, skip_serializing_if = "Option::is_none")]
1839 pub not_ready: Option<NotReadyReason>,
1840 #[serde(default, skip_serializing_if = "Option::is_none")]
1861 pub module_version: Option<String>,
1862 pub roles: Vec<ProviderRole>,
1863 pub control_ops: Vec<String>,
1864 #[serde(default, skip_serializing_if = "Option::is_none")]
1869 pub capabilities: Option<CapabilityDeclarations>,
1870 #[serde(default, skip_serializing_if = "Option::is_none")]
1873 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1874 #[serde(default, skip_serializing_if = "Option::is_none")]
1878 pub events: Option<Vec<EventDeclaration>>,
1879}
1880
1881impl CatalogEntry {
1882 #[must_use]
1884 pub fn new(
1885 module_id: impl Into<String>,
1886 roles: Vec<ProviderRole>,
1887 control_ops: Vec<String>,
1888 ) -> Self {
1889 Self {
1890 module_id: module_id.into(),
1891 ready: default_true(),
1892 not_ready: None,
1893 module_version: None,
1894 roles,
1895 control_ops,
1896 capabilities: None,
1897 self_signals: None,
1898 events: None,
1899 }
1900 }
1901
1902 #[must_use]
1904 pub fn with_ready(mut self, ready: bool) -> Self {
1905 self.ready = ready;
1906 self
1907 }
1908
1909 #[must_use]
1911 pub fn with_not_ready(mut self, not_ready: Option<NotReadyReason>) -> Self {
1912 self.not_ready = not_ready;
1913 self
1914 }
1915
1916 #[must_use]
1918 pub fn with_module_version(mut self, module_version: Option<String>) -> Self {
1919 self.module_version = module_version;
1920 self
1921 }
1922
1923 #[must_use]
1925 pub fn with_capabilities(mut self, capabilities: Option<CapabilityDeclarations>) -> Self {
1926 self.capabilities = capabilities;
1927 self
1928 }
1929
1930 #[must_use]
1932 pub fn with_self_signals(mut self, self_signals: Option<Vec<SelfSignalDeclaration>>) -> Self {
1933 self.self_signals = self_signals;
1934 self
1935 }
1936
1937 #[must_use]
1939 pub fn with_events(mut self, events: Option<Vec<EventDeclaration>>) -> Self {
1940 self.events = events;
1941 self
1942 }
1943}
1944
1945#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1947pub struct NotReadyReason {
1948 pub reason: String,
1953 #[serde(default, skip_serializing_if = "Option::is_none")]
1956 pub capability: Option<String>,
1957}
1958
1959impl NotReadyReason {
1960 pub const DECLARED_NOT_READY: &'static str = "declared_not_ready";
1961 pub const REQUIRED_CAPABILITY_UNPROVIDED: &'static str = "required_capability_unprovided";
1962}
1963
1964#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1965pub struct CapabilityRequirementStatus {
1966 pub consumer: String,
1967 pub capability: String,
1968 pub need: String,
1969 pub verdict: String,
1970 pub episode_seq: u64,
1971 pub config_satisfiable: bool,
1972 pub runtime_available: bool,
1973 pub detail: String,
1974}
1975
1976#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1977pub struct SupervisorRescanResult {
1978 pub added: Vec<String>,
1979 pub removed: Vec<String>,
1980 pub changed_pending_reload: Vec<String>,
1981 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1994 pub enabled_changes: Vec<String>,
1995 pub unchanged: u32,
1996 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2004 pub preview: bool,
2005 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2023 pub restart_required: Vec<String>,
2024 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2028 pub capability_warnings: Vec<String>,
2029}
2030
2031#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
2047#[serde(rename_all = "snake_case")]
2048pub enum ModuleProtocol {
2049 #[default]
2053 Subc,
2054 None,
2063}
2064
2065#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2077#[non_exhaustive]
2078pub struct SupervisorEntry {
2079 pub module_id: String,
2080 pub state: String,
2081 pub enabled: bool,
2082 pub live: bool,
2092 #[serde(default)]
2096 pub protocol: ModuleProtocol,
2097 #[serde(default, skip_serializing_if = "Option::is_none")]
2100 pub launch_nonce_env: Option<bool>,
2101 #[serde(default, skip_serializing_if = "Option::is_none")]
2107 pub launch_nonce_source: Option<String>,
2108 pub health: SupervisorHealthStatus,
2109 #[serde(default, skip_serializing_if = "Option::is_none")]
2112 pub pending_reload: Option<PendingReloadVerdict>,
2113 #[serde(default)]
2119 pub last_probe_ms: Option<u64>,
2120 #[serde(default, skip_serializing_if = "Option::is_none")]
2124 pub last_exit_code: Option<i32>,
2125 #[serde(default, skip_serializing_if = "Option::is_none")]
2129 pub last_exit_signal: Option<i32>,
2130 #[serde(default, skip_serializing_if = "Option::is_none")]
2134 pub last_exit_ms: Option<u64>,
2135 #[serde(default, skip_serializing_if = "Option::is_none")]
2138 pub last_exit_kind: Option<TerminalExitKind>,
2139 #[serde(default, skip_serializing_if = "Option::is_none")]
2156 pub restart_count: Option<u32>,
2157 #[serde(default, skip_serializing_if = "Option::is_none")]
2160 pub max_restarts: Option<u32>,
2161 #[serde(default, skip_serializing_if = "Option::is_none")]
2164 pub lifetime_restarts: Option<u32>,
2165 #[serde(default, skip_serializing_if = "Option::is_none")]
2169 pub spawn_generation: Option<u64>,
2170 #[serde(default, skip_serializing_if = "Option::is_none")]
2180 pub restart_window_secs: Option<u64>,
2181 #[serde(default, skip_serializing_if = "Option::is_none")]
2185 pub drain_timeout_ms: Option<u64>,
2186 #[serde(default, skip_serializing_if = "Option::is_none")]
2189 pub restart_backoff_ms: Option<u64>,
2190 #[serde(default, skip_serializing_if = "Option::is_none")]
2193 pub restart_max_backoff_ms: Option<u64>,
2194 #[serde(default, skip_serializing_if = "Option::is_none")]
2207 pub resources: Option<ChildResourceUsage>,
2208}
2209
2210impl SupervisorEntry {
2211 #[must_use]
2213 pub fn new(
2214 module_id: impl Into<String>,
2215 state: impl Into<String>,
2216 enabled: bool,
2217 live: bool,
2218 health: SupervisorHealthStatus,
2219 ) -> Self {
2220 Self {
2221 module_id: module_id.into(),
2222 state: state.into(),
2223 enabled,
2224 live,
2225 protocol: ModuleProtocol::default(),
2226 launch_nonce_env: None,
2227 launch_nonce_source: None,
2228 health,
2229 pending_reload: None,
2230 last_probe_ms: None,
2231 last_exit_code: None,
2232 last_exit_signal: None,
2233 last_exit_ms: None,
2234 last_exit_kind: None,
2235 restart_count: None,
2236 max_restarts: None,
2237 lifetime_restarts: None,
2238 spawn_generation: None,
2239 restart_window_secs: None,
2240 drain_timeout_ms: None,
2241 restart_backoff_ms: None,
2242 restart_max_backoff_ms: None,
2243 resources: None,
2244 }
2245 }
2246
2247 #[must_use]
2249 pub fn with_protocol(mut self, protocol: ModuleProtocol) -> Self {
2250 self.protocol = protocol;
2251 self
2252 }
2253
2254 #[must_use]
2256 pub fn with_launch_nonce_env(mut self, launch_nonce_env: Option<bool>) -> Self {
2257 self.launch_nonce_env = launch_nonce_env;
2258 self
2259 }
2260
2261 pub fn with_launch_nonce_source(mut self, source: Option<String>) -> Self {
2262 self.launch_nonce_source = source;
2263 self
2264 }
2265
2266 #[must_use]
2268 pub fn with_pending_reload(mut self, pending_reload: Option<PendingReloadVerdict>) -> Self {
2269 self.pending_reload = pending_reload;
2270 self
2271 }
2272
2273 #[must_use]
2275 pub fn with_last_probe_ms(mut self, last_probe_ms: Option<u64>) -> Self {
2276 self.last_probe_ms = last_probe_ms;
2277 self
2278 }
2279
2280 #[must_use]
2282 pub fn with_last_exit_code(mut self, last_exit_code: Option<i32>) -> Self {
2283 self.last_exit_code = last_exit_code;
2284 self
2285 }
2286
2287 #[must_use]
2289 pub fn with_last_exit_signal(mut self, last_exit_signal: Option<i32>) -> Self {
2290 self.last_exit_signal = last_exit_signal;
2291 self
2292 }
2293
2294 #[must_use]
2296 pub fn with_last_exit_ms(mut self, last_exit_ms: Option<u64>) -> Self {
2297 self.last_exit_ms = last_exit_ms;
2298 self
2299 }
2300
2301 #[must_use]
2303 pub fn with_last_exit_kind(mut self, last_exit_kind: Option<TerminalExitKind>) -> Self {
2304 self.last_exit_kind = last_exit_kind;
2305 self
2306 }
2307
2308 #[must_use]
2310 pub fn with_restart_count(mut self, restart_count: Option<u32>) -> Self {
2311 self.restart_count = restart_count;
2312 self
2313 }
2314
2315 #[must_use]
2317 pub fn with_max_restarts(mut self, max_restarts: Option<u32>) -> Self {
2318 self.max_restarts = max_restarts;
2319 self
2320 }
2321
2322 #[must_use]
2324 pub fn with_lifetime_restarts(mut self, lifetime_restarts: Option<u32>) -> Self {
2325 self.lifetime_restarts = lifetime_restarts;
2326 self
2327 }
2328
2329 #[must_use]
2331 pub fn with_spawn_generation(mut self, spawn_generation: Option<u64>) -> Self {
2332 self.spawn_generation = spawn_generation;
2333 self
2334 }
2335
2336 #[must_use]
2338 pub fn with_restart_window_secs(mut self, restart_window_secs: Option<u64>) -> Self {
2339 self.restart_window_secs = restart_window_secs;
2340 self
2341 }
2342
2343 #[must_use]
2345 pub fn with_drain_timeout_ms(mut self, drain_timeout_ms: Option<u64>) -> Self {
2346 self.drain_timeout_ms = drain_timeout_ms;
2347 self
2348 }
2349
2350 #[must_use]
2352 pub fn with_restart_backoff_ms(mut self, restart_backoff_ms: Option<u64>) -> Self {
2353 self.restart_backoff_ms = restart_backoff_ms;
2354 self
2355 }
2356
2357 #[must_use]
2359 pub fn with_restart_max_backoff_ms(mut self, restart_max_backoff_ms: Option<u64>) -> Self {
2360 self.restart_max_backoff_ms = restart_max_backoff_ms;
2361 self
2362 }
2363
2364 #[must_use]
2366 pub fn with_resources(mut self, resources: Option<ChildResourceUsage>) -> Self {
2367 self.resources = resources;
2368 self
2369 }
2370}
2371
2372#[derive(Debug, Clone, PartialEq)]
2375pub enum ChildResourceUsage {
2376 Measured(ChildResourceReading),
2377 Unavailable {
2378 reason: ChildResourceUnavailableReason,
2379 },
2380 Unknown {
2383 tag: String,
2384 body: OrderedJsonObject,
2385 },
2386}
2387
2388#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2390pub struct ChildResourceReading {
2391 pub memory_bytes: u64,
2394 pub memory_kind: ChildMemoryKind,
2395 #[serde(default, skip_serializing_if = "Option::is_none")]
2398 pub swap_bytes: Option<u64>,
2399 pub cpu_user_ms: u64,
2403 pub cpu_system_ms: u64,
2406}
2407
2408open_string_enum! {
2409 ChildMemoryKind {
2411 PhysFootprint => "phys_footprint",
2415 ResidentSet => "resident_set",
2418 WindowsWorkingSet => "windows_working_set",
2421 }
2422}
2423
2424open_string_enum! {
2425 ChildResourceUnavailableReason {
2427 NotRunning => "not_running",
2429 UnsupportedPlatform => "unsupported_platform",
2431 Unreadable => "unreadable",
2434 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
2437 }
2438}
2439
2440#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2441#[serde(rename_all = "snake_case")]
2442pub enum SupervisorHealthStatus {
2443 Ok,
2444 Degraded,
2445 Failing,
2446 Unresponsive,
2447 Unknown,
2448}
2449
2450#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2451pub struct SupervisorHealthEntry {
2452 pub module_id: String,
2453 pub status: SupervisorHealthStatus,
2454 #[serde(default, skip_serializing_if = "Option::is_none")]
2460 pub detail: Option<String>,
2461 #[serde(default, skip_serializing_if = "Option::is_none")]
2466 pub metrics: Option<serde_json::Value>,
2467 pub consecutive_failures: u32,
2468 #[serde(default)]
2471 pub late_answer_count: u64,
2472 #[serde(default, skip_serializing_if = "Option::is_none")]
2474 pub last_late_answer_latency_ms: Option<u64>,
2475 #[serde(default)]
2480 pub last_action: Option<String>,
2481 #[serde(default)]
2484 pub last_action_ms: Option<u64>,
2485 #[serde(default, skip_serializing_if = "Option::is_none")]
2498 pub last_probe_ms: Option<u64>,
2499}
2500
2501#[cfg(test)]
2502mod tests {
2503 #[test]
2504 fn windows_running_image_evidence_round_trips_all_sixteen_id_bytes() {
2505 let evidence = super::RunningImageEvidence::WindowsSpawnFileId {
2506 volume_serial_number: u64::MAX,
2507 file_id: [
2508 0, 1, 2, 3, 4, 5, 6, 7, 128, 129, 130, 131, 252, 253, 254, 255,
2509 ],
2510 };
2511 let agreement = super::RunningImageAgreement::Match { evidence };
2512 let encoded = serde_json::to_string(&agreement).unwrap();
2513 assert!(encoded.contains("windows_spawn_file_id"));
2514 assert_eq!(
2515 serde_json::from_str::<super::RunningImageAgreement>(&encoded).unwrap(),
2516 agreement
2517 );
2518 }
2519
2520 #[test]
2521 fn unix_running_image_evidence_keeps_original_wire_bytes() {
2522 assert_eq!(
2523 serde_json::to_string(&super::RunningImageEvidence::MacosSpawnInode {
2524 device: 7,
2525 inode: 11
2526 })
2527 .unwrap(),
2528 "{\"method\":\"macos_spawn_inode\",\"device\":7,\"inode\":11}"
2529 );
2530 assert_eq!(
2531 serde_json::to_string(&super::RunningImageEvidence::LinuxProcSha256 {
2532 digest: "abc".to_owned()
2533 })
2534 .unwrap(),
2535 "{\"method\":\"linux_proc_sha256\",\"digest\":\"abc\"}"
2536 );
2537 }
2538 use super::*;
2539 use subc_protocol::{BindIdentity, RouteTarget};
2540
2541 #[test]
2542 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
2543 let entry = TerminalEntry {
2544 daemon_incarnation: Some("daemon-before-restart".into()),
2545 exit_code: Some(1),
2546 exit_signal: None,
2547 at_ms: 1_700_000_000_123,
2548 disposition: TerminalDisposition::Restarting,
2549 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
2550 disposition_detail: None,
2551 };
2552 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
2553 assert_eq!(
2554 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
2555 ["exit_kind"],
2556 "deliberate_severance"
2557 );
2558
2559 #[derive(serde::Deserialize)]
2560 struct LegacyTerminalEntry {
2561 exit_code: Option<i32>,
2562 exit_signal: Option<i32>,
2563 at_ms: u64,
2564 disposition: TerminalDisposition,
2565 }
2566
2567 let decoded: LegacyTerminalEntry =
2568 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
2569 assert_eq!(decoded.exit_code, Some(1));
2570 assert_eq!(decoded.exit_signal, None);
2571 assert_eq!(decoded.at_ms, 1_700_000_000_123);
2572 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
2573
2574 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
2575 let future: TerminalEntry =
2576 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
2577 assert_eq!(
2578 future.exit_kind,
2579 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
2580 );
2581 }
2582
2583 #[test]
2584 fn terminal_incarnation_is_optional_for_older_daemons() {
2585 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
2586 "at_ms": 123,
2587 "disposition": "stopped"
2588 }))
2589 .unwrap();
2590 let encoded = serde_json::to_value(&entry).unwrap();
2591 assert_eq!(
2592 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
2593 (None, None)
2594 );
2595 }
2596
2597 #[test]
2598 fn route_poll_uses_kind_field() {
2599 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
2600 route_channel: 7,
2601 route_epoch: 11,
2602 kind: PollKind::Status,
2603 })
2604 .unwrap();
2605
2606 assert_eq!(body["op"], "route.poll");
2607 assert_eq!(body["route_epoch"], 11);
2608 assert_eq!(body["kind"], "status");
2609 assert!(body.get("op").is_some());
2610 }
2611
2612 #[test]
2613 fn route_open_is_internally_tagged() {
2614 let request = ClientControlRequest::RouteOpen {
2615 target: RouteTarget::ToolProvider {
2616 module_id: "aft".to_string(),
2617 },
2618 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
2619 consumer_identity: None,
2620 consumer_capabilities: None,
2621 role_versions: None,
2622 admission_facts: None,
2623 scope: None,
2624 };
2625
2626 let body = serde_json::to_value(request).unwrap();
2627 assert_eq!(body["op"], "route.open");
2628 assert_eq!(body["target"]["kind"], "tool_provider");
2629 assert!(body.get("consumer_identity").is_none());
2630 assert!(body.get("consumer_capabilities").is_none());
2631 assert!(body.get("role_versions").is_none());
2632 }
2633
2634 #[test]
2635 fn route_open_without_optional_fields_still_decodes() {
2636 let body = serde_json::json!({
2637 "op": "route.open",
2638 "target": { "kind": "tool_provider", "module_id": "aft" },
2639 "identity": {
2640 "project_root": "/tmp/project",
2641 "harness": "opencode",
2642 "session": "session-1"
2643 }
2644 });
2645
2646 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
2647 let ClientControlRequest::RouteOpen {
2648 consumer_identity,
2649 consumer_capabilities,
2650 role_versions,
2651 admission_facts,
2652 ..
2653 } = decoded
2654 else {
2655 panic!("decoded wrong request variant");
2656 };
2657 assert_eq!(consumer_identity, None);
2658 assert_eq!(consumer_capabilities, None);
2659 assert_eq!(role_versions, None);
2660 assert_eq!(admission_facts, None);
2661 }
2662
2663 #[test]
2664 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
2665 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
2666 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
2667 match decoded {
2668 ClientControlPush::RouteClosed {
2669 excluded_subscriptions,
2670 terminal,
2671 ..
2672 } => {
2673 assert_eq!(excluded_subscriptions, 0);
2674 assert_eq!(terminal, None);
2675 }
2676 other => panic!("unexpected push: {other:?}"),
2677 }
2678 assert!(!serde_json::to_string(&decoded)
2679 .unwrap()
2680 .contains("terminal"));
2681 }
2682
2683 #[test]
2684 fn old_route_closed_decoder_ignores_new_terminal_field() {
2685 #[derive(serde::Deserialize)]
2686 #[serde(tag = "op")]
2687 enum LegacyClientControlPush {
2688 #[serde(rename = "route.closed")]
2689 RouteClosed {
2690 module_id: String,
2691 reason: RouteCloseReason,
2692 drained: bool,
2693 abandoned: u32,
2694 },
2695 }
2696
2697 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
2698 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
2699 match decoded {
2700 LegacyClientControlPush::RouteClosed {
2701 module_id,
2702 reason,
2703 drained,
2704 abandoned,
2705 } => {
2706 assert_eq!(module_id, "aft-tools");
2707 assert_eq!(reason, RouteCloseReason::Crash);
2708 assert!(!drained);
2709 assert_eq!(abandoned, 0);
2710 }
2711 }
2712 }
2713
2714 #[test]
2715 fn supervisor_routes_is_a_control_plane_request() {
2716 let body = serde_json::json!({
2717 "op": "supervisor.routes",
2718 "module_id": "aft"
2719 });
2720
2721 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
2722 assert_eq!(serde_json::to_value(request).unwrap(), body);
2723 }
2724
2725 #[test]
2726 fn diagnostic_string_enums_retain_unknown_wire_values() {
2727 let reason: RunningImageUnavailableReason =
2728 serde_json::from_str("\"future_reason\"").unwrap();
2729 let disposition: TerminalDisposition =
2730 serde_json::from_str("\"future_disposition\"").unwrap();
2731
2732 assert_eq!(
2733 reason,
2734 RunningImageUnavailableReason::Unknown("future_reason".to_string())
2735 );
2736 assert_eq!(
2737 disposition,
2738 TerminalDisposition::Unknown("future_disposition".to_string())
2739 );
2740 }
2741
2742 #[test]
2743 fn diagnostic_string_enums_preserve_existing_wire_names() {
2744 let names = [
2745 (RunningImageUnavailableReason::NotRunning, "not_running"),
2746 (
2747 RunningImageUnavailableReason::UnsupportedPlatform,
2748 "unsupported_platform",
2749 ),
2750 (
2751 RunningImageUnavailableReason::RunningExecutableUnreadable,
2752 "running_executable_unreadable",
2753 ),
2754 (
2755 RunningImageUnavailableReason::SpawnedPathUnreadable,
2756 "spawned_path_unreadable",
2757 ),
2758 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2759 (
2760 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2761 "process_identity_unconfirmed",
2762 ),
2763 ];
2764 for (value, expected) in names {
2765 let wire = serde_json::to_string(&value).unwrap();
2766 assert_eq!(wire, format!("\"{expected}\""));
2767 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2768 assert_eq!(decoded, value);
2769 }
2770
2771 for (value, expected) in [
2772 (TerminalDisposition::Stopped, "stopped"),
2773 (TerminalDisposition::Disabled, "disabled"),
2774 (TerminalDisposition::Failed, "failed"),
2775 (TerminalDisposition::Restarting, "restarting"),
2776 (TerminalDisposition::DaemonShutdown, "daemon_shutdown"),
2777 ] {
2778 let wire = serde_json::to_string(&value).unwrap();
2779 assert_eq!(wire, format!("\"{expected}\""));
2780 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2781 assert_eq!(decoded, value);
2782 }
2783 }
2784
2785 #[test]
2786 fn diagnostic_string_enums_reject_non_string_bodies() {
2787 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2788 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2789 }
2790
2791 #[test]
2792 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2793 let body = serde_json::json!({
2794 "op": "supervisor.provenance",
2795 "daemon": {
2796 "daemon_build": {},
2797 "daemon_observed": {
2798 "running_image": {
2799 "status": "unavailable",
2800 "reason": "not_running"
2801 }
2802 }
2803 },
2804 "modules": [
2805 {
2806 "module_id": "future",
2807 "module_declared": { "status": "unverifiable" },
2808 "daemon_observed": {
2809 "running_image": {
2810 "status": "unavailable",
2811 "reason": "future_reason"
2812 }
2813 }
2814 },
2815 {
2816 "module_id": "healthy-a",
2817 "module_declared": { "status": "unverifiable" },
2818 "daemon_observed": {
2819 "running_image": {
2820 "status": "match",
2821 "evidence": {
2822 "method": "linux_proc_sha256",
2823 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2824 }
2825 }
2826 }
2827 },
2828 {
2829 "module_id": "healthy-b",
2830 "module_declared": { "status": "unverifiable" },
2831 "daemon_observed": {
2832 "running_image": {
2833 "status": "unavailable",
2834 "reason": "unsupported_platform"
2835 }
2836 }
2837 }
2838 ]
2839 });
2840
2841 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2842 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2843 panic!("decoded wrong response variant");
2844 };
2845 assert_eq!(modules.len(), 3);
2846 assert_eq!(modules[0].module_id, "future");
2847 assert_eq!(
2848 modules[0].daemon_observed.running_image,
2849 RunningImageAgreement::Unavailable {
2850 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2851 }
2852 );
2853 assert_eq!(modules[1].module_id, "healthy-a");
2854 assert_eq!(modules[2].module_id, "healthy-b");
2855 }
2856
2857 #[test]
2858 fn tagged_unknown_values_retain_tag_and_body() {
2859 macro_rules! assert_unknown_round_trip {
2860 ($ty:ident, $field:literal, $value:expr) => {
2861 let value = $value;
2862 let wire = serde_json::to_string(&value).unwrap();
2863 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2864 match decoded {
2865 $ty::Unknown { tag, body } => {
2866 assert_eq!(tag, value[$field].as_str().unwrap());
2867 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2868 }
2869 _ => panic!("decoded known variant"),
2870 }
2871 };
2872 }
2873
2874 assert_unknown_round_trip!(
2875 ModuleDeclaredProvenance,
2876 "status",
2877 serde_json::json!({"status": "future", "build": {"version": 7}})
2878 );
2879 assert_unknown_round_trip!(
2880 RunningImageAgreement,
2881 "status",
2882 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2883 );
2884 assert_unknown_round_trip!(
2885 RunningImageEvidence,
2886 "method",
2887 serde_json::json!({"method": "future", "digest": "abc"})
2888 );
2889 assert_unknown_round_trip!(
2890 SupervisorRouteConsumer,
2891 "kind",
2892 serde_json::json!({"kind": "future", "module_id": "m"})
2893 );
2894 assert_unknown_round_trip!(
2895 StderrCaptureState,
2896 "state",
2897 serde_json::json!({"state": "future", "reason": "because"})
2898 );
2899 assert_unknown_round_trip!(
2900 StderrTailEntry,
2901 "kind",
2902 serde_json::json!({"kind": "future", "text": "line"})
2903 );
2904 assert_unknown_round_trip!(
2905 ChildResourceUsage,
2906 "status",
2907 serde_json::json!({"status": "future", "memory_bytes": 1})
2908 );
2909 }
2910
2911 #[test]
2912 fn child_resource_usage_round_trips_both_known_states() {
2913 let measured = ChildResourceUsage::Measured(ChildResourceReading {
2914 memory_bytes: 0,
2915 memory_kind: ChildMemoryKind::ResidentSet,
2916 swap_bytes: Some(0),
2917 cpu_user_ms: 0,
2918 cpu_system_ms: 0,
2919 });
2920 let wire = serde_json::to_value(&measured).unwrap();
2921 assert_eq!(
2922 wire,
2923 serde_json::json!({
2924 "status": "measured",
2925 "memory_bytes": 0,
2926 "memory_kind": "resident_set",
2927 "swap_bytes": 0,
2928 "cpu_user_ms": 0,
2929 "cpu_system_ms": 0
2930 })
2931 );
2932 assert_eq!(
2933 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2934 measured
2935 );
2936
2937 let unavailable = ChildResourceUsage::Unavailable {
2938 reason: ChildResourceUnavailableReason::NotRunning,
2939 };
2940 let wire = serde_json::to_value(&unavailable).unwrap();
2941 assert_eq!(
2942 wire,
2943 serde_json::json!({"status": "unavailable", "reason": "not_running"})
2944 );
2945 assert_eq!(
2946 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2947 unavailable
2948 );
2949 }
2950
2951 #[test]
2952 fn a_stderr_line_decodes_with_and_without_its_capture_time() {
2953 let stamped: StderrTailEntry = serde_json::from_str(
2957 r#"{"kind":"line","text":"boom","truncated":true,"at_ms":1789801440685}"#,
2958 )
2959 .unwrap();
2960 assert_eq!(
2961 stamped,
2962 StderrTailEntry::Line {
2963 text: "boom".to_string(),
2964 truncated: true,
2965 at_ms: Some(1_789_801_440_685),
2966 }
2967 );
2968 let unstamped: StderrTailEntry =
2969 serde_json::from_str(r#"{"kind":"line","text":"boom"}"#).unwrap();
2970 assert_eq!(
2971 unstamped,
2972 StderrTailEntry::Line {
2973 text: "boom".to_string(),
2974 truncated: false,
2975 at_ms: None,
2976 }
2977 );
2978 assert_eq!(
2981 serde_json::to_string(&unstamped).unwrap(),
2982 r#"{"kind":"line","text":"boom"}"#
2983 );
2984 assert_eq!(
2985 serde_json::to_value(&stamped).unwrap()["at_ms"],
2986 serde_json::json!(1_789_801_440_685u64)
2987 );
2988 }
2989
2990 #[test]
2991 fn tagged_unknown_values_round_trip_the_original_json() {
2992 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2993 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2994 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2995 }
2996
2997 #[test]
2998 fn tagged_unknown_values_round_trip_trailing_tag() {
2999 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
3000 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
3001 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
3002
3003 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
3004 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
3005 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
3006 }
3007
3008 #[test]
3009 fn tagged_unknown_values_round_trip_middle_tag() {
3010 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
3011 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
3012 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
3013
3014 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
3015 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
3016 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
3017 }
3018
3019 #[test]
3020 fn tagged_unknown_values_round_trip_deep_payload() {
3021 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
3022 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
3023 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
3024
3025 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
3026 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
3027 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
3028 }
3029
3030 #[test]
3031 fn tagged_unknown_values_reject_non_object_bodies() {
3032 for wire in ["42", r#""future""#, "[]"] {
3033 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
3034 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
3035 }
3036 }
3037
3038 #[test]
3039 fn duplicate_discriminators_reject_without_panicking() {
3040 assert_eq!(
3041 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
3042 .unwrap(),
3043 ModuleDeclaredProvenance::Unverifiable
3044 );
3045 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
3046 .unwrap()
3047 {
3048 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
3049 _ => panic!("future discriminator decoded as a known variant"),
3050 }
3051
3052 let wires = [
3053 r#"{"status":"reported","status":"unverifiable"}"#,
3054 r#"{"status":"unverifiable","status":"reported"}"#,
3055 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
3056 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
3057 ];
3058
3059 for wire in wires {
3060 let result =
3061 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
3062 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
3063 assert!(
3064 result.unwrap().is_err(),
3065 "duplicate discriminator decoded: {wire}"
3066 );
3067 }
3068
3069 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
3070 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
3071 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
3072 assert!(
3073 result.unwrap().is_err(),
3074 "duplicate discriminator decoded: {wire}"
3075 );
3076 }
3077
3078 #[test]
3079 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
3080 let known_wire =
3081 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
3082 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
3083 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
3084
3085 for wire in [
3086 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
3087 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
3088 ] {
3089 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
3090 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
3091 }
3092
3093 for wire in [
3094 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
3095 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
3096 ] {
3097 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
3098 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
3099 }
3100
3101 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
3102 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
3103 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
3104
3105 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
3106 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
3107 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
3108 }
3109
3110 #[test]
3111 fn tagged_unknown_member_does_not_discard_known_siblings() {
3112 let body = serde_json::json!({
3113 "modules": [{
3114 "module_id": "target",
3115 "routes": [
3116 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
3117 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
3118 ]
3119 }]
3120 });
3121 let decoded: ClientControlResponse = serde_json::from_value(
3122 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
3123 )
3124 .unwrap();
3125 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
3126 panic!("decoded wrong response variant");
3127 };
3128 assert_eq!(modules[0].routes.len(), 2);
3129 assert_eq!(
3130 modules[0].routes[1].consumer,
3131 SupervisorRouteConsumer::Direct { connection_id: 7 }
3132 );
3133 }
3134}
3135
3136#[cfg(test)]
3137mod launch_nonce_redaction_tests {
3138 use super::*;
3139
3140 const NONCE: &str = "nonce-f00dfeed1234abcd";
3141
3142 fn identity() -> ConsumerIdentity {
3143 ConsumerIdentity {
3144 module_id: "wernicke".to_string(),
3145 launch_nonce: NONCE.to_string(),
3146 }
3147 }
3148
3149 #[test]
3150 fn consumer_identity_debug_names_the_module_and_never_the_nonce() {
3151 let printed = format!("{:?}", identity());
3152 assert!(printed.contains("wernicke"), "{printed}");
3153 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
3154 }
3155
3156 #[test]
3157 fn route_open_request_debug_never_prints_the_nonce() {
3158 let request = ClientControlRequest::RouteOpen {
3159 target: subc_protocol::RouteTarget::ToolProvider {
3160 module_id: "broca".to_string(),
3161 },
3162 identity: subc_protocol::BindIdentity::new(
3163 PathBuf::from("/tmp/project"),
3164 "test".to_string(),
3165 "session".to_string(),
3166 ),
3167 consumer_identity: Some(identity()),
3168 consumer_capabilities: None,
3169 role_versions: None,
3170 admission_facts: None,
3171 scope: None,
3172 };
3173 let printed = format!("{request:?}");
3174 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
3175 }
3176}