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