1#![forbid(unsafe_code)]
9
10use std::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 session::HealthStatus,
20 BindIdentity, RouteTarget,
21};
22
23macro_rules! open_string_enum {
24 (
25 $(#[$meta:meta])*
26 $name:ident {
27 $( $variant:ident => $wire_name:literal ),+ $(,)?
28 }
29 ) => {
30 $(#[$meta])*
31 #[derive(Debug, Clone, PartialEq, Eq)]
32 pub enum $name {
33 $( $variant, )+
34 Unknown(String),
35 }
36
37 impl $name {
38 fn wire_name(&self) -> &str {
39 match self {
40 $( Self::$variant => $wire_name, )+
41 Self::Unknown(value) => value,
42 }
43 }
44 }
45
46 impl Serialize for $name {
47 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
48 where
49 S: serde::Serializer,
50 {
51 serializer.serialize_str(self.wire_name())
52 }
53 }
54
55 impl<'de> Deserialize<'de> for $name {
56 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57 where
58 D: serde::Deserializer<'de>,
59 {
60 let value = String::deserialize(deserializer)?;
61 Ok(match value.as_str() {
62 $( $wire_name => Self::$variant, )+
63 _ => Self::Unknown(value),
64 })
65 }
66 }
67 };
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
72pub struct ConsumerIdentity {
73 pub module_id: String,
74 pub launch_nonce: String,
75}
76
77pub mod ops {
88 pub const SERVER: &str = "server.";
89 pub const CATALOG: &str = "catalog.";
90 pub const ROUTE: &str = "route.";
91 pub const SUPERVISOR: &str = "supervisor.";
92 pub const CONFIG: &str = "config.";
93
94 pub const SERVER_DESCRIBE: &str = "server.describe";
95 pub const CATALOG_LIST: &str = "catalog.list";
96 pub const ROUTE_OPEN: &str = "route.open";
97 pub const ROUTE_POLL: &str = "route.poll";
98 pub const ROUTE_CLOSING: &str = "route.closing";
99 pub const ROUTE_CLOSED: &str = "route.closed";
100 pub const SUPERVISOR_LIST: &str = "supervisor.list";
101 pub const SUPERVISOR_RESTART: &str = "supervisor.restart";
102 pub const SUPERVISOR_RELOAD: &str = "supervisor.reload";
103 pub const SUPERVISOR_RESCAN: &str = "supervisor.rescan";
104 pub const SUPERVISOR_RELEASE_RESERVED: &str = "supervisor.release_reserved";
105 pub const SUPERVISOR_SET_ENABLED: &str = "supervisor.set_enabled";
106 pub const SUPERVISOR_HEALTH_PROBE: &str = "supervisor.health_probe";
107 pub const SUPERVISOR_HEALTH: &str = "supervisor.health";
108 pub const SUPERVISOR_STDERR_TAIL: &str = "supervisor.stderr_tail";
109 pub const SUPERVISOR_TERMINALS: &str = "supervisor.terminals";
110 pub const SUPERVISOR_ROUTES: &str = "supervisor.routes";
111 pub const SUPERVISOR_PROVENANCE: &str = "supervisor.provenance";
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
116#[serde(tag = "op")]
117#[allow(clippy::large_enum_variant)]
120pub enum ClientControlRequest {
121 #[serde(rename = "server.describe")]
122 ServerDescribe {},
123 #[serde(rename = "catalog.list")]
124 CatalogList {
125 #[serde(default)]
130 module_id: Option<String>,
131 },
132 #[serde(rename = "route.open")]
133 RouteOpen {
134 target: RouteTarget,
135 identity: BindIdentity,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
145 consumer_identity: Option<ConsumerIdentity>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
154 consumer_capabilities: Option<Vec<String>>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
157 admission_facts: Option<serde_json::Value>,
158 },
159 #[serde(rename = "route.poll")]
160 RoutePoll {
161 route_channel: u16,
162 route_epoch: u32,
163 kind: PollKind,
164 },
165 #[serde(rename = "supervisor.list")]
166 SupervisorList {},
167 #[serde(rename = "supervisor.restart")]
168 SupervisorRestart {
169 module_id: String,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
178 drain_timeout_ms: Option<u64>,
179 },
180 #[serde(rename = "supervisor.reload")]
181 SupervisorReload { module_id: String },
182 #[serde(rename = "supervisor.rescan")]
183 SupervisorRescan {
184 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
204 preview: bool,
205 },
206 #[serde(rename = "supervisor.release_reserved")]
210 SupervisorReleaseReserved { module_id: String },
211 #[serde(rename = "supervisor.set_enabled")]
212 SupervisorSetEnabled { module_id: String, enabled: bool },
213 #[serde(rename = "supervisor.health_probe")]
214 SupervisorHealthProbe { module_id: String },
215 #[serde(rename = "supervisor.health")]
216 SupervisorHealth {},
217 #[serde(rename = "supervisor.routes")]
230 SupervisorRoutes {
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 module_id: Option<String>,
233 },
234 #[serde(rename = "supervisor.provenance")]
237 SupervisorProvenance {
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 module_id: Option<String>,
240 },
241 #[serde(rename = "supervisor.stderr_tail")]
249 SupervisorStderrTail {
250 module_id: String,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 max_lines: Option<u32>,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
254 max_bytes: Option<u32>,
255 },
256 #[serde(rename = "supervisor.terminals")]
269 SupervisorTerminals { module_id: String },
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
274#[serde(tag = "op")]
275pub enum ClientControlResponse {
276 #[serde(rename = "server.describe")]
277 ServerDescribe {
278 protocol_ver: u8,
279 subc_ops: Vec<String>,
280 capabilities: Vec<String>,
281 connected_clients: u64,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 counters: Option<serde_json::Value>,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
291 build_git_sha: Option<String>,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
298 build_lock_digest: Option<String>,
299 #[serde(default, skip_serializing_if = "Vec::is_empty")]
303 capability_requirements: Vec<CapabilityRequirementStatus>,
304 },
305 #[serde(rename = "catalog.list")]
306 CatalogList {
307 generation: u64,
308 modules: Vec<CatalogEntry>,
309 subc_ops: Vec<String>,
310 },
311 #[serde(rename = "route.open")]
312 RouteOpen {
313 route_channel: u16,
314 route_epoch: u32,
315 },
316 #[serde(rename = "route.poll")]
317 RoutePoll {
318 route_channel: u16,
319 route_epoch: u32,
320 status: Option<String>,
321 live: Option<bool>,
322 },
323 #[serde(rename = "supervisor.list")]
324 SupervisorList {
325 generation: u64,
326 modules: Vec<SupervisorEntry>,
327 },
328 #[serde(rename = "supervisor.ack")]
329 SupervisorAck { module_id: String, applied: bool },
330 #[serde(rename = "supervisor.rescan")]
331 SupervisorRescan {
332 #[serde(flatten)]
333 result: SupervisorRescanResult,
334 },
335 #[serde(rename = "supervisor.health_probe")]
336 SupervisorHealthProbe {
337 module_id: String,
338 status: HealthStatus,
339 #[serde(default, skip_serializing_if = "Option::is_none")]
340 detail: Option<String>,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 metrics: Option<serde_json::Value>,
343 },
344 #[serde(rename = "supervisor.health")]
345 SupervisorHealth {
346 generation: u64,
347 modules: Vec<SupervisorHealthEntry>,
348 },
349 #[serde(rename = "supervisor.routes")]
350 SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
351 #[serde(rename = "supervisor.provenance")]
352 SupervisorProvenance {
353 daemon: SupervisorDaemonProvenance,
354 modules: Vec<SupervisorModuleProvenance>,
355 },
356 #[serde(rename = "supervisor.stderr_tail")]
357 SupervisorStderrTail {
358 module_id: String,
359 #[serde(flatten)]
360 tail: StderrTail,
361 },
362 #[serde(rename = "supervisor.terminals")]
363 SupervisorTerminals {
364 module_id: String,
365 #[serde(flatten)]
366 terminals: TerminalHistory,
367 },
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
375#[serde(tag = "op")]
376pub enum ClientControlPush {
377 #[serde(rename = "route.closing")]
378 RouteClosing {
379 module_id: String,
380 reason: RouteCloseReason,
381 },
382 #[serde(rename = "route.closed")]
383 RouteClosed {
384 module_id: String,
385 reason: RouteCloseReason,
386 drained: bool,
388 abandoned: u32,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
397 terminal: Option<bool>,
398 },
399}
400
401#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
403#[serde(rename_all = "snake_case")]
404pub enum RouteCloseReason {
405 Reload,
406 Restart,
407 Disable,
408 Crash,
409 CapabilityDenied,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
416pub struct StderrTail {
417 pub capture: StderrCaptureState,
418 pub entries: Vec<StderrTailEntry>,
419 #[serde(default, skip_serializing_if = "is_zero_u64")]
428 pub dropped_lines: u64,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
433pub struct SupervisorRouteModule {
434 pub module_id: String,
435 pub routes: Vec<SupervisorRoute>,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
440pub struct SupervisorRoute {
441 pub consumer: SupervisorRouteConsumer,
442 pub age_ms: u64,
444 pub draining: bool,
447 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub drain_reason: Option<RouteCloseReason>,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
458pub struct SupervisorModuleProvenance {
459 pub module_id: String,
460 pub module_declared: ModuleDeclaredProvenance,
461 pub daemon_observed: SupervisorObservedProcess,
462}
463
464#[derive(Debug, Clone, PartialEq)]
466pub enum ModuleDeclaredProvenance {
467 Reported {
468 build: ManifestProvenance,
469 },
470 Unverifiable,
471 Unknown {
474 tag: String,
475 body: OrderedJsonObject,
476 },
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
484pub struct SupervisorObservedProcess {
485 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub pid: Option<u32>,
487 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub spawned_at_ms: Option<u64>,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub spawned_from: Option<PathBuf>,
491 pub running_image: RunningImageAgreement,
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
496pub struct SupervisorDaemonProvenance {
497 pub daemon_build: DaemonBuildProvenance,
498 pub daemon_observed: DaemonObservedProcess,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
503pub struct DaemonBuildProvenance {
504 #[serde(default, skip_serializing_if = "Option::is_none")]
505 pub build_git_sha: Option<String>,
506 #[serde(default, skip_serializing_if = "Option::is_none")]
507 pub build_lock_digest: Option<String>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
512pub struct DaemonObservedProcess {
513 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub pid: Option<u32>,
515 #[serde(default, skip_serializing_if = "Option::is_none")]
516 pub started_at_ms: Option<u64>,
517 pub running_image: RunningImageAgreement,
518}
519
520#[derive(Debug, Clone, PartialEq)]
522pub enum RunningImageAgreement {
523 Match {
524 evidence: RunningImageEvidence,
525 },
526 Mismatch {
527 running: RunningImageEvidence,
528 disk: RunningImageEvidence,
529 },
530 Unavailable {
531 reason: RunningImageUnavailableReason,
532 },
533 Unknown {
536 tag: String,
537 body: OrderedJsonObject,
538 },
539}
540
541#[derive(Debug, Clone, PartialEq)]
543pub enum RunningImageEvidence {
544 LinuxProcSha256 {
545 digest: String,
546 },
547 MacosSpawnInode {
548 device: u64,
549 inode: u64,
550 },
551 Unknown {
554 tag: String,
555 body: OrderedJsonObject,
556 },
557}
558
559open_string_enum! {
560 RunningImageUnavailableReason {
562 NotRunning => "not_running",
563 UnsupportedPlatform => "unsupported_platform",
564 RunningExecutableUnreadable => "running_executable_unreadable",
565 SpawnedPathUnreadable => "spawned_path_unreadable",
566 HashFailed => "hash_failed",
567 ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
568 }
569}
570
571#[derive(Debug, Clone, PartialEq)]
577pub enum SupervisorRouteConsumer {
578 Reserved {
579 module_id: String,
580 },
581 Direct {
582 connection_id: u64,
583 },
584 Unknown {
587 tag: String,
588 body: OrderedJsonObject,
589 },
590}
591
592#[derive(Debug, Clone, PartialEq)]
599pub enum StderrCaptureState {
600 Captured,
603 Incomplete { reason: String },
605 NotCaptured { reason: String },
607 Unknown {
610 tag: String,
611 body: OrderedJsonObject,
612 },
613}
614
615#[derive(Debug, Clone, PartialEq)]
616pub enum StderrTailEntry {
617 Line {
618 text: String,
619 truncated: bool,
624 },
625 ProcessStart,
630 Unknown {
633 tag: String,
634 body: OrderedJsonObject,
635 },
636}
637
638#[derive(Debug, Serialize, Deserialize)]
639#[serde(tag = "status", rename_all = "snake_case")]
640enum ModuleDeclaredProvenanceWire {
641 Reported { build: ManifestProvenance },
642 Unverifiable,
643}
644
645#[derive(Debug, Serialize, Deserialize)]
646#[serde(tag = "status", rename_all = "snake_case")]
647enum RunningImageAgreementWire {
648 Match {
649 evidence: RunningImageEvidence,
650 },
651 Mismatch {
652 running: RunningImageEvidence,
653 disk: RunningImageEvidence,
654 },
655 Unavailable {
656 reason: RunningImageUnavailableReason,
657 },
658}
659
660#[derive(Debug, Serialize, Deserialize)]
661#[serde(tag = "method", rename_all = "snake_case")]
662enum RunningImageEvidenceWire {
663 LinuxProcSha256 { digest: String },
664 MacosSpawnInode { device: u64, inode: u64 },
665}
666
667#[derive(Debug, Serialize, Deserialize)]
668#[serde(tag = "kind", rename_all = "snake_case")]
669enum SupervisorRouteConsumerWire {
670 Reserved { module_id: String },
671 Direct { connection_id: u64 },
672}
673
674#[derive(Debug, Serialize, Deserialize)]
675#[serde(tag = "state", rename_all = "snake_case")]
676enum StderrCaptureStateWire {
677 Captured,
678 Incomplete { reason: String },
679 NotCaptured { reason: String },
680}
681
682#[derive(Debug, Serialize, Deserialize)]
683#[serde(tag = "kind", rename_all = "snake_case")]
684enum StderrTailEntryWire {
685 Line {
686 text: String,
687 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
688 truncated: bool,
689 },
690 ProcessStart,
691}
692
693#[derive(Debug, Clone, PartialEq)]
695pub enum OrderedJsonValue {
696 Null,
697 Bool(bool),
698 Number(serde_json::Number),
699 String(String),
700 Array(Vec<Self>),
701 Object(OrderedJsonObject),
702}
703
704#[derive(Debug, Clone, PartialEq)]
706pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
707
708impl OrderedJsonObject {
709 pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
711 &self.0
712 }
713
714 fn into_value(self) -> serde_json::Value {
715 serde_json::Value::Object(
716 self.0
717 .into_iter()
718 .map(|(key, value)| (key, value.into_value()))
719 .collect(),
720 )
721 }
722}
723
724impl OrderedJsonValue {
725 fn into_value(self) -> serde_json::Value {
726 match self {
727 Self::Null => serde_json::Value::Null,
728 Self::Bool(value) => serde_json::Value::Bool(value),
729 Self::Number(value) => serde_json::Value::Number(value),
730 Self::String(value) => serde_json::Value::String(value),
731 Self::Array(values) => {
732 serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
733 }
734 Self::Object(value) => value.into_value(),
735 }
736 }
737}
738
739impl Serialize for OrderedJsonValue {
740 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
741 where
742 S: Serializer,
743 {
744 match self {
745 Self::Null => serializer.serialize_unit(),
746 Self::Bool(value) => serializer.serialize_bool(*value),
747 Self::Number(value) => value.serialize(serializer),
748 Self::String(value) => serializer.serialize_str(value),
749 Self::Array(values) => values.serialize(serializer),
750 Self::Object(value) => value.serialize(serializer),
751 }
752 }
753}
754
755impl<'de> Deserialize<'de> for OrderedJsonValue {
756 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
757 where
758 D: Deserializer<'de>,
759 {
760 struct OrderedValueVisitor;
761
762 impl<'de> Visitor<'de> for OrderedValueVisitor {
763 type Value = OrderedJsonValue;
764
765 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
766 formatter.write_str("a JSON value with ordered object members")
767 }
768
769 fn visit_unit<E>(self) -> Result<Self::Value, E>
770 where
771 E: serde::de::Error,
772 {
773 Ok(OrderedJsonValue::Null)
774 }
775
776 fn visit_none<E>(self) -> Result<Self::Value, E>
777 where
778 E: serde::de::Error,
779 {
780 Ok(OrderedJsonValue::Null)
781 }
782
783 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
784 where
785 D: Deserializer<'de>,
786 {
787 OrderedJsonValue::deserialize(deserializer)
788 }
789
790 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
791 where
792 E: serde::de::Error,
793 {
794 Ok(OrderedJsonValue::Bool(value))
795 }
796
797 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
798 where
799 E: serde::de::Error,
800 {
801 Ok(OrderedJsonValue::Number(value.into()))
802 }
803
804 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
805 where
806 E: serde::de::Error,
807 {
808 Ok(OrderedJsonValue::Number(value.into()))
809 }
810
811 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
812 where
813 E: serde::de::Error,
814 {
815 serde_json::Number::from_f64(value)
816 .map(OrderedJsonValue::Number)
817 .ok_or_else(|| E::custom("non-finite JSON number"))
818 }
819
820 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
821 where
822 E: serde::de::Error,
823 {
824 Ok(OrderedJsonValue::String(value.to_owned()))
825 }
826
827 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
828 where
829 E: serde::de::Error,
830 {
831 Ok(OrderedJsonValue::String(value))
832 }
833
834 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
835 where
836 A: SeqAccess<'de>,
837 {
838 let mut values = Vec::new();
839 while let Some(value) = sequence.next_element()? {
840 values.push(value);
841 }
842 Ok(OrderedJsonValue::Array(values))
843 }
844
845 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
846 where
847 A: MapAccess<'de>,
848 {
849 let mut entries = Vec::new();
850 while let Some((key, value)) = map.next_entry()? {
851 entries.push((key, value));
852 }
853 Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
854 }
855 }
856
857 deserializer.deserialize_any(OrderedValueVisitor)
858 }
859}
860
861impl Serialize for OrderedJsonObject {
862 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
863 where
864 S: Serializer,
865 {
866 let mut map = serializer.serialize_map(Some(self.0.len()))?;
867 for (key, value) in &self.0 {
868 map.serialize_entry(key, value)?;
869 }
870 map.end()
871 }
872}
873
874impl<'de> Deserialize<'de> for OrderedJsonObject {
875 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
876 where
877 D: Deserializer<'de>,
878 {
879 struct OrderedObjectVisitor;
880
881 impl<'de> Visitor<'de> for OrderedObjectVisitor {
882 type Value = OrderedJsonObject;
883
884 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
885 formatter.write_str("an object with ordered JSON members")
886 }
887
888 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
889 where
890 A: MapAccess<'de>,
891 {
892 let mut entries = Vec::new();
893 while let Some((key, value)) = map.next_entry()? {
894 entries.push((key, value));
895 }
896 Ok(OrderedJsonObject(entries))
897 }
898 }
899
900 deserializer.deserialize_map(OrderedObjectVisitor)
901 }
902}
903
904fn read_tagged<'de, D>(
905 deserializer: D,
906 field: &'static str,
907) -> Result<(String, OrderedJsonObject), D::Error>
908where
909 D: Deserializer<'de>,
910{
911 let body = OrderedJsonObject::deserialize(deserializer)?;
912 let mut tag = None;
913 for (key, value) in body.as_entries() {
914 if key != field {
915 continue;
916 }
917 if tag.is_some() {
918 return Err(D::Error::custom(format!(
919 "tagged object has duplicate `{field}` field"
920 )));
921 }
922 let OrderedJsonValue::String(value) = value else {
923 return Err(D::Error::custom(format!(
924 "tagged object has no string `{field}` field"
925 )));
926 };
927 tag = Some(value);
928 }
929 let Some(tag) = tag else {
930 return Err(D::Error::custom(format!(
931 "tagged object has no string `{field}` field"
932 )));
933 };
934 Ok((tag.to_string(), body))
935}
936
937fn read_ordered_tagged(
938 value: OrderedJsonValue,
939 field: &'static str,
940) -> Result<(String, OrderedJsonObject), String> {
941 let OrderedJsonValue::Object(body) = value else {
942 return Err(format!("expected tagged object with `{field}` field"));
943 };
944 let mut tag = None;
945 for (key, value) in body.as_entries() {
946 if key != field {
947 continue;
948 }
949 if tag.is_some() {
950 return Err(format!("tagged object has duplicate `{field}` field"));
951 }
952 let OrderedJsonValue::String(value) = value else {
953 return Err(format!("tagged object has no string `{field}` field"));
954 };
955 tag = Some(value);
956 }
957 let Some(tag) = tag else {
958 return Err(format!("tagged object has no string `{field}` field"));
959 };
960 Ok((tag.to_string(), body))
961}
962
963fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
964 body.as_entries()
965 .iter()
966 .find_map(|(key, value)| (key == field).then_some(value))
967}
968
969fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
970 match ordered_field(body, field) {
971 Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
972 Some(_) => Err(format!("tagged object field `{field}` is not a string")),
973 None => Err(format!("tagged object has no `{field}` field")),
974 }
975}
976
977fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
978 let (tag, body) = read_ordered_tagged(value, "method")?;
979 match tag.as_str() {
980 "linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
981 digest: ordered_string(&body, "digest")?,
982 }),
983 "macos_spawn_inode" => {
984 let device = ordered_field(&body, "device")
985 .and_then(|value| match value {
986 OrderedJsonValue::Number(number) => number.as_u64(),
987 _ => None,
988 })
989 .ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
990 let inode = ordered_field(&body, "inode")
991 .and_then(|value| match value {
992 OrderedJsonValue::Number(number) => number.as_u64(),
993 _ => None,
994 })
995 .ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
996 Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
997 }
998 _ => Ok(RunningImageEvidence::Unknown { tag, body }),
999 }
1000}
1001
1002impl Serialize for ModuleDeclaredProvenance {
1003 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1004 where
1005 S: Serializer,
1006 {
1007 match self {
1008 Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
1009 build: build.clone(),
1010 }
1011 .serialize(serializer),
1012 Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
1013 Self::Unknown { body, .. } => body.serialize(serializer),
1014 }
1015 }
1016}
1017
1018impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
1019 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1020 where
1021 D: serde::Deserializer<'de>,
1022 {
1023 let (tag, value) = read_tagged(deserializer, "status")?;
1024 match tag.as_str() {
1025 "reported" => match serde_json::from_value(value.into_value())
1026 .map_err(D::Error::custom)?
1027 {
1028 ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
1029 ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
1030 },
1031 "unverifiable" => {
1032 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1033 ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
1034 ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
1035 }
1036 }
1037 _ => Ok(Self::Unknown { tag, body: value }),
1038 }
1039 }
1040}
1041
1042impl Serialize for RunningImageAgreement {
1043 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1044 where
1045 S: Serializer,
1046 {
1047 match self {
1048 Self::Match { evidence } => RunningImageAgreementWire::Match {
1049 evidence: evidence.clone(),
1050 }
1051 .serialize(serializer),
1052 Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
1053 running: running.clone(),
1054 disk: disk.clone(),
1055 }
1056 .serialize(serializer),
1057 Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
1058 reason: reason.clone(),
1059 }
1060 .serialize(serializer),
1061 Self::Unknown { body, .. } => body.serialize(serializer),
1062 }
1063 }
1064}
1065
1066impl<'de> Deserialize<'de> for RunningImageAgreement {
1067 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1068 where
1069 D: serde::Deserializer<'de>,
1070 {
1071 let (tag, value) = read_tagged(deserializer, "status")?;
1072 match tag.as_str() {
1073 "match" => Ok(Self::Match {
1074 evidence: decode_running_image_evidence(
1075 ordered_field(&value, "evidence")
1076 .cloned()
1077 .ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
1078 )
1079 .map_err(D::Error::custom)?,
1080 }),
1081 "mismatch" => Ok(Self::Mismatch {
1082 running: decode_running_image_evidence(
1083 ordered_field(&value, "running")
1084 .cloned()
1085 .ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
1086 )
1087 .map_err(D::Error::custom)?,
1088 disk: decode_running_image_evidence(
1089 ordered_field(&value, "disk")
1090 .cloned()
1091 .ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
1092 )
1093 .map_err(D::Error::custom)?,
1094 }),
1095 "unavailable" => Ok(Self::Unavailable {
1096 reason: serde_json::from_value(
1097 ordered_field(&value, "reason")
1098 .cloned()
1099 .ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
1100 .into_value(),
1101 )
1102 .map_err(D::Error::custom)?,
1103 }),
1104 _ => Ok(Self::Unknown { tag, body: value }),
1105 }
1106 }
1107}
1108
1109impl Serialize for RunningImageEvidence {
1110 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1111 where
1112 S: Serializer,
1113 {
1114 match self {
1115 Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
1116 digest: digest.clone(),
1117 }
1118 .serialize(serializer),
1119 Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
1120 device: *device,
1121 inode: *inode,
1122 }
1123 .serialize(serializer),
1124 Self::Unknown { body, .. } => body.serialize(serializer),
1125 }
1126 }
1127}
1128
1129impl<'de> Deserialize<'de> for RunningImageEvidence {
1130 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1131 where
1132 D: serde::Deserializer<'de>,
1133 {
1134 let (tag, value) = read_tagged(deserializer, "method")?;
1135 match tag.as_str() {
1136 "linux_proc_sha256" => {
1137 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1138 RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
1139 Ok(Self::LinuxProcSha256 { digest })
1140 }
1141 _ => unreachable!(),
1142 }
1143 }
1144 "macos_spawn_inode" => {
1145 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1146 RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
1147 Ok(Self::MacosSpawnInode { device, inode })
1148 }
1149 _ => unreachable!(),
1150 }
1151 }
1152 _ => Ok(Self::Unknown { tag, body: value }),
1153 }
1154 }
1155}
1156
1157impl Serialize for SupervisorRouteConsumer {
1158 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1159 where
1160 S: Serializer,
1161 {
1162 match self {
1163 Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
1164 module_id: module_id.clone(),
1165 }
1166 .serialize(serializer),
1167 Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
1168 connection_id: *connection_id,
1169 }
1170 .serialize(serializer),
1171 Self::Unknown { body, .. } => body.serialize(serializer),
1172 }
1173 }
1174}
1175
1176impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
1177 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1178 where
1179 D: serde::Deserializer<'de>,
1180 {
1181 let (tag, value) = read_tagged(deserializer, "kind")?;
1182 match tag.as_str() {
1183 "reserved" => {
1184 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1185 SupervisorRouteConsumerWire::Reserved { module_id } => {
1186 Ok(Self::Reserved { module_id })
1187 }
1188 _ => unreachable!(),
1189 }
1190 }
1191 "direct" => {
1192 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1193 SupervisorRouteConsumerWire::Direct { connection_id } => {
1194 Ok(Self::Direct { connection_id })
1195 }
1196 _ => unreachable!(),
1197 }
1198 }
1199 _ => Ok(Self::Unknown { tag, body: value }),
1200 }
1201 }
1202}
1203
1204impl Serialize for StderrCaptureState {
1205 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1206 where
1207 S: Serializer,
1208 {
1209 match self {
1210 Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
1211 Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
1212 reason: reason.clone(),
1213 }
1214 .serialize(serializer),
1215 Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
1216 reason: reason.clone(),
1217 }
1218 .serialize(serializer),
1219 Self::Unknown { body, .. } => body.serialize(serializer),
1220 }
1221 }
1222}
1223
1224impl<'de> Deserialize<'de> for StderrCaptureState {
1225 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1226 where
1227 D: serde::Deserializer<'de>,
1228 {
1229 let (tag, value) = read_tagged(deserializer, "state")?;
1230 match tag.as_str() {
1231 "captured" => {
1232 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1233 StderrCaptureStateWire::Captured => Ok(Self::Captured),
1234 _ => unreachable!(),
1235 }
1236 }
1237 "incomplete" => match serde_json::from_value(value.into_value())
1238 .map_err(D::Error::custom)?
1239 {
1240 StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
1241 _ => unreachable!(),
1242 },
1243 "not_captured" => match serde_json::from_value(value.into_value())
1244 .map_err(D::Error::custom)?
1245 {
1246 StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
1247 _ => unreachable!(),
1248 },
1249 _ => Ok(Self::Unknown { tag, body: value }),
1250 }
1251 }
1252}
1253
1254impl Serialize for StderrTailEntry {
1255 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1256 where
1257 S: Serializer,
1258 {
1259 match self {
1260 Self::Line { text, truncated } => StderrTailEntryWire::Line {
1261 text: text.clone(),
1262 truncated: *truncated,
1263 }
1264 .serialize(serializer),
1265 Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
1266 Self::Unknown { body, .. } => body.serialize(serializer),
1267 }
1268 }
1269}
1270
1271impl<'de> Deserialize<'de> for StderrTailEntry {
1272 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1273 where
1274 D: serde::Deserializer<'de>,
1275 {
1276 let (tag, value) = read_tagged(deserializer, "kind")?;
1277 match tag.as_str() {
1278 "line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1279 StderrTailEntryWire::Line { text, truncated } => Ok(Self::Line { text, truncated }),
1280 _ => unreachable!(),
1281 },
1282 "process_start" => {
1283 match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
1284 StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
1285 _ => unreachable!(),
1286 }
1287 }
1288 _ => Ok(Self::Unknown { tag, body: value }),
1289 }
1290 }
1291}
1292
1293fn is_zero_u64(value: &u64) -> bool {
1294 *value == 0
1295}
1296
1297#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1299pub struct TerminalHistory {
1300 pub daemon_started_at_ms: u64,
1303 pub entries: Vec<TerminalEntry>,
1304 #[serde(default, skip_serializing_if = "is_zero_u64")]
1306 pub dropped: u64,
1307}
1308
1309#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1311pub struct TerminalEntry {
1312 #[serde(default, skip_serializing_if = "Option::is_none")]
1313 pub exit_code: Option<i32>,
1314 #[serde(default, skip_serializing_if = "Option::is_none")]
1315 pub exit_signal: Option<i32>,
1316 pub at_ms: u64,
1317 pub disposition: TerminalDisposition,
1318 #[serde(default, skip_serializing_if = "Option::is_none")]
1322 pub exit_kind: Option<TerminalExitKind>,
1323 #[serde(default, skip_serializing_if = "Option::is_none")]
1330 pub disposition_detail: Option<String>,
1331}
1332
1333#[derive(Debug, Clone, PartialEq, Eq)]
1338pub enum TerminalExitKind {
1339 Clean,
1340 Crash,
1341 DeliberateSeverance,
1342 Unknown(String),
1343}
1344
1345impl TerminalExitKind {
1346 fn wire_name(&self) -> &str {
1347 match self {
1348 Self::Clean => "clean",
1349 Self::Crash => "crash",
1350 Self::DeliberateSeverance => "deliberate_severance",
1351 Self::Unknown(value) => value,
1352 }
1353 }
1354}
1355
1356impl Serialize for TerminalExitKind {
1357 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1358 where
1359 S: serde::Serializer,
1360 {
1361 serializer.serialize_str(self.wire_name())
1362 }
1363}
1364
1365impl<'de> Deserialize<'de> for TerminalExitKind {
1366 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1367 where
1368 D: serde::Deserializer<'de>,
1369 {
1370 let value = String::deserialize(deserializer)?;
1371 Ok(match value.as_str() {
1372 "clean" => Self::Clean,
1373 "crash" => Self::Crash,
1374 "deliberate_severance" => Self::DeliberateSeverance,
1375 _ => Self::Unknown(value),
1376 })
1377 }
1378}
1379
1380open_string_enum! {
1381 TerminalDisposition {
1383 Stopped => "stopped",
1384 Disabled => "disabled",
1385 Failed => "failed",
1386 Restarting => "restarting",
1387 }
1388}
1389
1390#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1391#[serde(rename_all = "snake_case")]
1392pub enum PollKind {
1393 Status,
1394 Liveness,
1395}
1396
1397#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1398pub struct CatalogEntry {
1399 pub module_id: String,
1400 #[serde(default, skip_serializing_if = "Option::is_none")]
1421 pub module_version: Option<String>,
1422 pub roles: Vec<ProviderRole>,
1423 pub control_ops: Vec<String>,
1424 #[serde(default, skip_serializing_if = "Option::is_none")]
1429 pub capabilities: Option<CapabilityDeclarations>,
1430 #[serde(default, skip_serializing_if = "Option::is_none")]
1433 pub self_signals: Option<Vec<SelfSignalDeclaration>>,
1434}
1435
1436#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1437pub struct CapabilityRequirementStatus {
1438 pub consumer: String,
1439 pub capability: String,
1440 pub need: String,
1441 pub verdict: String,
1442 pub episode_seq: u64,
1443 pub config_satisfiable: bool,
1444 pub runtime_available: bool,
1445 pub detail: String,
1446}
1447
1448#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1449pub struct SupervisorRescanResult {
1450 pub added: Vec<String>,
1451 pub removed: Vec<String>,
1452 pub changed_pending_reload: Vec<String>,
1453 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1466 pub enabled_changes: Vec<String>,
1467 pub unchanged: u32,
1468 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1476 pub preview: bool,
1477 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1495 pub restart_required: Vec<String>,
1496 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1500 pub capability_warnings: Vec<String>,
1501}
1502
1503#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1504pub struct SupervisorEntry {
1505 pub module_id: String,
1506 pub state: String,
1507 pub enabled: bool,
1508 pub live: bool,
1509 pub health: SupervisorHealthStatus,
1510 #[serde(default)]
1516 pub last_probe_ms: Option<u64>,
1517 #[serde(default, skip_serializing_if = "Option::is_none")]
1521 pub last_exit_code: Option<i32>,
1522 #[serde(default, skip_serializing_if = "Option::is_none")]
1526 pub last_exit_signal: Option<i32>,
1527 #[serde(default, skip_serializing_if = "Option::is_none")]
1531 pub last_exit_ms: Option<u64>,
1532 #[serde(default, skip_serializing_if = "Option::is_none")]
1535 pub last_exit_kind: Option<TerminalExitKind>,
1536 #[serde(default, skip_serializing_if = "Option::is_none")]
1553 pub restart_count: Option<u32>,
1554 #[serde(default, skip_serializing_if = "Option::is_none")]
1557 pub max_restarts: Option<u32>,
1558 #[serde(default, skip_serializing_if = "Option::is_none")]
1561 pub lifetime_restarts: Option<u32>,
1562 #[serde(default, skip_serializing_if = "Option::is_none")]
1572 pub restart_window_secs: Option<u64>,
1573 #[serde(default, skip_serializing_if = "Option::is_none")]
1577 pub drain_timeout_ms: Option<u64>,
1578 #[serde(default, skip_serializing_if = "Option::is_none")]
1581 pub restart_backoff_ms: Option<u64>,
1582 #[serde(default, skip_serializing_if = "Option::is_none")]
1585 pub restart_max_backoff_ms: Option<u64>,
1586}
1587
1588#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1589#[serde(rename_all = "snake_case")]
1590pub enum SupervisorHealthStatus {
1591 Ok,
1592 Degraded,
1593 Failing,
1594 Unresponsive,
1595 Unknown,
1596}
1597
1598#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1599pub struct SupervisorHealthEntry {
1600 pub module_id: String,
1601 pub status: SupervisorHealthStatus,
1602 #[serde(default, skip_serializing_if = "Option::is_none")]
1608 pub detail: Option<String>,
1609 #[serde(default, skip_serializing_if = "Option::is_none")]
1614 pub metrics: Option<serde_json::Value>,
1615 pub consecutive_failures: u32,
1616 #[serde(default)]
1619 pub late_answer_count: u64,
1620 #[serde(default, skip_serializing_if = "Option::is_none")]
1622 pub last_late_answer_latency_ms: Option<u64>,
1623 #[serde(default)]
1628 pub last_action: Option<String>,
1629 #[serde(default)]
1632 pub last_action_ms: Option<u64>,
1633 #[serde(default, skip_serializing_if = "Option::is_none")]
1646 pub last_probe_ms: Option<u64>,
1647}
1648
1649#[cfg(test)]
1650mod tests {
1651 use super::*;
1652 use subc_protocol::{BindIdentity, RouteTarget};
1653
1654 #[test]
1655 fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
1656 let entry = TerminalEntry {
1657 exit_code: Some(1),
1658 exit_signal: None,
1659 at_ms: 1_700_000_000_123,
1660 disposition: TerminalDisposition::Restarting,
1661 exit_kind: Some(TerminalExitKind::DeliberateSeverance),
1662 disposition_detail: None,
1663 };
1664 let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
1665 assert_eq!(
1666 serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
1667 ["exit_kind"],
1668 "deliberate_severance"
1669 );
1670
1671 #[derive(serde::Deserialize)]
1672 struct LegacyTerminalEntry {
1673 exit_code: Option<i32>,
1674 exit_signal: Option<i32>,
1675 at_ms: u64,
1676 disposition: TerminalDisposition,
1677 }
1678
1679 let decoded: LegacyTerminalEntry =
1680 serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
1681 assert_eq!(decoded.exit_code, Some(1));
1682 assert_eq!(decoded.exit_signal, None);
1683 assert_eq!(decoded.at_ms, 1_700_000_000_123);
1684 assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
1685
1686 let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
1687 let future: TerminalEntry =
1688 serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
1689 assert_eq!(
1690 future.exit_kind,
1691 Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
1692 );
1693 }
1694
1695 #[test]
1696 fn route_poll_uses_kind_field() {
1697 let body = serde_json::to_value(ClientControlRequest::RoutePoll {
1698 route_channel: 7,
1699 route_epoch: 11,
1700 kind: PollKind::Status,
1701 })
1702 .unwrap();
1703
1704 assert_eq!(body["op"], "route.poll");
1705 assert_eq!(body["route_epoch"], 11);
1706 assert_eq!(body["kind"], "status");
1707 assert!(body.get("op").is_some());
1708 }
1709
1710 #[test]
1711 fn route_open_is_internally_tagged() {
1712 let request = ClientControlRequest::RouteOpen {
1713 target: RouteTarget::ToolProvider {
1714 module_id: "aft".to_string(),
1715 },
1716 identity: BindIdentity {
1717 project_root: "/tmp/project".into(),
1718 harness: "opencode".to_string(),
1719 session: "session-1".to_string(),
1720 },
1721 consumer_identity: None,
1722 consumer_capabilities: None,
1723 admission_facts: None,
1724 };
1725
1726 let body = serde_json::to_value(request).unwrap();
1727 assert_eq!(body["op"], "route.open");
1728 assert_eq!(body["target"]["kind"], "tool_provider");
1729 assert!(body.get("consumer_identity").is_none());
1730 assert!(body.get("consumer_capabilities").is_none());
1731 }
1732
1733 #[test]
1734 fn route_open_without_optional_fields_still_decodes() {
1735 let body = serde_json::json!({
1736 "op": "route.open",
1737 "target": { "kind": "tool_provider", "module_id": "aft" },
1738 "identity": {
1739 "project_root": "/tmp/project",
1740 "harness": "opencode",
1741 "session": "session-1"
1742 }
1743 });
1744
1745 let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
1746 let ClientControlRequest::RouteOpen {
1747 consumer_identity,
1748 consumer_capabilities,
1749 admission_facts,
1750 ..
1751 } = decoded
1752 else {
1753 panic!("decoded wrong request variant");
1754 };
1755 assert_eq!(consumer_identity, None);
1756 assert_eq!(consumer_capabilities, None);
1757 assert_eq!(admission_facts, None);
1758 }
1759
1760 #[test]
1761 fn new_route_closed_decoder_accepts_old_daemon_without_terminal() {
1762 let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
1763 let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
1764 match decoded {
1765 ClientControlPush::RouteClosed { terminal, .. } => assert_eq!(terminal, None),
1766 other => panic!("unexpected push: {other:?}"),
1767 }
1768 assert!(!serde_json::to_string(&decoded)
1769 .unwrap()
1770 .contains("terminal"));
1771 }
1772
1773 #[test]
1774 fn old_route_closed_decoder_ignores_new_terminal_field() {
1775 #[derive(serde::Deserialize)]
1776 #[serde(tag = "op")]
1777 enum LegacyClientControlPush {
1778 #[serde(rename = "route.closed")]
1779 RouteClosed {
1780 module_id: String,
1781 reason: RouteCloseReason,
1782 drained: bool,
1783 abandoned: u32,
1784 },
1785 }
1786
1787 let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"terminal":true}"#;
1788 let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
1789 match decoded {
1790 LegacyClientControlPush::RouteClosed {
1791 module_id,
1792 reason,
1793 drained,
1794 abandoned,
1795 } => {
1796 assert_eq!(module_id, "aft-tools");
1797 assert_eq!(reason, RouteCloseReason::Crash);
1798 assert!(!drained);
1799 assert_eq!(abandoned, 0);
1800 }
1801 }
1802 }
1803
1804 #[test]
1805 fn supervisor_routes_is_a_control_plane_request() {
1806 let body = serde_json::json!({
1807 "op": "supervisor.routes",
1808 "module_id": "aft"
1809 });
1810
1811 let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
1812 assert_eq!(serde_json::to_value(request).unwrap(), body);
1813 }
1814
1815 #[test]
1816 fn diagnostic_string_enums_retain_unknown_wire_values() {
1817 let reason: RunningImageUnavailableReason =
1818 serde_json::from_str("\"future_reason\"").unwrap();
1819 let disposition: TerminalDisposition =
1820 serde_json::from_str("\"future_disposition\"").unwrap();
1821
1822 assert_eq!(
1823 reason,
1824 RunningImageUnavailableReason::Unknown("future_reason".to_string())
1825 );
1826 assert_eq!(
1827 disposition,
1828 TerminalDisposition::Unknown("future_disposition".to_string())
1829 );
1830 }
1831
1832 #[test]
1833 fn diagnostic_string_enums_preserve_existing_wire_names() {
1834 let names = [
1835 (RunningImageUnavailableReason::NotRunning, "not_running"),
1836 (
1837 RunningImageUnavailableReason::UnsupportedPlatform,
1838 "unsupported_platform",
1839 ),
1840 (
1841 RunningImageUnavailableReason::RunningExecutableUnreadable,
1842 "running_executable_unreadable",
1843 ),
1844 (
1845 RunningImageUnavailableReason::SpawnedPathUnreadable,
1846 "spawned_path_unreadable",
1847 ),
1848 (RunningImageUnavailableReason::HashFailed, "hash_failed"),
1849 (
1850 RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
1851 "process_identity_unconfirmed",
1852 ),
1853 ];
1854 for (value, expected) in names {
1855 let wire = serde_json::to_string(&value).unwrap();
1856 assert_eq!(wire, format!("\"{expected}\""));
1857 let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
1858 assert_eq!(decoded, value);
1859 }
1860
1861 for (value, expected) in [
1862 (TerminalDisposition::Stopped, "stopped"),
1863 (TerminalDisposition::Disabled, "disabled"),
1864 (TerminalDisposition::Failed, "failed"),
1865 (TerminalDisposition::Restarting, "restarting"),
1866 ] {
1867 let wire = serde_json::to_string(&value).unwrap();
1868 assert_eq!(wire, format!("\"{expected}\""));
1869 let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
1870 assert_eq!(decoded, value);
1871 }
1872 }
1873
1874 #[test]
1875 fn diagnostic_string_enums_reject_non_string_bodies() {
1876 assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
1877 assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
1878 }
1879
1880 #[test]
1881 fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
1882 let body = serde_json::json!({
1883 "op": "supervisor.provenance",
1884 "daemon": {
1885 "daemon_build": {},
1886 "daemon_observed": {
1887 "running_image": {
1888 "status": "unavailable",
1889 "reason": "not_running"
1890 }
1891 }
1892 },
1893 "modules": [
1894 {
1895 "module_id": "future",
1896 "module_declared": { "status": "unverifiable" },
1897 "daemon_observed": {
1898 "running_image": {
1899 "status": "unavailable",
1900 "reason": "future_reason"
1901 }
1902 }
1903 },
1904 {
1905 "module_id": "healthy-a",
1906 "module_declared": { "status": "unverifiable" },
1907 "daemon_observed": {
1908 "running_image": {
1909 "status": "match",
1910 "evidence": {
1911 "method": "linux_proc_sha256",
1912 "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1913 }
1914 }
1915 }
1916 },
1917 {
1918 "module_id": "healthy-b",
1919 "module_declared": { "status": "unverifiable" },
1920 "daemon_observed": {
1921 "running_image": {
1922 "status": "unavailable",
1923 "reason": "unsupported_platform"
1924 }
1925 }
1926 }
1927 ]
1928 });
1929
1930 let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
1931 let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
1932 panic!("decoded wrong response variant");
1933 };
1934 assert_eq!(modules.len(), 3);
1935 assert_eq!(modules[0].module_id, "future");
1936 assert_eq!(
1937 modules[0].daemon_observed.running_image,
1938 RunningImageAgreement::Unavailable {
1939 reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
1940 }
1941 );
1942 assert_eq!(modules[1].module_id, "healthy-a");
1943 assert_eq!(modules[2].module_id, "healthy-b");
1944 }
1945
1946 #[test]
1947 fn tagged_unknown_values_retain_tag_and_body() {
1948 macro_rules! assert_unknown_round_trip {
1949 ($ty:ident, $field:literal, $value:expr) => {
1950 let value = $value;
1951 let wire = serde_json::to_string(&value).unwrap();
1952 let decoded: $ty = serde_json::from_str(&wire).unwrap();
1953 match decoded {
1954 $ty::Unknown { tag, body } => {
1955 assert_eq!(tag, value[$field].as_str().unwrap());
1956 assert_eq!(serde_json::to_value(&body).unwrap(), value);
1957 }
1958 _ => panic!("decoded known variant"),
1959 }
1960 };
1961 }
1962
1963 assert_unknown_round_trip!(
1964 ModuleDeclaredProvenance,
1965 "status",
1966 serde_json::json!({"status": "future", "build": {"version": 7}})
1967 );
1968 assert_unknown_round_trip!(
1969 RunningImageAgreement,
1970 "status",
1971 serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
1972 );
1973 assert_unknown_round_trip!(
1974 RunningImageEvidence,
1975 "method",
1976 serde_json::json!({"method": "future", "digest": "abc"})
1977 );
1978 assert_unknown_round_trip!(
1979 SupervisorRouteConsumer,
1980 "kind",
1981 serde_json::json!({"kind": "future", "module_id": "m"})
1982 );
1983 assert_unknown_round_trip!(
1984 StderrCaptureState,
1985 "state",
1986 serde_json::json!({"state": "future", "reason": "because"})
1987 );
1988 assert_unknown_round_trip!(
1989 StderrTailEntry,
1990 "kind",
1991 serde_json::json!({"kind": "future", "text": "line"})
1992 );
1993 }
1994
1995 #[test]
1996 fn tagged_unknown_values_round_trip_the_original_json() {
1997 let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
1998 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
1999 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2000 }
2001
2002 #[test]
2003 fn tagged_unknown_values_round_trip_trailing_tag() {
2004 let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
2005 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2006 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2007
2008 let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
2009 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2010 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2011 }
2012
2013 #[test]
2014 fn tagged_unknown_values_round_trip_middle_tag() {
2015 let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
2016 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2017 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2018
2019 let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
2020 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2021 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2022 }
2023
2024 #[test]
2025 fn tagged_unknown_values_round_trip_deep_payload() {
2026 let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
2027 let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
2028 assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
2029
2030 let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
2031 let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
2032 assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
2033 }
2034
2035 #[test]
2036 fn tagged_unknown_values_reject_non_object_bodies() {
2037 for wire in ["42", r#""future""#, "[]"] {
2038 assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
2039 assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
2040 }
2041 }
2042
2043 #[test]
2044 fn duplicate_discriminators_reject_without_panicking() {
2045 assert_eq!(
2046 serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
2047 .unwrap(),
2048 ModuleDeclaredProvenance::Unverifiable
2049 );
2050 match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
2051 .unwrap()
2052 {
2053 ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
2054 _ => panic!("future discriminator decoded as a known variant"),
2055 }
2056
2057 let wires = [
2058 r#"{"status":"reported","status":"unverifiable"}"#,
2059 r#"{"status":"unverifiable","status":"reported"}"#,
2060 r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
2061 r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
2062 ];
2063
2064 for wire in wires {
2065 let result =
2066 std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
2067 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2068 assert!(
2069 result.unwrap().is_err(),
2070 "duplicate discriminator decoded: {wire}"
2071 );
2072 }
2073
2074 let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
2075 let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
2076 assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
2077 assert!(
2078 result.unwrap().is_err(),
2079 "duplicate discriminator decoded: {wire}"
2080 );
2081 }
2082
2083 #[test]
2084 fn nested_unknown_values_round_trip_without_normalizing_member_order() {
2085 let known_wire =
2086 r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
2087 let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
2088 assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
2089
2090 for wire in [
2091 r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
2092 r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
2093 ] {
2094 let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
2095 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2096 }
2097
2098 for wire in [
2099 r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
2100 r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
2101 ] {
2102 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2103 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2104 }
2105
2106 let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
2107 let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
2108 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2109
2110 let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
2111 let decoded: StderrTail = serde_json::from_str(wire).unwrap();
2112 assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
2113 }
2114
2115 #[test]
2116 fn tagged_unknown_member_does_not_discard_known_siblings() {
2117 let body = serde_json::json!({
2118 "modules": [{
2119 "module_id": "target",
2120 "routes": [
2121 {"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
2122 {"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
2123 ]
2124 }]
2125 });
2126 let decoded: ClientControlResponse = serde_json::from_value(
2127 serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
2128 )
2129 .unwrap();
2130 let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
2131 panic!("decoded wrong response variant");
2132 };
2133 assert_eq!(modules[0].routes.len(), 2);
2134 assert_eq!(
2135 modules[0].routes[1].consumer,
2136 SupervisorRouteConsumer::Direct { connection_id: 7 }
2137 );
2138 }
2139}