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")]
245 drain_timeout_ms: Option<u64>,
246 },
247 #[serde(rename = "supervisor.swap")]
262 SupervisorSwap {
263 module_id: String,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
267 ready_timeout_ms: Option<u64>,
268 },
269 #[serde(rename = "supervisor.reload")]
270 SupervisorReload { module_id: String },
271 #[serde(rename = "supervisor.rescan")]
272 SupervisorRescan {
273 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
293 preview: bool,
294 },
295 #[serde(rename = "supervisor.release_reserved")]
299 SupervisorReleaseReserved { module_id: String },
300 #[serde(rename = "supervisor.set_enabled")]
301 SupervisorSetEnabled { module_id: String, enabled: bool },
302 #[serde(rename = "supervisor.health_probe")]
303 SupervisorHealthProbe { module_id: String },
304 #[serde(rename = "supervisor.health")]
305 SupervisorHealth {},
306 #[serde(rename = "supervisor.routes")]
319 SupervisorRoutes {
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 module_id: Option<String>,
322 },
323 #[serde(rename = "supervisor.provenance")]
326 SupervisorProvenance {
327 #[serde(default, skip_serializing_if = "Option::is_none")]
328 module_id: Option<String>,
329 },
330 #[serde(rename = "supervisor.stderr_tail")]
338 SupervisorStderrTail {
339 module_id: String,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 max_lines: Option<u32>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 max_bytes: Option<u32>,
344 },
345 #[serde(rename = "supervisor.terminals")]
358 SupervisorTerminals { module_id: String },
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
363#[serde(tag = "op")]
364pub enum ClientControlResponse {
365 #[serde(rename = "server.describe")]
366 ServerDescribe {
367 protocol_ver: u8,
368 subc_ops: Vec<String>,
369 capabilities: Vec<String>,
370 connected_clients: u64,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
372 counters: Option<serde_json::Value>,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
380 build_git_sha: Option<String>,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
387 build_lock_digest: Option<String>,
388 #[serde(default, skip_serializing_if = "Vec::is_empty")]
392 capability_requirements: Vec<CapabilityRequirementStatus>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
398 machine_id: Option<String>,
399 },
400 #[serde(rename = "catalog.list")]
401 CatalogList {
402 generation: u64,
403 modules: Vec<CatalogEntry>,
404 subc_ops: Vec<String>,
405 },
406 #[serde(rename = "route.open")]
407 RouteOpen {
408 route_channel: u16,
409 route_epoch: u32,
410 },
411 #[serde(rename = "route.poll")]
412 RoutePoll {
413 route_channel: u16,
414 route_epoch: u32,
415 status: Option<String>,
416 live: Option<bool>,
417 },
418 #[serde(rename = "supervisor.list")]
419 SupervisorList {
420 generation: u64,
421 modules: Vec<SupervisorEntry>,
422 },
423 #[serde(rename = "supervisor.spawn_snapshot")]
424 SupervisorSpawnSnapshot {
425 #[serde(flatten)]
426 snapshot: SpawnSnapshot,
427 },
428 #[serde(rename = "supervisor.ack")]
429 SupervisorAck { module_id: String, applied: bool },
430 #[serde(rename = "supervisor.rescan")]
431 SupervisorRescan {
432 #[serde(flatten)]
433 result: SupervisorRescanResult,
434 },
435 #[serde(rename = "supervisor.health_probe")]
436 SupervisorHealthProbe {
437 module_id: String,
438 status: HealthStatus,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
440 detail: Option<String>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
442 metrics: Option<serde_json::Value>,
443 },
444 #[serde(rename = "supervisor.health")]
445 SupervisorHealth {
446 generation: u64,
447 modules: Vec<SupervisorHealthEntry>,
448 },
449 #[serde(rename = "supervisor.routes")]
450 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
451 #[serde(rename = "supervisor.provenance")]
452 SupervisorProvenance {
453 daemon: SupervisorDaemonProvenance,
454 modules: Vec<SupervisorModuleProvenance>,
455 },
456 #[serde(rename = "supervisor.stderr_tail")]
457 SupervisorStderrTail {
458 module_id: String,
459 #[serde(flatten)]
460 tail: StderrTail,
461 },
462 #[serde(rename = "supervisor.terminals")]
463 SupervisorTerminals {
464 module_id: String,
465 #[serde(flatten)]
466 terminals: TerminalHistory,
467 },
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
475#[serde(tag = "op")]
476pub enum ClientControlPush {
477 #[serde(rename = "route.closing")]
478 RouteClosing {
479 module_id: String,
480 #[serde(default)]
483 channels: Vec<u16>,
484 reason: RouteCloseReason,
485 },
486 #[serde(rename = "route.closed")]
487 RouteClosed {
488 module_id: String,
489 #[serde(default)]
492 channels: Vec<u16>,
493 reason: RouteCloseReason,
494 drained: bool,
496 abandoned: u32,
499 #[serde(default)]
501 excluded_subscriptions: u32,
502 #[serde(default, skip_serializing_if = "Option::is_none")]
508 terminal: Option<bool>,
509 },
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
514pub struct SpawnCursor {
515 pub daemon_incarnation: String,
516 pub seq: u64,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
521pub struct LiveSpawn {
522 pub module_id: String,
523 pub spawn_generation: u64,
524 pub pid: u32,
525 pub spawned_at_ms: u64,
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
530pub struct SpawnSnapshot {
531 pub cursor: SpawnCursor,
532 pub ring_bound: u64,
534 pub live: Vec<LiveSpawn>,
535}
536
537#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
539#[serde(rename_all = "snake_case")]
540pub enum SpawnEventKind {
541 Spawned,
542 Exited,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
551pub struct SpawnEvent {
552 pub cursor: SpawnCursor,
553 pub kind: SpawnEventKind,
554 pub module_id: String,
555 pub spawn_generation: u64,
556 pub pid: u32,
557 #[serde(default, skip_serializing_if = "Option::is_none")]
558 pub exit_code: Option<i32>,
559 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub exit_signal: Option<i32>,
561}
562
563#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
565pub struct StderrTail {
566 pub capture: StderrCaptureState,
567 pub entries: Vec<StderrTailEntry>,
568 #[serde(default, skip_serializing_if = "is_zero_u64")]
577 pub dropped_lines: u64,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
582pub struct SupervisorRouteModule {
583 pub module_id: String,
584 pub routes: Vec<SupervisorRoute>,
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
589pub struct SupervisorRoute {
590 pub consumer: SupervisorRouteConsumer,
591 pub age_ms: u64,
593 pub draining: bool,
596 #[serde(default, skip_serializing_if = "Option::is_none")]
602 pub drain_reason: Option<RouteCloseReason>,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
607pub struct SupervisorModuleProvenance {
608 pub module_id: String,
609 pub module_declared: ModuleDeclaredProvenance,
610 pub daemon_observed: SupervisorObservedProcess,
611}
612
613#[derive(Debug, Clone, PartialEq)]
615pub enum ModuleDeclaredProvenance {
616 Reported {
617 build: ManifestProvenance,
618 },
619 Unverifiable,
620 Unknown {
623 tag: String,
624 body: OrderedJsonObject,
625 },
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
633pub struct SupervisorObservedProcess {
634 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub pid: Option<u32>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub spawned_at_ms: Option<u64>,
638 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub spawned_from: Option<PathBuf>,
640 pub running_image: RunningImageAgreement,
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
646pub struct PendingReloadVerdict {
647 pub path: ReloadPathAgreement,
648 pub image: RunningImageAgreement,
649}
650
651#[derive(Debug, Clone, PartialEq)]
653pub enum ReloadPathAgreement {
654 Match,
655 Mismatch {
656 configured: PathBuf,
657 spawned_from: PathBuf,
658 },
659 Unavailable {
660 reason: ReloadPathUnavailableReason,
661 },
662 Unknown {
663 tag: String,
664 body: OrderedJsonObject,
665 },
666}
667
668open_string_enum! {
669 ReloadPathUnavailableReason {
671 NotRunning => "not_running",
672 SpawnedPathUnavailable => "spawned_path_unavailable",
673 }
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
678pub struct SupervisorDaemonProvenance {
679 pub daemon_build: DaemonBuildProvenance,
680 pub daemon_observed: DaemonObservedProcess,
681}
682
683#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
685pub struct DaemonBuildProvenance {
686 #[serde(default, skip_serializing_if = "Option::is_none")]
687 pub build_git_sha: Option<String>,
688 #[serde(default, skip_serializing_if = "Option::is_none")]
689 pub build_lock_digest: Option<String>,
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
694pub struct DaemonObservedProcess {
695 #[serde(default, skip_serializing_if = "Option::is_none")]
696 pub pid: Option<u32>,
697 #[serde(default, skip_serializing_if = "Option::is_none")]
702 pub started_at_ms: Option<u64>,
703 pub running_image: RunningImageAgreement,
704}
705
706#[derive(Debug, Clone, PartialEq)]
708pub enum RunningImageAgreement {
709 Match {
710 evidence: RunningImageEvidence,
711 },
712 Mismatch {
713 running: RunningImageEvidence,
714 disk: RunningImageEvidence,
715 },
716 Unavailable {
717 reason: RunningImageUnavailableReason,
718 },
719 Unknown {
722 tag: String,
723 body: OrderedJsonObject,
724 },
725}
726
727#[derive(Debug, Clone, PartialEq)]
729pub enum RunningImageEvidence {
730 LinuxProcSha256 {
731 digest: String,
732 },
733 MacosSpawnInode {
734 device: u64,
735 inode: u64,
736 },
737 Unknown {
740 tag: String,
741 body: OrderedJsonObject,
742 },
743}
744
745open_string_enum! {
746 RunningImageUnavailableReason {
748 NotRunning => "not_running",
749 UnsupportedPlatform => "unsupported_platform",
750 RunningExecutableUnreadable => "running_executable_unreadable",
751 SpawnedPathUnreadable => "spawned_path_unreadable",
752 HashFailed => "hash_failed",
753 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
754 }
755}
756
757#[derive(Debug, Clone, PartialEq)]
763pub enum SupervisorRouteConsumer {
764 Reserved {
765 module_id: String,
766 },
767 Direct {
768 connection_id: u64,
769 },
770 Unknown {
773 tag: String,
774 body: OrderedJsonObject,
775 },
776}
777
778#[derive(Debug, Clone, PartialEq)]
785pub enum StderrCaptureState {
786 Captured,
789 Incomplete { reason: String },
791 NotCaptured { reason: String },
793 Unknown {
796 tag: String,
797 body: OrderedJsonObject,
798 },
799}
800
801#[derive(Debug, Clone, PartialEq)]
802pub enum StderrTailEntry {
803 Line {
804 text: String,
805 truncated: bool,
810 at_ms: Option<u64>,
814 },
815 ProcessStart,
820 Unknown {
823 tag: String,
824 body: OrderedJsonObject,
825 },
826}
827
828#[derive(Debug, Serialize, Deserialize)]
829#[serde(tag = "status", rename_all = "snake_case")]
830enum ModuleDeclaredProvenanceWire {
831 Reported { build: ManifestProvenance },
832 Unverifiable,
833}
834
835#[derive(Debug, Serialize, Deserialize)]
836#[serde(tag = "status", rename_all = "snake_case")]
837enum RunningImageAgreementWire {
838 Match {
839 evidence: RunningImageEvidence,
840 },
841 Mismatch {
842 running: RunningImageEvidence,
843 disk: RunningImageEvidence,
844 },
845 Unavailable {
846 reason: RunningImageUnavailableReason,
847 },
848}
849
850#[derive(Debug, Serialize, Deserialize)]
851#[serde(tag = "status", rename_all = "snake_case")]
852enum ReloadPathAgreementWire {
853 Match,
854 Mismatch {
855 configured: PathBuf,
856 spawned_from: PathBuf,
857 },
858 Unavailable {
859 reason: ReloadPathUnavailableReason,
860 },
861}
862
863#[derive(Debug, Serialize, Deserialize)]
864#[serde(tag = "method", rename_all = "snake_case")]
865enum RunningImageEvidenceWire {
866 LinuxProcSha256 { digest: String },
867 MacosSpawnInode { device: u64, inode: u64 },
868}
869
870#[derive(Debug, Serialize, Deserialize)]
871#[serde(tag = "kind", rename_all = "snake_case")]
872enum SupervisorRouteConsumerWire {
873 Reserved { module_id: String },
874 Direct { connection_id: u64 },
875}
876
877#[derive(Debug, Serialize, Deserialize)]
878#[serde(tag = "state", rename_all = "snake_case")]
879enum StderrCaptureStateWire {
880 Captured,
881 Incomplete { reason: String },
882 NotCaptured { reason: String },
883}
884
885#[derive(Debug, Serialize, Deserialize)]
886#[serde(tag = "status", rename_all = "snake_case")]
887enum ChildResourceUsageWire {
888 Measured(ChildResourceReading),
889 Unavailable {
890 reason: ChildResourceUnavailableReason,
891 },
892}
893
894#[derive(Debug, Serialize, Deserialize)]
895#[serde(tag = "kind", rename_all = "snake_case")]
896enum StderrTailEntryWire {
897 Line {
898 text: String,
899 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
900 truncated: bool,
901 #[serde(default, skip_serializing_if = "Option::is_none")]
904 at_ms: Option<u64>,
905 },
906 ProcessStart,
907}
908
909#[derive(Debug, Clone, PartialEq)]
911pub enum OrderedJsonValue {
912 Null,
913 Bool(bool),
914 Number(serde_json::Number),
915 String(String),
916 Array(Vec<Self>),
917 Object(OrderedJsonObject),
918}
919
920#[derive(Debug, Clone, PartialEq)]
922pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
923
924impl OrderedJsonObject {
925 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
927 &self.0
928 }
929
930 fn into_value(self) -> serde_json::Value {
931 serde_json::Value::Object(
932 self.0
933 .into_iter()
934 .map(|(key, value)| (key, value.into_value()))
935 .collect(),
936 )
937 }
938}
939
940impl OrderedJsonValue {
941 fn into_value(self) -> serde_json::Value {
942 match self {
943 Self::Null => serde_json::Value::Null,
944 Self::Bool(value) => serde_json::Value::Bool(value),
945 Self::Number(value) => serde_json::Value::Number(value),
946 Self::String(value) => serde_json::Value::String(value),
947 Self::Array(values) => {
948 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
949 }
950 Self::Object(value) => value.into_value(),
951 }
952 }
953}
954
955impl Serialize for OrderedJsonValue {
956 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
957 where
958 S: Serializer,
959 {
960 match self {
961 Self::Null => serializer.serialize_unit(),
962 Self::Bool(value) => serializer.serialize_bool(*value),
963 Self::Number(value) => value.serialize(serializer),
964 Self::String(value) => serializer.serialize_str(value),
965 Self::Array(values) => values.serialize(serializer),
966 Self::Object(value) => value.serialize(serializer),
967 }
968 }
969}
970
971impl<'de> Deserialize<'de> for OrderedJsonValue {
972 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
973 where
974 D: Deserializer<'de>,
975 {
976 struct OrderedValueVisitor;
977
978 impl<'de> Visitor<'de> for OrderedValueVisitor {
979 type Value = OrderedJsonValue;
980
981 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
982 formatter.write_str("a JSON value with ordered object members")
983 }
984
985 fn visit_unit<E>(self) -> Result<Self::Value, E>
986 where
987 E: serde::de::Error,
988 {
989 Ok(OrderedJsonValue::Null)
990 }
991
992 fn visit_none<E>(self) -> Result<Self::Value, E>
993 where
994 E: serde::de::Error,
995 {
996 Ok(OrderedJsonValue::Null)
997 }
998
999 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1000 where
1001 D: Deserializer<'de>,
1002 {
1003 OrderedJsonValue::deserialize(deserializer)
1004 }
1005
1006 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
1007 where
1008 E: serde::de::Error,
1009 {
1010 Ok(OrderedJsonValue::Bool(value))
1011 }
1012
1013 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
1014 where
1015 E: serde::de::Error,
1016 {
1017 Ok(OrderedJsonValue::Number(value.into()))
1018 }
1019
1020 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
1021 where
1022 E: serde::de::Error,
1023 {
1024 Ok(OrderedJsonValue::Number(value.into()))
1025 }
1026
1027 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1028 where
1029 E: serde::de::Error,
1030 {
1031 serde_json::Number::from_f64(value)
1032 .map(OrderedJsonValue::Number)
1033 .ok_or_else(|| E::custom("non-finite JSON number"))
1034 }
1035
1036 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1037 where
1038 E: serde::de::Error,
1039 {
1040 Ok(OrderedJsonValue::String(value.to_owned()))
1041 }
1042
1043 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
1044 where
1045 E: serde::de::Error,
1046 {
1047 Ok(OrderedJsonValue::String(value))
1048 }
1049
1050 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1051 where
1052 A: SeqAccess<'de>,
1053 {
1054 let mut values = Vec::new();
1055 while let Some(value) = sequence.next_element()? {
1056 values.push(value);
1057 }
1058 Ok(OrderedJsonValue::Array(values))
1059 }
1060
1061 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1062 where
1063 A: MapAccess<'de>,
1064 {
1065 let mut entries = Vec::new();
1066 while let Some((key, value)) = map.next_entry()? {
1067 entries.push((key, value));
1068 }
1069 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
1070 }
1071 }
1072
1073 deserializer.deserialize_any(OrderedValueVisitor)
1074 }
1075}
1076
1077impl Serialize for OrderedJsonObject {
1078 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1079 where
1080 S: Serializer,
1081 {
1082 let mut map = serializer.serialize_map(Some(self.0.len()))?;
1083 for (key, value) in &self.0 {
1084 map.serialize_entry(key, value)?;
1085 }
1086 map.end()
1087 }
1088}
1089
1090impl<'de> Deserialize<'de> for OrderedJsonObject {
1091 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1092 where
1093 D: Deserializer<'de>,
1094 {
1095 struct OrderedObjectVisitor;
1096
1097 impl<'de> Visitor<'de> for OrderedObjectVisitor {
1098 type Value = OrderedJsonObject;
1099
1100 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1101 formatter.write_str("an object with ordered JSON members")
1102 }
1103
1104 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1105 where
1106 A: MapAccess<'de>,
1107 {
1108 let mut entries = Vec::new();
1109 while let Some((key, value)) = map.next_entry()? {
1110 entries.push((key, value));
1111 }
1112 Ok(OrderedJsonObject(entries))
1113 }
1114 }
1115
1116 deserializer.deserialize_map(OrderedObjectVisitor)
1117 }
1118}
1119
1120fn read_tagged<'de, D>(
1121 deserializer: D,
1122 field: &'static str,
1123) -> Result<(String, OrderedJsonObject), D::Error>
1124where
1125 D: Deserializer<'de>,
1126{
1127 let body = OrderedJsonObject::deserialize(deserializer)?;
1128 let mut tag = None;
1129 for (key, value) in body.as_entries() {
1130 if key != field {
1131 continue;
1132 }
1133 if tag.is_some() {
1134 return Err(D::Error::custom(format!(
1135 "tagged object has duplicate `{field}` field"
1136 )));
1137 }
1138 let OrderedJsonValue::String(value) = value else {
1139 return Err(D::Error::custom(format!(
1140 "tagged object has no string `{field}` field"
1141 )));
1142 };
1143 tag = Some(value);
1144 }
1145 let Some(tag) = tag else {
1146 return Err(D::Error::custom(format!(
1147 "tagged object has no string `{field}` field"
1148 )));
1149 };
1150 Ok((tag.to_string(), body))
1151}
1152
1153fn read_ordered_tagged(
1154 value: OrderedJsonValue,
1155 field: &'static str,
1156) -> Result<(String, OrderedJsonObject), String> {
1157 let OrderedJsonValue::Object(body) = value else {
1158 return Err(format!("expected tagged object with `{field}` field"));
1159 };
1160 let mut tag = None;
1161 for (key, value) in body.as_entries() {
1162 if key != field {
1163 continue;
1164 }
1165 if tag.is_some() {
1166 return Err(format!("tagged object has duplicate `{field}` field"));
1167 }
1168 let OrderedJsonValue::String(value) = value else {
1169 return Err(format!("tagged object has no string `{field}` field"));
1170 };
1171 tag = Some(value);
1172 }
1173 let Some(tag) = tag else {
1174 return Err(format!("tagged object has no string `{field}` field"));
1175 };
1176 Ok((tag.to_string(), body))
1177}
1178
1179fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
1180 body.as_entries()
1181 .iter()
1182 .find_map(|(key, value)| (key == field).then_some(value))
1183}
1184
1185fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
1186 match ordered_field(body, field) {
1187 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
1188 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
1189 None => Err(format!("tagged object has no `{field}` field")),
1190 }
1191}
1192
1193fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
1194 let (tag, body) = read_ordered_tagged(value, "method")?;
1195 match tag.as_str() {
1196 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
1197 digest: ordered_string(&body, "digest")?,
1198 }),
1199 "macos_spawn_inode" => {
1200 let device = ordered_field(&body, "device")
1201 .and_then(|value| match value {
1202 OrderedJsonValue::Number(number) => number.as_u64(),
1203 _ => None,
1204 })
1205 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
1206 let inode = ordered_field(&body, "inode")
1207 .and_then(|value| match value {
1208 OrderedJsonValue::Number(number) => number.as_u64(),
1209 _ => None,
1210 })
1211 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
1212 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
1213 }
1214 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
1215 }
1216}
1217
1218impl Serialize for ModuleDeclaredProvenance {
1219 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1220 where
1221 S: Serializer,
1222 {
1223 match self {
1224 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1225 build: build.clone(),
1226 }
1227 .serialize(serializer),
1228 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1229 Self::Unknown { body, .. } => body.serialize(serializer),
1230 }
1231 }
1232}
1233
1234impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1235 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1236 where
1237 D: serde::Deserializer<'de>,
1238 {
1239 let (tag, value) = read_tagged(deserializer, "status")?;
1240 match tag.as_str() {
1241 "reported" => match serde_json::from_value(value.into_value())
1242 .map_err(D::Error::custom)?
1243 {
1244 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1245 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1246 },
1247 "unverifiable" => {
1248 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1249 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1250 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1251 }
1252 }
1253 _ => Ok(Self::Unknown { tag, body: value }),
1254 }
1255 }
1256}
1257
1258impl Serialize for RunningImageAgreement {
1259 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1260 where
1261 S: Serializer,
1262 {
1263 match self {
1264 Self::Match { evidence } => RunningImageAgreementWire::Match {
1265 evidence: evidence.clone(),
1266 }
1267 .serialize(serializer),
1268 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1269 running: running.clone(),
1270 disk: disk.clone(),
1271 }
1272 .serialize(serializer),
1273 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1274 reason: reason.clone(),
1275 }
1276 .serialize(serializer),
1277 Self::Unknown { body, .. } => body.serialize(serializer),
1278 }
1279 }
1280}
1281
1282impl Serialize for ReloadPathAgreement {
1283 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1284 where
1285 S: Serializer,
1286 {
1287 match self {
1288 Self::Match => ReloadPathAgreementWire::Match.serialize(serializer),
1289 Self::Mismatch {
1290 configured,
1291 spawned_from,
1292 } => ReloadPathAgreementWire::Mismatch {
1293 configured: configured.clone(),
1294 spawned_from: spawned_from.clone(),
1295 }
1296 .serialize(serializer),
1297 Self::Unavailable { reason } => ReloadPathAgreementWire::Unavailable {
1298 reason: reason.clone(),
1299 }
1300 .serialize(serializer),
1301 Self::Unknown { body, .. } => body.serialize(serializer),
1302 }
1303 }
1304}
1305
1306impl<'de> Deserialize<'de> for ReloadPathAgreement {
1307 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1308 where
1309 D: Deserializer<'de>,
1310 {
1311 let (tag, body) = read_tagged(deserializer, "status")?;
1312 match tag.as_str() {
1313 "match" => Ok(Self::Match),
1314 "mismatch" => {
1315 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1316 ReloadPathAgreementWire::Mismatch {
1317 configured,
1318 spawned_from,
1319 } => Ok(Self::Mismatch {
1320 configured,
1321 spawned_from,
1322 }),
1323 _ => unreachable!(),
1324 }
1325 }
1326 "unavailable" => match serde_json::from_value(body.into_value())
1327 .map_err(D::Error::custom)?
1328 {
1329 ReloadPathAgreementWire::Unavailable { reason } => Ok(Self::Unavailable { reason }),
1330 _ => unreachable!(),
1331 },
1332 _ => Ok(Self::Unknown { tag, body }),
1333 }
1334 }
1335}
1336
1337impl<'de> Deserialize<'de> for RunningImageAgreement {
1338 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1339 where
1340 D: serde::Deserializer<'de>,
1341 {
1342 let (tag, value) = read_tagged(deserializer, "status")?;
1343 match tag.as_str() {
1344 "match" => Ok(Self::Match {
1345 evidence: decode_running_image_evidence(
1346 ordered_field(&value, "evidence")
1347 .cloned()
1348 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1349 )
1350 .map_err(D::Error::custom)?,
1351 }),
1352 "mismatch" => Ok(Self::Mismatch {
1353 running: decode_running_image_evidence(
1354 ordered_field(&value, "running")
1355 .cloned()
1356 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1357 )
1358 .map_err(D::Error::custom)?,
1359 disk: decode_running_image_evidence(
1360 ordered_field(&value, "disk")
1361 .cloned()
1362 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1363 )
1364 .map_err(D::Error::custom)?,
1365 }),
1366 "unavailable" => Ok(Self::Unavailable {
1367 reason: serde_json::from_value(
1368 ordered_field(&value, "reason")
1369 .cloned()
1370 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1371 .into_value(),
1372 )
1373 .map_err(D::Error::custom)?,
1374 }),
1375 _ => Ok(Self::Unknown { tag, body: value }),
1376 }
1377 }
1378}
1379
1380impl Serialize for ChildResourceUsage {
1381 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1382 where
1383 S: Serializer,
1384 {
1385 match self {
1386 Self::Measured(reading) => {
1387 ChildResourceUsageWire::Measured(reading.clone()).serialize(serializer)
1388 }
1389 Self::Unavailable { reason } => ChildResourceUsageWire::Unavailable {
1390 reason: reason.clone(),
1391 }
1392 .serialize(serializer),
1393 Self::Unknown { body, .. } => body.serialize(serializer),
1394 }
1395 }
1396}
1397
1398impl<'de> Deserialize<'de> for ChildResourceUsage {
1399 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1400 where
1401 D: Deserializer<'de>,
1402 {
1403 let (tag, body) = read_tagged(deserializer, "status")?;
1404 match tag.as_str() {
1405 "measured" | "unavailable" => {
1406 match serde_json::from_value(body.into_value()).map_err(D::Error::custom)? {
1407 ChildResourceUsageWire::Measured(reading) => Ok(Self::Measured(reading)),
1408 ChildResourceUsageWire::Unavailable { reason } => {
1409 Ok(Self::Unavailable { reason })
1410 }
1411 }
1412 }
1413 _ => Ok(Self::Unknown { tag, body }),
1414 }
1415 }
1416}
1417
1418impl Serialize for RunningImageEvidence {
1419 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1420 where
1421 S: Serializer,
1422 {
1423 match self {
1424 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1425 digest: digest.clone(),
1426 }
1427 .serialize(serializer),
1428 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1429 device: *device,
1430 inode: *inode,
1431 }
1432 .serialize(serializer),
1433 Self::Unknown { body, .. } => body.serialize(serializer),
1434 }
1435 }
1436}
1437
1438impl<'de> Deserialize<'de> for RunningImageEvidence {
1439 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1440 where
1441 D: serde::Deserializer<'de>,
1442 {
1443 let (tag, value) = read_tagged(deserializer, "method")?;
1444 match tag.as_str() {
1445 "linux_proc_sha256" => {
1446 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1447 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1448 Ok(Self::LinuxProcSha256 { digest })
1449 }
1450 _ => unreachable!(),
1451 }
1452 }
1453 "macos_spawn_inode" => {
1454 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1455 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1456 Ok(Self::MacosSpawnInode { device, inode })
1457 }
1458 _ => unreachable!(),
1459 }
1460 }
1461 _ => Ok(Self::Unknown { tag, body: value }),
1462 }
1463 }
1464}
1465
1466impl Serialize for SupervisorRouteConsumer {
1467 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1468 where
1469 S: Serializer,
1470 {
1471 match self {
1472 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1473 module_id: module_id.clone(),
1474 }
1475 .serialize(serializer),
1476 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1477 connection_id: *connection_id,
1478 }
1479 .serialize(serializer),
1480 Self::Unknown { body, .. } => body.serialize(serializer),
1481 }
1482 }
1483}
1484
1485impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1486 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1487 where
1488 D: serde::Deserializer<'de>,
1489 {
1490 let (tag, value) = read_tagged(deserializer, "kind")?;
1491 match tag.as_str() {
1492 "reserved" => {
1493 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1494 SupervisorRouteConsumerWire::Reserved { module_id } => {
1495 Ok(Self::Reserved { module_id })
1496 }
1497 _ => unreachable!(),
1498 }
1499 }
1500 "direct" => {
1501 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1502 SupervisorRouteConsumerWire::Direct { connection_id } => {
1503 Ok(Self::Direct { connection_id })
1504 }
1505 _ => unreachable!(),
1506 }
1507 }
1508 _ => Ok(Self::Unknown { tag, body: value }),
1509 }
1510 }
1511}
1512
1513impl Serialize for StderrCaptureState {
1514 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1515 where
1516 S: Serializer,
1517 {
1518 match self {
1519 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1520 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1521 reason: reason.clone(),
1522 }
1523 .serialize(serializer),
1524 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1525 reason: reason.clone(),
1526 }
1527 .serialize(serializer),
1528 Self::Unknown { body, .. } => body.serialize(serializer),
1529 }
1530 }
1531}
1532
1533impl<'de> Deserialize<'de> for StderrCaptureState {
1534 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1535 where
1536 D: serde::Deserializer<'de>,
1537 {
1538 let (tag, value) = read_tagged(deserializer, "state")?;
1539 match tag.as_str() {
1540 "captured" => {
1541 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1542 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1543 _ => unreachable!(),
1544 }
1545 }
1546 "incomplete" => match serde_json::from_value(value.into_value())
1547 .map_err(D::Error::custom)?
1548 {
1549 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1550 _ => unreachable!(),
1551 },
1552 "not_captured" => match serde_json::from_value(value.into_value())
1553 .map_err(D::Error::custom)?
1554 {
1555 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1556 _ => unreachable!(),
1557 },
1558 _ => Ok(Self::Unknown { tag, body: value }),
1559 }
1560 }
1561}
1562
1563impl Serialize for StderrTailEntry {
1564 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1565 where
1566 S: Serializer,
1567 {
1568 match self {
1569 Self::Line {
1570 text,
1571 truncated,
1572 at_ms,
1573 } => StderrTailEntryWire::Line {
1574 text: text.clone(),
1575 truncated: *truncated,
1576 at_ms: *at_ms,
1577 }
1578 .serialize(serializer),
1579 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1580 Self::Unknown { body, .. } => body.serialize(serializer),
1581 }
1582 }
1583}
1584
1585impl<'de> Deserialize<'de> for StderrTailEntry {
1586 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1587 where
1588 D: serde::Deserializer<'de>,
1589 {
1590 let (tag, value) = read_tagged(deserializer, "kind")?;
1591 match tag.as_str() {
1592 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1593 StderrTailEntryWire::Line {
1594 text,
1595 truncated,
1596 at_ms,
1597 } => Ok(Self::Line {
1598 text,
1599 truncated,
1600 at_ms,
1601 }),
1602 _ => unreachable!(),
1603 },
1604 "process_start" => {
1605 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1606 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1607 _ => unreachable!(),
1608 }
1609 }
1610 _ => Ok(Self::Unknown { tag, body: value }),
1611 }
1612 }
1613}
1614
1615fn is_zero_u64(value: &u64) -> bool {
1616 *value == 0
1617}
1618
1619fn default_true() -> bool {
1620 true
1621}
1622
1623#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1625pub struct TerminalHistory {
1626 pub daemon_started_at_ms: u64,
1628 pub entries: Vec<TerminalEntry>,
1629 #[serde(default, skip_serializing_if = "is_zero_u64")]
1632 pub dropped: u64,
1633 #[serde(default, skip_serializing_if = "is_zero_u64")]
1636 pub journal_skipped_lines: u64,
1637 #[serde(default, skip_serializing_if = "is_zero_u64")]
1639 pub journal_read_errors: u64,
1640 #[serde(default, skip_serializing_if = "is_zero_u64")]
1642 pub journal_write_failures: u64,
1643}
1644
1645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1647pub struct TerminalEntry {
1648 #[serde(default, skip_serializing_if = "Option::is_none")]
1651 pub daemon_incarnation: Option<String>,
1652 #[serde(default, skip_serializing_if = "Option::is_none")]
1653 pub exit_code: Option<i32>,
1654 #[serde(default, skip_serializing_if = "Option::is_none")]
1655 pub exit_signal: Option<i32>,
1656 pub at_ms: u64,
1657 pub disposition: TerminalDisposition,
1658 #[serde(default, skip_serializing_if = "Option::is_none")]
1662 pub exit_kind: Option<TerminalExitKind>,
1663 #[serde(default, skip_serializing_if = "Option::is_none")]
1670 pub disposition_detail: Option<String>,
1671}
1672
1673#[derive(Debug, Clone, PartialEq, Eq)]
1678pub enum TerminalExitKind {
1679 Clean,
1680 Crash,
1681 DeliberateSeverance,
1682 Unknown(String),
1683}
1684
1685impl TerminalExitKind {
1686 fn wire_name(&self) -> &str {
1687 match self {
1688 Self::Clean => "clean",
1689 Self::Crash => "crash",
1690 Self::DeliberateSeverance => "deliberate_severance",
1691 Self::Unknown(value) => value,
1692 }
1693 }
1694}
1695
1696impl Serialize for TerminalExitKind {
1697 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1698 where
1699 S: serde::Serializer,
1700 {
1701 serializer.serialize_str(self.wire_name())
1702 }
1703}
1704
1705impl<'de> Deserialize<'de> for TerminalExitKind {
1706 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1707 where
1708 D: serde::Deserializer<'de>,
1709 {
1710 let value = String::deserialize(deserializer)?;
1711 Ok(match value.as_str() {
1712 "clean" => Self::Clean,
1713 "crash" => Self::Crash,
1714 "deliberate_severance" => Self::DeliberateSeverance,
1715 _ => Self::Unknown(value),
1716 })
1717 }
1718}
1719
1720open_string_enum! {
1721 TerminalDisposition {
1723 Stopped => "stopped",
1724 Disabled => "disabled",
1725 Failed => "failed",
1726 Restarting => "restarting",
1727 DaemonShutdown => "daemon_shutdown",
1732 }
1733}
1734
1735#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1736#[serde(rename_all = "snake_case")]
1737pub enum PollKind {
1738 Status,
1739 Liveness,
1740}
1741
1742#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1743pub struct CatalogEntry {
1744 pub module_id: String,
1745 #[serde(default = "default_true")]
1756 pub ready: bool,
1757 #[serde(default, skip_serializing_if = "Option::is_none")]
1761 pub not_ready: Option<NotReadyReason>,
1762 #[serde(default, skip_serializing_if = "Option::is_none")]
1783 pub module_version: Option<String>,
1784 pub roles: Vec<ProviderRole>,
1785 pub control_ops: Vec<String>,
1786 #[serde(default, skip_serializing_if = "Option::is_none")]
1791 pub capabilities: Option<CapabilityDeclarations>,
1792 #[serde(default, skip_serializing_if = "Option::is_none")]
1795 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1796}
1797
1798#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1800pub struct NotReadyReason {
1801 pub reason: String,
1806 #[serde(default, skip_serializing_if = "Option::is_none")]
1809 pub capability: Option<String>,
1810}
1811
1812impl NotReadyReason {
1813 pub const DECLARED_NOT_READY: &'static str = "declared_not_ready";
1814 pub const REQUIRED_CAPABILITY_UNPROVIDED: &'static str = "required_capability_unprovided";
1815}
1816
1817#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1818pub struct CapabilityRequirementStatus {
1819 pub consumer: String,
1820 pub capability: String,
1821 pub need: String,
1822 pub verdict: String,
1823 pub episode_seq: u64,
1824 pub config_satisfiable: bool,
1825 pub runtime_available: bool,
1826 pub detail: String,
1827}
1828
1829#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1830pub struct SupervisorRescanResult {
1831 pub added: Vec<String>,
1832 pub removed: Vec<String>,
1833 pub changed_pending_reload: Vec<String>,
1834 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1847 pub enabled_changes: Vec<String>,
1848 pub unchanged: u32,
1849 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1857 pub preview: bool,
1858 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1876 pub restart_required: Vec<String>,
1877 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1881 pub capability_warnings: Vec<String>,
1882}
1883
1884#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1900#[serde(rename_all = "snake_case")]
1901pub enum ModuleProtocol {
1902 #[default]
1906 Subc,
1907 None,
1916}
1917
1918#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1919pub struct SupervisorEntry {
1920 pub module_id: String,
1921 pub state: String,
1922 pub enabled: bool,
1923 pub live: bool,
1933 #[serde(default)]
1937 pub protocol: ModuleProtocol,
1938 #[serde(default, skip_serializing_if = "Option::is_none")]
1941 pub launch_nonce_env: Option<bool>,
1942 pub health: SupervisorHealthStatus,
1943 #[serde(default, skip_serializing_if = "Option::is_none")]
1946 pub pending_reload: Option<PendingReloadVerdict>,
1947 #[serde(default)]
1953 pub last_probe_ms: Option<u64>,
1954 #[serde(default, skip_serializing_if = "Option::is_none")]
1958 pub last_exit_code: Option<i32>,
1959 #[serde(default, skip_serializing_if = "Option::is_none")]
1963 pub last_exit_signal: Option<i32>,
1964 #[serde(default, skip_serializing_if = "Option::is_none")]
1968 pub last_exit_ms: Option<u64>,
1969 #[serde(default, skip_serializing_if = "Option::is_none")]
1972 pub last_exit_kind: Option<TerminalExitKind>,
1973 #[serde(default, skip_serializing_if = "Option::is_none")]
1990 pub restart_count: Option<u32>,
1991 #[serde(default, skip_serializing_if = "Option::is_none")]
1994 pub max_restarts: Option<u32>,
1995 #[serde(default, skip_serializing_if = "Option::is_none")]
1998 pub lifetime_restarts: Option<u32>,
1999 #[serde(default, skip_serializing_if = "Option::is_none")]
2003 pub spawn_generation: Option<u64>,
2004 #[serde(default, skip_serializing_if = "Option::is_none")]
2014 pub restart_window_secs: Option<u64>,
2015 #[serde(default, skip_serializing_if = "Option::is_none")]
2019 pub drain_timeout_ms: Option<u64>,
2020 #[serde(default, skip_serializing_if = "Option::is_none")]
2023 pub restart_backoff_ms: Option<u64>,
2024 #[serde(default, skip_serializing_if = "Option::is_none")]
2027 pub restart_max_backoff_ms: Option<u64>,
2028 #[serde(default, skip_serializing_if = "Option::is_none")]
2041 pub resources: Option<ChildResourceUsage>,
2042}
2043
2044#[derive(Debug, Clone, PartialEq)]
2047pub enum ChildResourceUsage {
2048 Measured(ChildResourceReading),
2049 Unavailable {
2050 reason: ChildResourceUnavailableReason,
2051 },
2052 Unknown {
2055 tag: String,
2056 body: OrderedJsonObject,
2057 },
2058}
2059
2060#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2062pub struct ChildResourceReading {
2063 pub memory_bytes: u64,
2066 pub memory_kind: ChildMemoryKind,
2067 #[serde(default, skip_serializing_if = "Option::is_none")]
2070 pub swap_bytes: Option<u64>,
2071 pub cpu_user_ms: u64,
2075 pub cpu_system_ms: u64,
2078}
2079
2080open_string_enum! {
2081 ChildMemoryKind {
2083 PhysFootprint => "phys_footprint",
2087 ResidentSet => "resident_set",
2090 }
2091}
2092
2093open_string_enum! {
2094 ChildResourceUnavailableReason {
2096 NotRunning => "not_running",
2098 UnsupportedPlatform => "unsupported_platform",
2100 Unreadable => "unreadable",
2103 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
2106 }
2107}
2108
2109#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2110#[serde(rename_all = "snake_case")]
2111pub enum SupervisorHealthStatus {
2112 Ok,
2113 Degraded,
2114 Failing,
2115 Unresponsive,
2116 Unknown,
2117}
2118
2119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2120pub struct SupervisorHealthEntry {
2121 pub module_id: String,
2122 pub status: SupervisorHealthStatus,
2123 #[serde(default, skip_serializing_if = "Option::is_none")]
2129 pub detail: Option<String>,
2130 #[serde(default, skip_serializing_if = "Option::is_none")]
2135 pub metrics: Option<serde_json::Value>,
2136 pub consecutive_failures: u32,
2137 #[serde(default)]
2140 pub late_answer_count: u64,
2141 #[serde(default, skip_serializing_if = "Option::is_none")]
2143 pub last_late_answer_latency_ms: Option<u64>,
2144 #[serde(default)]
2149 pub last_action: Option<String>,
2150 #[serde(default)]
2153 pub last_action_ms: Option<u64>,
2154 #[serde(default, skip_serializing_if = "Option::is_none")]
2167 pub last_probe_ms: Option<u64>,
2168}
2169
2170#[cfg(test)]
2171mod tests {
2172 use super::*;
2173 use subc_protocol::{BindIdentity, RouteTarget};
2174
2175 #[test]
2176 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
2177 let entry = TerminalEntry {
2178 daemon_incarnation: Some("daemon-before-restart".into()),
2179 exit_code: Some(1),
2180 exit_signal: None,
2181 at_ms: 1_700_000_000_123,
2182 disposition: TerminalDisposition::Restarting,
2183 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
2184 disposition_detail: None,
2185 };
2186 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
2187 assert_eq!(
2188 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
2189 ["exit_kind"],
2190 "deliberate_severance"
2191 );
2192
2193 #[derive(serde::Deserialize)]
2194 struct LegacyTerminalEntry {
2195 exit_code: Option<i32>,
2196 exit_signal: Option<i32>,
2197 at_ms: u64,
2198 disposition: TerminalDisposition,
2199 }
2200
2201 let decoded: LegacyTerminalEntry =
2202 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
2203 assert_eq!(decoded.exit_code, Some(1));
2204 assert_eq!(decoded.exit_signal, None);
2205 assert_eq!(decoded.at_ms, 1_700_000_000_123);
2206 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
2207
2208 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
2209 let future: TerminalEntry =
2210 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
2211 assert_eq!(
2212 future.exit_kind,
2213 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
2214 );
2215 }
2216
2217 #[test]
2218 fn terminal_incarnation_is_optional_for_older_daemons() {
2219 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
2220 "at_ms": 123,
2221 "disposition": "stopped"
2222 }))
2223 .unwrap();
2224 let encoded = serde_json::to_value(&entry).unwrap();
2225 assert_eq!(
2226 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
2227 (None, None)
2228 );
2229 }
2230
2231 #[test]
2232 fn route_poll_uses_kind_field() {
2233 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
2234 route_channel: 7,
2235 route_epoch: 11,
2236 kind: PollKind::Status,
2237 })
2238 .unwrap();
2239
2240 assert_eq!(body["op"], "route.poll");
2241 assert_eq!(body["route_epoch"], 11);
2242 assert_eq!(body["kind"], "status");
2243 assert!(body.get("op").is_some());
2244 }
2245
2246 #[test]
2247 fn route_open_is_internally_tagged() {
2248 let request = ClientControlRequest::RouteOpen {
2249 target: RouteTarget::ToolProvider {
2250 module_id: "aft".to_string(),
2251 },
2252 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
2253 consumer_identity: None,
2254 consumer_capabilities: None,
2255 role_versions: None,
2256 admission_facts: None,
2257 scope: None,
2258 };
2259
2260 let body = serde_json::to_value(request).unwrap();
2261 assert_eq!(body["op"], "route.open");
2262 assert_eq!(body["target"]["kind"], "tool_provider");
2263 assert!(body.get("consumer_identity").is_none());
2264 assert!(body.get("consumer_capabilities").is_none());
2265 assert!(body.get("role_versions").is_none());
2266 }
2267
2268 #[test]
2269 fn route_open_without_optional_fields_still_decodes() {
2270 let body = serde_json::json!({
2271 "op": "route.open",
2272 "target": { "kind": "tool_provider", "module_id": "aft" },
2273 "identity": {
2274 "project_root": "/tmp/project",
2275 "harness": "opencode",
2276 "session": "session-1"
2277 }
2278 });
2279
2280 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
2281 let ClientControlRequest::RouteOpen {
2282 consumer_identity,
2283 consumer_capabilities,
2284 role_versions,
2285 admission_facts,
2286 ..
2287 } = decoded
2288 else {
2289 panic!("decoded wrong request variant");
2290 };
2291 assert_eq!(consumer_identity, None);
2292 assert_eq!(consumer_capabilities, None);
2293 assert_eq!(role_versions, None);
2294 assert_eq!(admission_facts, None);
2295 }
2296
2297 #[test]
2298 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
2299 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
2300 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
2301 match decoded {
2302 ClientControlPush::RouteClosed {
2303 excluded_subscriptions,
2304 terminal,
2305 ..
2306 } => {
2307 assert_eq!(excluded_subscriptions, 0);
2308 assert_eq!(terminal, None);
2309 }
2310 other => panic!("unexpected push: {other:?}"),
2311 }
2312 assert!(!serde_json::to_string(&decoded)
2313 .unwrap()
2314 .contains("terminal"));
2315 }
2316
2317 #[test]
2318 fn old_route_closed_decoder_ignores_new_terminal_field() {
2319 #[derive(serde::Deserialize)]
2320 #[serde(tag = "op")]
2321 enum LegacyClientControlPush {
2322 #[serde(rename = "route.closed")]
2323 RouteClosed {
2324 module_id: String,
2325 reason: RouteCloseReason,
2326 drained: bool,
2327 abandoned: u32,
2328 },
2329 }
2330
2331 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
2332 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
2333 match decoded {
2334 LegacyClientControlPush::RouteClosed {
2335 module_id,
2336 reason,
2337 drained,
2338 abandoned,
2339 } => {
2340 assert_eq!(module_id, "aft-tools");
2341 assert_eq!(reason, RouteCloseReason::Crash);
2342 assert!(!drained);
2343 assert_eq!(abandoned, 0);
2344 }
2345 }
2346 }
2347
2348 #[test]
2349 fn supervisor_routes_is_a_control_plane_request() {
2350 let body = serde_json::json!({
2351 "op": "supervisor.routes",
2352 "module_id": "aft"
2353 });
2354
2355 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
2356 assert_eq!(serde_json::to_value(request).unwrap(), body);
2357 }
2358
2359 #[test]
2360 fn diagnostic_string_enums_retain_unknown_wire_values() {
2361 let reason: RunningImageUnavailableReason =
2362 serde_json::from_str("\"future_reason\"").unwrap();
2363 let disposition: TerminalDisposition =
2364 serde_json::from_str("\"future_disposition\"").unwrap();
2365
2366 assert_eq!(
2367 reason,
2368 RunningImageUnavailableReason::Unknown("future_reason".to_string())
2369 );
2370 assert_eq!(
2371 disposition,
2372 TerminalDisposition::Unknown("future_disposition".to_string())
2373 );
2374 }
2375
2376 #[test]
2377 fn diagnostic_string_enums_preserve_existing_wire_names() {
2378 let names = [
2379 (RunningImageUnavailableReason::NotRunning, "not_running"),
2380 (
2381 RunningImageUnavailableReason::UnsupportedPlatform,
2382 "unsupported_platform",
2383 ),
2384 (
2385 RunningImageUnavailableReason::RunningExecutableUnreadable,
2386 "running_executable_unreadable",
2387 ),
2388 (
2389 RunningImageUnavailableReason::SpawnedPathUnreadable,
2390 "spawned_path_unreadable",
2391 ),
2392 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2393 (
2394 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2395 "process_identity_unconfirmed",
2396 ),
2397 ];
2398 for (value, expected) in names {
2399 let wire = serde_json::to_string(&value).unwrap();
2400 assert_eq!(wire, format!("\"{expected}\""));
2401 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2402 assert_eq!(decoded, value);
2403 }
2404
2405 for (value, expected) in [
2406 (TerminalDisposition::Stopped, "stopped"),
2407 (TerminalDisposition::Disabled, "disabled"),
2408 (TerminalDisposition::Failed, "failed"),
2409 (TerminalDisposition::Restarting, "restarting"),
2410 (TerminalDisposition::DaemonShutdown, "daemon_shutdown"),
2411 ] {
2412 let wire = serde_json::to_string(&value).unwrap();
2413 assert_eq!(wire, format!("\"{expected}\""));
2414 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2415 assert_eq!(decoded, value);
2416 }
2417 }
2418
2419 #[test]
2420 fn diagnostic_string_enums_reject_non_string_bodies() {
2421 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2422 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2423 }
2424
2425 #[test]
2426 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2427 let body = serde_json::json!({
2428 "op": "supervisor.provenance",
2429 "daemon": {
2430 "daemon_build": {},
2431 "daemon_observed": {
2432 "running_image": {
2433 "status": "unavailable",
2434 "reason": "not_running"
2435 }
2436 }
2437 },
2438 "modules": [
2439 {
2440 "module_id": "future",
2441 "module_declared": { "status": "unverifiable" },
2442 "daemon_observed": {
2443 "running_image": {
2444 "status": "unavailable",
2445 "reason": "future_reason"
2446 }
2447 }
2448 },
2449 {
2450 "module_id": "healthy-a",
2451 "module_declared": { "status": "unverifiable" },
2452 "daemon_observed": {
2453 "running_image": {
2454 "status": "match",
2455 "evidence": {
2456 "method": "linux_proc_sha256",
2457 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2458 }
2459 }
2460 }
2461 },
2462 {
2463 "module_id": "healthy-b",
2464 "module_declared": { "status": "unverifiable" },
2465 "daemon_observed": {
2466 "running_image": {
2467 "status": "unavailable",
2468 "reason": "unsupported_platform"
2469 }
2470 }
2471 }
2472 ]
2473 });
2474
2475 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2476 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2477 panic!("decoded wrong response variant");
2478 };
2479 assert_eq!(modules.len(), 3);
2480 assert_eq!(modules[0].module_id, "future");
2481 assert_eq!(
2482 modules[0].daemon_observed.running_image,
2483 RunningImageAgreement::Unavailable {
2484 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2485 }
2486 );
2487 assert_eq!(modules[1].module_id, "healthy-a");
2488 assert_eq!(modules[2].module_id, "healthy-b");
2489 }
2490
2491 #[test]
2492 fn tagged_unknown_values_retain_tag_and_body() {
2493 macro_rules! assert_unknown_round_trip {
2494 ($ty:ident, $field:literal, $value:expr) => {
2495 let value = $value;
2496 let wire = serde_json::to_string(&value).unwrap();
2497 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2498 match decoded {
2499 $ty::Unknown { tag, body } => {
2500 assert_eq!(tag, value[$field].as_str().unwrap());
2501 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2502 }
2503 _ => panic!("decoded known variant"),
2504 }
2505 };
2506 }
2507
2508 assert_unknown_round_trip!(
2509 ModuleDeclaredProvenance,
2510 "status",
2511 serde_json::json!({"status": "future", "build": {"version": 7}})
2512 );
2513 assert_unknown_round_trip!(
2514 RunningImageAgreement,
2515 "status",
2516 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2517 );
2518 assert_unknown_round_trip!(
2519 RunningImageEvidence,
2520 "method",
2521 serde_json::json!({"method": "future", "digest": "abc"})
2522 );
2523 assert_unknown_round_trip!(
2524 SupervisorRouteConsumer,
2525 "kind",
2526 serde_json::json!({"kind": "future", "module_id": "m"})
2527 );
2528 assert_unknown_round_trip!(
2529 StderrCaptureState,
2530 "state",
2531 serde_json::json!({"state": "future", "reason": "because"})
2532 );
2533 assert_unknown_round_trip!(
2534 StderrTailEntry,
2535 "kind",
2536 serde_json::json!({"kind": "future", "text": "line"})
2537 );
2538 assert_unknown_round_trip!(
2539 ChildResourceUsage,
2540 "status",
2541 serde_json::json!({"status": "future", "memory_bytes": 1})
2542 );
2543 }
2544
2545 #[test]
2546 fn child_resource_usage_round_trips_both_known_states() {
2547 let measured = ChildResourceUsage::Measured(ChildResourceReading {
2548 memory_bytes: 0,
2549 memory_kind: ChildMemoryKind::ResidentSet,
2550 swap_bytes: Some(0),
2551 cpu_user_ms: 0,
2552 cpu_system_ms: 0,
2553 });
2554 let wire = serde_json::to_value(&measured).unwrap();
2555 assert_eq!(
2556 wire,
2557 serde_json::json!({
2558 "status": "measured",
2559 "memory_bytes": 0,
2560 "memory_kind": "resident_set",
2561 "swap_bytes": 0,
2562 "cpu_user_ms": 0,
2563 "cpu_system_ms": 0
2564 })
2565 );
2566 assert_eq!(
2567 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2568 measured
2569 );
2570
2571 let unavailable = ChildResourceUsage::Unavailable {
2572 reason: ChildResourceUnavailableReason::NotRunning,
2573 };
2574 let wire = serde_json::to_value(&unavailable).unwrap();
2575 assert_eq!(
2576 wire,
2577 serde_json::json!({"status": "unavailable", "reason": "not_running"})
2578 );
2579 assert_eq!(
2580 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2581 unavailable
2582 );
2583 }
2584
2585 #[test]
2586 fn a_stderr_line_decodes_with_and_without_its_capture_time() {
2587 let stamped: StderrTailEntry = serde_json::from_str(
2591 r#"{"kind":"line","text":"boom","truncated":true,"at_ms":1789801440685}"#,
2592 )
2593 .unwrap();
2594 assert_eq!(
2595 stamped,
2596 StderrTailEntry::Line {
2597 text: "boom".to_string(),
2598 truncated: true,
2599 at_ms: Some(1_789_801_440_685),
2600 }
2601 );
2602 let unstamped: StderrTailEntry =
2603 serde_json::from_str(r#"{"kind":"line","text":"boom"}"#).unwrap();
2604 assert_eq!(
2605 unstamped,
2606 StderrTailEntry::Line {
2607 text: "boom".to_string(),
2608 truncated: false,
2609 at_ms: None,
2610 }
2611 );
2612 assert_eq!(
2615 serde_json::to_string(&unstamped).unwrap(),
2616 r#"{"kind":"line","text":"boom"}"#
2617 );
2618 assert_eq!(
2619 serde_json::to_value(&stamped).unwrap()["at_ms"],
2620 serde_json::json!(1_789_801_440_685u64)
2621 );
2622 }
2623
2624 #[test]
2625 fn tagged_unknown_values_round_trip_the_original_json() {
2626 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2627 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2628 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2629 }
2630
2631 #[test]
2632 fn tagged_unknown_values_round_trip_trailing_tag() {
2633 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
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#"{"reason":"because","state":"future_state"}"#;
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_middle_tag() {
2644 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
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":1,"state":"future_state","b":2}"#;
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_round_trip_deep_payload() {
2655 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2656 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2657 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2658
2659 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2660 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2661 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2662 }
2663
2664 #[test]
2665 fn tagged_unknown_values_reject_non_object_bodies() {
2666 for wire in ["42", r#""future""#, "[]"] {
2667 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2668 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2669 }
2670 }
2671
2672 #[test]
2673 fn duplicate_discriminators_reject_without_panicking() {
2674 assert_eq!(
2675 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2676 .unwrap(),
2677 ModuleDeclaredProvenance::Unverifiable
2678 );
2679 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2680 .unwrap()
2681 {
2682 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2683 _ => panic!("future discriminator decoded as a known variant"),
2684 }
2685
2686 let wires = [
2687 r#"{"status":"reported","status":"unverifiable"}"#,
2688 r#"{"status":"unverifiable","status":"reported"}"#,
2689 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2690 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2691 ];
2692
2693 for wire in wires {
2694 let result =
2695 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2696 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2697 assert!(
2698 result.unwrap().is_err(),
2699 "duplicate discriminator decoded: {wire}"
2700 );
2701 }
2702
2703 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2704 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2705 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2706 assert!(
2707 result.unwrap().is_err(),
2708 "duplicate discriminator decoded: {wire}"
2709 );
2710 }
2711
2712 #[test]
2713 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2714 let known_wire =
2715 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2716 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2717 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2718
2719 for wire in [
2720 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2721 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2722 ] {
2723 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2724 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2725 }
2726
2727 for wire in [
2728 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2729 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2730 ] {
2731 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2732 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2733 }
2734
2735 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2736 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2737 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2738
2739 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2740 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2741 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2742 }
2743
2744 #[test]
2745 fn tagged_unknown_member_does_not_discard_known_siblings() {
2746 let body = serde_json::json!({
2747 "modules": [{
2748 "module_id": "target",
2749 "routes": [
2750 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2751 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2752 ]
2753 }]
2754 });
2755 let decoded: ClientControlResponse = serde_json::from_value(
2756 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2757 )
2758 .unwrap();
2759 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2760 panic!("decoded wrong response variant");
2761 };
2762 assert_eq!(modules[0].routes.len(), 2);
2763 assert_eq!(
2764 modules[0].routes[1].consumer,
2765 SupervisorRouteConsumer::Direct { connection_id: 7 }
2766 );
2767 }
2768}
2769
2770#[cfg(test)]
2771mod launch_nonce_redaction_tests {
2772 use super::*;
2773
2774 const NONCE: &str = "nonce-f00dfeed1234abcd";
2775
2776 fn identity() -> ConsumerIdentity {
2777 ConsumerIdentity {
2778 module_id: "wernicke".to_string(),
2779 launch_nonce: NONCE.to_string(),
2780 }
2781 }
2782
2783 #[test]
2784 fn consumer_identity_debug_names_the_module_and_never_the_nonce() {
2785 let printed = format!("{:?}", identity());
2786 assert!(printed.contains("wernicke"), "{printed}");
2787 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
2788 }
2789
2790 #[test]
2791 fn route_open_request_debug_never_prints_the_nonce() {
2792 let request = ClientControlRequest::RouteOpen {
2793 target: subc_protocol::RouteTarget::ToolProvider {
2794 module_id: "broca".to_string(),
2795 },
2796 identity: subc_protocol::BindIdentity::new(
2797 PathBuf::from("/tmp/project"),
2798 "test".to_string(),
2799 "session".to_string(),
2800 ),
2801 consumer_identity: Some(identity()),
2802 consumer_capabilities: None,
2803 role_versions: None,
2804 admission_facts: None,
2805 scope: None,
2806 };
2807 let printed = format!("{request:?}");
2808 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
2809 }
2810}