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)]
1754#[non_exhaustive]
1755pub struct CatalogEntry {
1756 pub module_id: String,
1757 #[serde(default = "default_true")]
1768 pub ready: bool,
1769 #[serde(default, skip_serializing_if = "Option::is_none")]
1773 pub not_ready: Option<NotReadyReason>,
1774 #[serde(default, skip_serializing_if = "Option::is_none")]
1795 pub module_version: Option<String>,
1796 pub roles: Vec<ProviderRole>,
1797 pub control_ops: Vec<String>,
1798 #[serde(default, skip_serializing_if = "Option::is_none")]
1803 pub capabilities: Option<CapabilityDeclarations>,
1804 #[serde(default, skip_serializing_if = "Option::is_none")]
1807 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1808}
1809
1810impl CatalogEntry {
1811 #[must_use]
1813 pub fn new(
1814 module_id: impl Into<String>,
1815 roles: Vec<ProviderRole>,
1816 control_ops: Vec<String>,
1817 ) -> Self {
1818 Self {
1819 module_id: module_id.into(),
1820 ready: default_true(),
1821 not_ready: None,
1822 module_version: None,
1823 roles,
1824 control_ops,
1825 capabilities: None,
1826 self_signals: None,
1827 }
1828 }
1829
1830 #[must_use]
1832 pub fn with_ready(mut self, ready: bool) -> Self {
1833 self.ready = ready;
1834 self
1835 }
1836
1837 #[must_use]
1839 pub fn with_not_ready(mut self, not_ready: Option<NotReadyReason>) -> Self {
1840 self.not_ready = not_ready;
1841 self
1842 }
1843
1844 #[must_use]
1846 pub fn with_module_version(mut self, module_version: Option<String>) -> Self {
1847 self.module_version = module_version;
1848 self
1849 }
1850
1851 #[must_use]
1853 pub fn with_capabilities(mut self, capabilities: Option<CapabilityDeclarations>) -> Self {
1854 self.capabilities = capabilities;
1855 self
1856 }
1857
1858 #[must_use]
1860 pub fn with_self_signals(mut self, self_signals: Option<Vec<SelfSignalDeclaration>>) -> Self {
1861 self.self_signals = self_signals;
1862 self
1863 }
1864}
1865
1866#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1868pub struct NotReadyReason {
1869 pub reason: String,
1874 #[serde(default, skip_serializing_if = "Option::is_none")]
1877 pub capability: Option<String>,
1878}
1879
1880impl NotReadyReason {
1881 pub const DECLARED_NOT_READY: &'static str = "declared_not_ready";
1882 pub const REQUIRED_CAPABILITY_UNPROVIDED: &'static str = "required_capability_unprovided";
1883}
1884
1885#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1886pub struct CapabilityRequirementStatus {
1887 pub consumer: String,
1888 pub capability: String,
1889 pub need: String,
1890 pub verdict: String,
1891 pub episode_seq: u64,
1892 pub config_satisfiable: bool,
1893 pub runtime_available: bool,
1894 pub detail: String,
1895}
1896
1897#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1898pub struct SupervisorRescanResult {
1899 pub added: Vec<String>,
1900 pub removed: Vec<String>,
1901 pub changed_pending_reload: Vec<String>,
1902 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1915 pub enabled_changes: Vec<String>,
1916 pub unchanged: u32,
1917 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1925 pub preview: bool,
1926 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1944 pub restart_required: Vec<String>,
1945 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1949 pub capability_warnings: Vec<String>,
1950}
1951
1952#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1968#[serde(rename_all = "snake_case")]
1969pub enum ModuleProtocol {
1970 #[default]
1974 Subc,
1975 None,
1984}
1985
1986#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1998#[non_exhaustive]
1999pub struct SupervisorEntry {
2000 pub module_id: String,
2001 pub state: String,
2002 pub enabled: bool,
2003 pub live: bool,
2013 #[serde(default)]
2017 pub protocol: ModuleProtocol,
2018 #[serde(default, skip_serializing_if = "Option::is_none")]
2021 pub launch_nonce_env: Option<bool>,
2022 pub health: SupervisorHealthStatus,
2023 #[serde(default, skip_serializing_if = "Option::is_none")]
2026 pub pending_reload: Option<PendingReloadVerdict>,
2027 #[serde(default)]
2033 pub last_probe_ms: Option<u64>,
2034 #[serde(default, skip_serializing_if = "Option::is_none")]
2038 pub last_exit_code: Option<i32>,
2039 #[serde(default, skip_serializing_if = "Option::is_none")]
2043 pub last_exit_signal: Option<i32>,
2044 #[serde(default, skip_serializing_if = "Option::is_none")]
2048 pub last_exit_ms: Option<u64>,
2049 #[serde(default, skip_serializing_if = "Option::is_none")]
2052 pub last_exit_kind: Option<TerminalExitKind>,
2053 #[serde(default, skip_serializing_if = "Option::is_none")]
2070 pub restart_count: Option<u32>,
2071 #[serde(default, skip_serializing_if = "Option::is_none")]
2074 pub max_restarts: Option<u32>,
2075 #[serde(default, skip_serializing_if = "Option::is_none")]
2078 pub lifetime_restarts: Option<u32>,
2079 #[serde(default, skip_serializing_if = "Option::is_none")]
2083 pub spawn_generation: Option<u64>,
2084 #[serde(default, skip_serializing_if = "Option::is_none")]
2094 pub restart_window_secs: Option<u64>,
2095 #[serde(default, skip_serializing_if = "Option::is_none")]
2099 pub drain_timeout_ms: Option<u64>,
2100 #[serde(default, skip_serializing_if = "Option::is_none")]
2103 pub restart_backoff_ms: Option<u64>,
2104 #[serde(default, skip_serializing_if = "Option::is_none")]
2107 pub restart_max_backoff_ms: Option<u64>,
2108 #[serde(default, skip_serializing_if = "Option::is_none")]
2121 pub resources: Option<ChildResourceUsage>,
2122}
2123
2124impl SupervisorEntry {
2125 #[must_use]
2127 pub fn new(
2128 module_id: impl Into<String>,
2129 state: impl Into<String>,
2130 enabled: bool,
2131 live: bool,
2132 health: SupervisorHealthStatus,
2133 ) -> Self {
2134 Self {
2135 module_id: module_id.into(),
2136 state: state.into(),
2137 enabled,
2138 live,
2139 protocol: ModuleProtocol::default(),
2140 launch_nonce_env: None,
2141 health,
2142 pending_reload: None,
2143 last_probe_ms: None,
2144 last_exit_code: None,
2145 last_exit_signal: None,
2146 last_exit_ms: None,
2147 last_exit_kind: None,
2148 restart_count: None,
2149 max_restarts: None,
2150 lifetime_restarts: None,
2151 spawn_generation: None,
2152 restart_window_secs: None,
2153 drain_timeout_ms: None,
2154 restart_backoff_ms: None,
2155 restart_max_backoff_ms: None,
2156 resources: None,
2157 }
2158 }
2159
2160 #[must_use]
2162 pub fn with_protocol(mut self, protocol: ModuleProtocol) -> Self {
2163 self.protocol = protocol;
2164 self
2165 }
2166
2167 #[must_use]
2169 pub fn with_launch_nonce_env(mut self, launch_nonce_env: Option<bool>) -> Self {
2170 self.launch_nonce_env = launch_nonce_env;
2171 self
2172 }
2173
2174 #[must_use]
2176 pub fn with_pending_reload(mut self, pending_reload: Option<PendingReloadVerdict>) -> Self {
2177 self.pending_reload = pending_reload;
2178 self
2179 }
2180
2181 #[must_use]
2183 pub fn with_last_probe_ms(mut self, last_probe_ms: Option<u64>) -> Self {
2184 self.last_probe_ms = last_probe_ms;
2185 self
2186 }
2187
2188 #[must_use]
2190 pub fn with_last_exit_code(mut self, last_exit_code: Option<i32>) -> Self {
2191 self.last_exit_code = last_exit_code;
2192 self
2193 }
2194
2195 #[must_use]
2197 pub fn with_last_exit_signal(mut self, last_exit_signal: Option<i32>) -> Self {
2198 self.last_exit_signal = last_exit_signal;
2199 self
2200 }
2201
2202 #[must_use]
2204 pub fn with_last_exit_ms(mut self, last_exit_ms: Option<u64>) -> Self {
2205 self.last_exit_ms = last_exit_ms;
2206 self
2207 }
2208
2209 #[must_use]
2211 pub fn with_last_exit_kind(mut self, last_exit_kind: Option<TerminalExitKind>) -> Self {
2212 self.last_exit_kind = last_exit_kind;
2213 self
2214 }
2215
2216 #[must_use]
2218 pub fn with_restart_count(mut self, restart_count: Option<u32>) -> Self {
2219 self.restart_count = restart_count;
2220 self
2221 }
2222
2223 #[must_use]
2225 pub fn with_max_restarts(mut self, max_restarts: Option<u32>) -> Self {
2226 self.max_restarts = max_restarts;
2227 self
2228 }
2229
2230 #[must_use]
2232 pub fn with_lifetime_restarts(mut self, lifetime_restarts: Option<u32>) -> Self {
2233 self.lifetime_restarts = lifetime_restarts;
2234 self
2235 }
2236
2237 #[must_use]
2239 pub fn with_spawn_generation(mut self, spawn_generation: Option<u64>) -> Self {
2240 self.spawn_generation = spawn_generation;
2241 self
2242 }
2243
2244 #[must_use]
2246 pub fn with_restart_window_secs(mut self, restart_window_secs: Option<u64>) -> Self {
2247 self.restart_window_secs = restart_window_secs;
2248 self
2249 }
2250
2251 #[must_use]
2253 pub fn with_drain_timeout_ms(mut self, drain_timeout_ms: Option<u64>) -> Self {
2254 self.drain_timeout_ms = drain_timeout_ms;
2255 self
2256 }
2257
2258 #[must_use]
2260 pub fn with_restart_backoff_ms(mut self, restart_backoff_ms: Option<u64>) -> Self {
2261 self.restart_backoff_ms = restart_backoff_ms;
2262 self
2263 }
2264
2265 #[must_use]
2267 pub fn with_restart_max_backoff_ms(mut self, restart_max_backoff_ms: Option<u64>) -> Self {
2268 self.restart_max_backoff_ms = restart_max_backoff_ms;
2269 self
2270 }
2271
2272 #[must_use]
2274 pub fn with_resources(mut self, resources: Option<ChildResourceUsage>) -> Self {
2275 self.resources = resources;
2276 self
2277 }
2278}
2279
2280#[derive(Debug, Clone, PartialEq)]
2283pub enum ChildResourceUsage {
2284 Measured(ChildResourceReading),
2285 Unavailable {
2286 reason: ChildResourceUnavailableReason,
2287 },
2288 Unknown {
2291 tag: String,
2292 body: OrderedJsonObject,
2293 },
2294}
2295
2296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2298pub struct ChildResourceReading {
2299 pub memory_bytes: u64,
2302 pub memory_kind: ChildMemoryKind,
2303 #[serde(default, skip_serializing_if = "Option::is_none")]
2306 pub swap_bytes: Option<u64>,
2307 pub cpu_user_ms: u64,
2311 pub cpu_system_ms: u64,
2314}
2315
2316open_string_enum! {
2317 ChildMemoryKind {
2319 PhysFootprint => "phys_footprint",
2323 ResidentSet => "resident_set",
2326 }
2327}
2328
2329open_string_enum! {
2330 ChildResourceUnavailableReason {
2332 NotRunning => "not_running",
2334 UnsupportedPlatform => "unsupported_platform",
2336 Unreadable => "unreadable",
2339 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
2342 }
2343}
2344
2345#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2346#[serde(rename_all = "snake_case")]
2347pub enum SupervisorHealthStatus {
2348 Ok,
2349 Degraded,
2350 Failing,
2351 Unresponsive,
2352 Unknown,
2353}
2354
2355#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2356pub struct SupervisorHealthEntry {
2357 pub module_id: String,
2358 pub status: SupervisorHealthStatus,
2359 #[serde(default, skip_serializing_if = "Option::is_none")]
2365 pub detail: Option<String>,
2366 #[serde(default, skip_serializing_if = "Option::is_none")]
2371 pub metrics: Option<serde_json::Value>,
2372 pub consecutive_failures: u32,
2373 #[serde(default)]
2376 pub late_answer_count: u64,
2377 #[serde(default, skip_serializing_if = "Option::is_none")]
2379 pub last_late_answer_latency_ms: Option<u64>,
2380 #[serde(default)]
2385 pub last_action: Option<String>,
2386 #[serde(default)]
2389 pub last_action_ms: Option<u64>,
2390 #[serde(default, skip_serializing_if = "Option::is_none")]
2403 pub last_probe_ms: Option<u64>,
2404}
2405
2406#[cfg(test)]
2407mod tests {
2408 use super::*;
2409 use subc_protocol::{BindIdentity, RouteTarget};
2410
2411 #[test]
2412 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
2413 let entry = TerminalEntry {
2414 daemon_incarnation: Some("daemon-before-restart".into()),
2415 exit_code: Some(1),
2416 exit_signal: None,
2417 at_ms: 1_700_000_000_123,
2418 disposition: TerminalDisposition::Restarting,
2419 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
2420 disposition_detail: None,
2421 };
2422 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
2423 assert_eq!(
2424 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
2425 ["exit_kind"],
2426 "deliberate_severance"
2427 );
2428
2429 #[derive(serde::Deserialize)]
2430 struct LegacyTerminalEntry {
2431 exit_code: Option<i32>,
2432 exit_signal: Option<i32>,
2433 at_ms: u64,
2434 disposition: TerminalDisposition,
2435 }
2436
2437 let decoded: LegacyTerminalEntry =
2438 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
2439 assert_eq!(decoded.exit_code, Some(1));
2440 assert_eq!(decoded.exit_signal, None);
2441 assert_eq!(decoded.at_ms, 1_700_000_000_123);
2442 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
2443
2444 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
2445 let future: TerminalEntry =
2446 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
2447 assert_eq!(
2448 future.exit_kind,
2449 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
2450 );
2451 }
2452
2453 #[test]
2454 fn terminal_incarnation_is_optional_for_older_daemons() {
2455 let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
2456 "at_ms": 123,
2457 "disposition": "stopped"
2458 }))
2459 .unwrap();
2460 let encoded = serde_json::to_value(&entry).unwrap();
2461 assert_eq!(
2462 (entry.daemon_incarnation, encoded.get("daemon_incarnation")),
2463 (None, None)
2464 );
2465 }
2466
2467 #[test]
2468 fn route_poll_uses_kind_field() {
2469 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
2470 route_channel: 7,
2471 route_epoch: 11,
2472 kind: PollKind::Status,
2473 })
2474 .unwrap();
2475
2476 assert_eq!(body["op"], "route.poll");
2477 assert_eq!(body["route_epoch"], 11);
2478 assert_eq!(body["kind"], "status");
2479 assert!(body.get("op").is_some());
2480 }
2481
2482 #[test]
2483 fn route_open_is_internally_tagged() {
2484 let request = ClientControlRequest::RouteOpen {
2485 target: RouteTarget::ToolProvider {
2486 module_id: "aft".to_string(),
2487 },
2488 identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
2489 consumer_identity: None,
2490 consumer_capabilities: None,
2491 role_versions: None,
2492 admission_facts: None,
2493 scope: None,
2494 };
2495
2496 let body = serde_json::to_value(request).unwrap();
2497 assert_eq!(body["op"], "route.open");
2498 assert_eq!(body["target"]["kind"], "tool_provider");
2499 assert!(body.get("consumer_identity").is_none());
2500 assert!(body.get("consumer_capabilities").is_none());
2501 assert!(body.get("role_versions").is_none());
2502 }
2503
2504 #[test]
2505 fn route_open_without_optional_fields_still_decodes() {
2506 let body = serde_json::json!({
2507 "op": "route.open",
2508 "target": { "kind": "tool_provider", "module_id": "aft" },
2509 "identity": {
2510 "project_root": "/tmp/project",
2511 "harness": "opencode",
2512 "session": "session-1"
2513 }
2514 });
2515
2516 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
2517 let ClientControlRequest::RouteOpen {
2518 consumer_identity,
2519 consumer_capabilities,
2520 role_versions,
2521 admission_facts,
2522 ..
2523 } = decoded
2524 else {
2525 panic!("decoded wrong request variant");
2526 };
2527 assert_eq!(consumer_identity, None);
2528 assert_eq!(consumer_capabilities, None);
2529 assert_eq!(role_versions, None);
2530 assert_eq!(admission_facts, None);
2531 }
2532
2533 #[test]
2534 fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
2535 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
2536 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
2537 match decoded {
2538 ClientControlPush::RouteClosed {
2539 excluded_subscriptions,
2540 terminal,
2541 ..
2542 } => {
2543 assert_eq!(excluded_subscriptions, 0);
2544 assert_eq!(terminal, None);
2545 }
2546 other => panic!("unexpected push: {other:?}"),
2547 }
2548 assert!(!serde_json::to_string(&decoded)
2549 .unwrap()
2550 .contains("terminal"));
2551 }
2552
2553 #[test]
2554 fn old_route_closed_decoder_ignores_new_terminal_field() {
2555 #[derive(serde::Deserialize)]
2556 #[serde(tag = "op")]
2557 enum LegacyClientControlPush {
2558 #[serde(rename = "route.closed")]
2559 RouteClosed {
2560 module_id: String,
2561 reason: RouteCloseReason,
2562 drained: bool,
2563 abandoned: u32,
2564 },
2565 }
2566
2567 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
2568 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
2569 match decoded {
2570 LegacyClientControlPush::RouteClosed {
2571 module_id,
2572 reason,
2573 drained,
2574 abandoned,
2575 } => {
2576 assert_eq!(module_id, "aft-tools");
2577 assert_eq!(reason, RouteCloseReason::Crash);
2578 assert!(!drained);
2579 assert_eq!(abandoned, 0);
2580 }
2581 }
2582 }
2583
2584 #[test]
2585 fn supervisor_routes_is_a_control_plane_request() {
2586 let body = serde_json::json!({
2587 "op": "supervisor.routes",
2588 "module_id": "aft"
2589 });
2590
2591 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
2592 assert_eq!(serde_json::to_value(request).unwrap(), body);
2593 }
2594
2595 #[test]
2596 fn diagnostic_string_enums_retain_unknown_wire_values() {
2597 let reason: RunningImageUnavailableReason =
2598 serde_json::from_str("\"future_reason\"").unwrap();
2599 let disposition: TerminalDisposition =
2600 serde_json::from_str("\"future_disposition\"").unwrap();
2601
2602 assert_eq!(
2603 reason,
2604 RunningImageUnavailableReason::Unknown("future_reason".to_string())
2605 );
2606 assert_eq!(
2607 disposition,
2608 TerminalDisposition::Unknown("future_disposition".to_string())
2609 );
2610 }
2611
2612 #[test]
2613 fn diagnostic_string_enums_preserve_existing_wire_names() {
2614 let names = [
2615 (RunningImageUnavailableReason::NotRunning, "not_running"),
2616 (
2617 RunningImageUnavailableReason::UnsupportedPlatform,
2618 "unsupported_platform",
2619 ),
2620 (
2621 RunningImageUnavailableReason::RunningExecutableUnreadable,
2622 "running_executable_unreadable",
2623 ),
2624 (
2625 RunningImageUnavailableReason::SpawnedPathUnreadable,
2626 "spawned_path_unreadable",
2627 ),
2628 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
2629 (
2630 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
2631 "process_identity_unconfirmed",
2632 ),
2633 ];
2634 for (value, expected) in names {
2635 let wire = serde_json::to_string(&value).unwrap();
2636 assert_eq!(wire, format!("\"{expected}\""));
2637 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
2638 assert_eq!(decoded, value);
2639 }
2640
2641 for (value, expected) in [
2642 (TerminalDisposition::Stopped, "stopped"),
2643 (TerminalDisposition::Disabled, "disabled"),
2644 (TerminalDisposition::Failed, "failed"),
2645 (TerminalDisposition::Restarting, "restarting"),
2646 (TerminalDisposition::DaemonShutdown, "daemon_shutdown"),
2647 ] {
2648 let wire = serde_json::to_string(&value).unwrap();
2649 assert_eq!(wire, format!("\"{expected}\""));
2650 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
2651 assert_eq!(decoded, value);
2652 }
2653 }
2654
2655 #[test]
2656 fn diagnostic_string_enums_reject_non_string_bodies() {
2657 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
2658 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
2659 }
2660
2661 #[test]
2662 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
2663 let body = serde_json::json!({
2664 "op": "supervisor.provenance",
2665 "daemon": {
2666 "daemon_build": {},
2667 "daemon_observed": {
2668 "running_image": {
2669 "status": "unavailable",
2670 "reason": "not_running"
2671 }
2672 }
2673 },
2674 "modules": [
2675 {
2676 "module_id": "future",
2677 "module_declared": { "status": "unverifiable" },
2678 "daemon_observed": {
2679 "running_image": {
2680 "status": "unavailable",
2681 "reason": "future_reason"
2682 }
2683 }
2684 },
2685 {
2686 "module_id": "healthy-a",
2687 "module_declared": { "status": "unverifiable" },
2688 "daemon_observed": {
2689 "running_image": {
2690 "status": "match",
2691 "evidence": {
2692 "method": "linux_proc_sha256",
2693 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2694 }
2695 }
2696 }
2697 },
2698 {
2699 "module_id": "healthy-b",
2700 "module_declared": { "status": "unverifiable" },
2701 "daemon_observed": {
2702 "running_image": {
2703 "status": "unavailable",
2704 "reason": "unsupported_platform"
2705 }
2706 }
2707 }
2708 ]
2709 });
2710
2711 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
2712 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
2713 panic!("decoded wrong response variant");
2714 };
2715 assert_eq!(modules.len(), 3);
2716 assert_eq!(modules[0].module_id, "future");
2717 assert_eq!(
2718 modules[0].daemon_observed.running_image,
2719 RunningImageAgreement::Unavailable {
2720 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
2721 }
2722 );
2723 assert_eq!(modules[1].module_id, "healthy-a");
2724 assert_eq!(modules[2].module_id, "healthy-b");
2725 }
2726
2727 #[test]
2728 fn tagged_unknown_values_retain_tag_and_body() {
2729 macro_rules! assert_unknown_round_trip {
2730 ($ty:ident, $field:literal, $value:expr) => {
2731 let value = $value;
2732 let wire = serde_json::to_string(&value).unwrap();
2733 let decoded: $ty = serde_json::from_str(&wire).unwrap();
2734 match decoded {
2735 $ty::Unknown { tag, body } => {
2736 assert_eq!(tag, value[$field].as_str().unwrap());
2737 assert_eq!(serde_json::to_value(&body).unwrap(), value);
2738 }
2739 _ => panic!("decoded known variant"),
2740 }
2741 };
2742 }
2743
2744 assert_unknown_round_trip!(
2745 ModuleDeclaredProvenance,
2746 "status",
2747 serde_json::json!({"status": "future", "build": {"version": 7}})
2748 );
2749 assert_unknown_round_trip!(
2750 RunningImageAgreement,
2751 "status",
2752 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
2753 );
2754 assert_unknown_round_trip!(
2755 RunningImageEvidence,
2756 "method",
2757 serde_json::json!({"method": "future", "digest": "abc"})
2758 );
2759 assert_unknown_round_trip!(
2760 SupervisorRouteConsumer,
2761 "kind",
2762 serde_json::json!({"kind": "future", "module_id": "m"})
2763 );
2764 assert_unknown_round_trip!(
2765 StderrCaptureState,
2766 "state",
2767 serde_json::json!({"state": "future", "reason": "because"})
2768 );
2769 assert_unknown_round_trip!(
2770 StderrTailEntry,
2771 "kind",
2772 serde_json::json!({"kind": "future", "text": "line"})
2773 );
2774 assert_unknown_round_trip!(
2775 ChildResourceUsage,
2776 "status",
2777 serde_json::json!({"status": "future", "memory_bytes": 1})
2778 );
2779 }
2780
2781 #[test]
2782 fn child_resource_usage_round_trips_both_known_states() {
2783 let measured = ChildResourceUsage::Measured(ChildResourceReading {
2784 memory_bytes: 0,
2785 memory_kind: ChildMemoryKind::ResidentSet,
2786 swap_bytes: Some(0),
2787 cpu_user_ms: 0,
2788 cpu_system_ms: 0,
2789 });
2790 let wire = serde_json::to_value(&measured).unwrap();
2791 assert_eq!(
2792 wire,
2793 serde_json::json!({
2794 "status": "measured",
2795 "memory_bytes": 0,
2796 "memory_kind": "resident_set",
2797 "swap_bytes": 0,
2798 "cpu_user_ms": 0,
2799 "cpu_system_ms": 0
2800 })
2801 );
2802 assert_eq!(
2803 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2804 measured
2805 );
2806
2807 let unavailable = ChildResourceUsage::Unavailable {
2808 reason: ChildResourceUnavailableReason::NotRunning,
2809 };
2810 let wire = serde_json::to_value(&unavailable).unwrap();
2811 assert_eq!(
2812 wire,
2813 serde_json::json!({"status": "unavailable", "reason": "not_running"})
2814 );
2815 assert_eq!(
2816 serde_json::from_value::<ChildResourceUsage>(wire).unwrap(),
2817 unavailable
2818 );
2819 }
2820
2821 #[test]
2822 fn a_stderr_line_decodes_with_and_without_its_capture_time() {
2823 let stamped: StderrTailEntry = serde_json::from_str(
2827 r#"{"kind":"line","text":"boom","truncated":true,"at_ms":1789801440685}"#,
2828 )
2829 .unwrap();
2830 assert_eq!(
2831 stamped,
2832 StderrTailEntry::Line {
2833 text: "boom".to_string(),
2834 truncated: true,
2835 at_ms: Some(1_789_801_440_685),
2836 }
2837 );
2838 let unstamped: StderrTailEntry =
2839 serde_json::from_str(r#"{"kind":"line","text":"boom"}"#).unwrap();
2840 assert_eq!(
2841 unstamped,
2842 StderrTailEntry::Line {
2843 text: "boom".to_string(),
2844 truncated: false,
2845 at_ms: None,
2846 }
2847 );
2848 assert_eq!(
2851 serde_json::to_string(&unstamped).unwrap(),
2852 r#"{"kind":"line","text":"boom"}"#
2853 );
2854 assert_eq!(
2855 serde_json::to_value(&stamped).unwrap()["at_ms"],
2856 serde_json::json!(1_789_801_440_685u64)
2857 );
2858 }
2859
2860 #[test]
2861 fn tagged_unknown_values_round_trip_the_original_json() {
2862 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
2863 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2864 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2865 }
2866
2867 #[test]
2868 fn tagged_unknown_values_round_trip_trailing_tag() {
2869 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2870 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2871 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2872
2873 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2874 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2875 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2876 }
2877
2878 #[test]
2879 fn tagged_unknown_values_round_trip_middle_tag() {
2880 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2881 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2882 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2883
2884 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2885 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2886 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2887 }
2888
2889 #[test]
2890 fn tagged_unknown_values_round_trip_deep_payload() {
2891 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2892 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2893 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2894
2895 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2896 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2897 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2898 }
2899
2900 #[test]
2901 fn tagged_unknown_values_reject_non_object_bodies() {
2902 for wire in ["42", r#""future""#, "[]"] {
2903 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2904 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2905 }
2906 }
2907
2908 #[test]
2909 fn duplicate_discriminators_reject_without_panicking() {
2910 assert_eq!(
2911 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2912 .unwrap(),
2913 ModuleDeclaredProvenance::Unverifiable
2914 );
2915 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2916 .unwrap()
2917 {
2918 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2919 _ => panic!("future discriminator decoded as a known variant"),
2920 }
2921
2922 let wires = [
2923 r#"{"status":"reported","status":"unverifiable"}"#,
2924 r#"{"status":"unverifiable","status":"reported"}"#,
2925 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2926 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2927 ];
2928
2929 for wire in wires {
2930 let result =
2931 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2932 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2933 assert!(
2934 result.unwrap().is_err(),
2935 "duplicate discriminator decoded: {wire}"
2936 );
2937 }
2938
2939 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2940 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2941 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2942 assert!(
2943 result.unwrap().is_err(),
2944 "duplicate discriminator decoded: {wire}"
2945 );
2946 }
2947
2948 #[test]
2949 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2950 let known_wire =
2951 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2952 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2953 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2954
2955 for wire in [
2956 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2957 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2958 ] {
2959 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2960 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2961 }
2962
2963 for wire in [
2964 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2965 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2966 ] {
2967 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2968 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2969 }
2970
2971 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2972 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2973 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2974
2975 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2976 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2977 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2978 }
2979
2980 #[test]
2981 fn tagged_unknown_member_does_not_discard_known_siblings() {
2982 let body = serde_json::json!({
2983 "modules": [{
2984 "module_id": "target",
2985 "routes": [
2986 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2987 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2988 ]
2989 }]
2990 });
2991 let decoded: ClientControlResponse = serde_json::from_value(
2992 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2993 )
2994 .unwrap();
2995 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2996 panic!("decoded wrong response variant");
2997 };
2998 assert_eq!(modules[0].routes.len(), 2);
2999 assert_eq!(
3000 modules[0].routes[1].consumer,
3001 SupervisorRouteConsumer::Direct { connection_id: 7 }
3002 );
3003 }
3004}
3005
3006#[cfg(test)]
3007mod launch_nonce_redaction_tests {
3008 use super::*;
3009
3010 const NONCE: &str = "nonce-f00dfeed1234abcd";
3011
3012 fn identity() -> ConsumerIdentity {
3013 ConsumerIdentity {
3014 module_id: "wernicke".to_string(),
3015 launch_nonce: NONCE.to_string(),
3016 }
3017 }
3018
3019 #[test]
3020 fn consumer_identity_debug_names_the_module_and_never_the_nonce() {
3021 let printed = format!("{:?}", identity());
3022 assert!(printed.contains("wernicke"), "{printed}");
3023 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
3024 }
3025
3026 #[test]
3027 fn route_open_request_debug_never_prints_the_nonce() {
3028 let request = ClientControlRequest::RouteOpen {
3029 target: subc_protocol::RouteTarget::ToolProvider {
3030 module_id: "broca".to_string(),
3031 },
3032 identity: subc_protocol::BindIdentity::new(
3033 PathBuf::from("/tmp/project"),
3034 "test".to_string(),
3035 "session".to_string(),
3036 ),
3037 consumer_identity: Some(identity()),
3038 consumer_capabilities: None,
3039 role_versions: None,
3040 admission_facts: None,
3041 scope: None,
3042 };
3043 let printed = format!("{request:?}");
3044 assert!(!printed.contains(NONCE), "launch nonce printed: {printed}");
3045 }
3046}