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