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