#![forbid(unsafe_code)]
use std::path::PathBuf;
use serde::{
de::{Error as _, MapAccess, SeqAccess, Visitor},
ser::SerializeMap,
Deserialize, Deserializer, Serialize, Serializer,
};
use subc_protocol::{
manifest::{CapabilityDeclarations, ManifestProvenance, ProviderRole, SelfSignalDeclaration},
session::HealthStatus,
BindIdentity, RouteTarget,
};
pub use subc_protocol::RouteCloseReason;
macro_rules! open_string_enum {
(
$(#[$meta:meta])*
$name:ident {
$( $variant:ident => $wire_name:literal ),+ $(,)?
}
) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum $name {
$( $variant, )+
Unknown(String),
}
impl $name {
fn wire_name(&self) -> &str {
match self {
$( Self::$variant => $wire_name, )+
Self::Unknown(value) => value,
}
}
}
impl Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.wire_name())
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(match value.as_str() {
$( $wire_name => Self::$variant, )+
_ => Self::Unknown(value),
})
}
}
};
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ConsumerIdentity {
pub module_id: String,
pub launch_nonce: String,
}
pub mod ops {
pub const SERVER: &str = "server.";
pub const CATALOG: &str = "catalog.";
pub const ROUTE: &str = "route.";
pub const SUPERVISOR: &str = "supervisor.";
pub const CONFIG: &str = "config.";
pub const SERVER_DESCRIBE: &str = "server.describe";
pub const CATALOG_LIST: &str = "catalog.list";
pub const ROUTE_OPEN: &str = "route.open";
pub const ROUTE_POLL: &str = "route.poll";
pub const ROUTE_CLOSING: &str = "route.closing";
pub const ROUTE_CLOSED: &str = "route.closed";
pub const SUPERVISOR_LIST: &str = "supervisor.list";
pub const SUPERVISOR_RESTART: &str = "supervisor.restart";
pub const SUPERVISOR_SWAP: &str = "supervisor.swap";
pub const SUPERVISOR_RELOAD: &str = "supervisor.reload";
pub const SUPERVISOR_RESCAN: &str = "supervisor.rescan";
pub const SUPERVISOR_RELEASE_RESERVED: &str = "supervisor.release_reserved";
pub const SUPERVISOR_SET_ENABLED: &str = "supervisor.set_enabled";
pub const SUPERVISOR_HEALTH_PROBE: &str = "supervisor.health_probe";
pub const SUPERVISOR_HEALTH: &str = "supervisor.health";
pub const SUPERVISOR_STDERR_TAIL: &str = "supervisor.stderr_tail";
pub const SUPERVISOR_TERMINALS: &str = "supervisor.terminals";
pub const SUPERVISOR_ROUTES: &str = "supervisor.routes";
pub const SUPERVISOR_PROVENANCE: &str = "supervisor.provenance";
pub const SUPERVISOR_SPAWN_SNAPSHOT: &str = "supervisor.spawn_snapshot";
pub const SUPERVISOR_SPAWN_SUBSCRIBE: &str = "supervisor.spawn_subscribe";
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "op")]
#[allow(clippy::large_enum_variant)]
pub enum ClientControlRequest {
#[serde(rename = "server.describe")]
ServerDescribe {},
#[serde(rename = "catalog.list")]
CatalogList {
#[serde(default)]
module_id: Option<String>,
},
#[serde(rename = "route.open")]
RouteOpen {
target: RouteTarget,
identity: BindIdentity,
#[serde(default, skip_serializing_if = "Option::is_none")]
consumer_identity: Option<ConsumerIdentity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
consumer_capabilities: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
admission_facts: Option<serde_json::Value>,
},
#[serde(rename = "route.poll")]
RoutePoll {
route_channel: u16,
route_epoch: u32,
kind: PollKind,
},
#[serde(rename = "supervisor.list")]
SupervisorList {},
#[serde(rename = "supervisor.spawn_snapshot")]
SupervisorSpawnSnapshot {},
#[serde(rename = "supervisor.spawn_subscribe")]
SupervisorSpawnSubscribe {
#[serde(default, skip_serializing_if = "Option::is_none")]
since: Option<SpawnCursor>,
},
#[serde(rename = "supervisor.restart")]
SupervisorRestart {
module_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
drain_timeout_ms: Option<u64>,
},
#[serde(rename = "supervisor.swap")]
SupervisorSwap {
module_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
ready_timeout_ms: Option<u64>,
},
#[serde(rename = "supervisor.reload")]
SupervisorReload { module_id: String },
#[serde(rename = "supervisor.rescan")]
SupervisorRescan {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
preview: bool,
},
#[serde(rename = "supervisor.release_reserved")]
SupervisorReleaseReserved { module_id: String },
#[serde(rename = "supervisor.set_enabled")]
SupervisorSetEnabled { module_id: String, enabled: bool },
#[serde(rename = "supervisor.health_probe")]
SupervisorHealthProbe { module_id: String },
#[serde(rename = "supervisor.health")]
SupervisorHealth {},
#[serde(rename = "supervisor.routes")]
SupervisorRoutes {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<String>,
},
#[serde(rename = "supervisor.provenance")]
SupervisorProvenance {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<String>,
},
#[serde(rename = "supervisor.stderr_tail")]
SupervisorStderrTail {
module_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
max_lines: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max_bytes: Option<u32>,
},
#[serde(rename = "supervisor.terminals")]
SupervisorTerminals { module_id: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "op")]
pub enum ClientControlResponse {
#[serde(rename = "server.describe")]
ServerDescribe {
protocol_ver: u8,
subc_ops: Vec<String>,
capabilities: Vec<String>,
connected_clients: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
counters: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
build_git_sha: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
build_lock_digest: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
capability_requirements: Vec<CapabilityRequirementStatus>,
},
#[serde(rename = "catalog.list")]
CatalogList {
generation: u64,
modules: Vec<CatalogEntry>,
subc_ops: Vec<String>,
},
#[serde(rename = "route.open")]
RouteOpen {
route_channel: u16,
route_epoch: u32,
},
#[serde(rename = "route.poll")]
RoutePoll {
route_channel: u16,
route_epoch: u32,
status: Option<String>,
live: Option<bool>,
},
#[serde(rename = "supervisor.list")]
SupervisorList {
generation: u64,
modules: Vec<SupervisorEntry>,
},
#[serde(rename = "supervisor.spawn_snapshot")]
SupervisorSpawnSnapshot {
#[serde(flatten)]
snapshot: SpawnSnapshot,
},
#[serde(rename = "supervisor.ack")]
SupervisorAck { module_id: String, applied: bool },
#[serde(rename = "supervisor.rescan")]
SupervisorRescan {
#[serde(flatten)]
result: SupervisorRescanResult,
},
#[serde(rename = "supervisor.health_probe")]
SupervisorHealthProbe {
module_id: String,
status: HealthStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
detail: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
metrics: Option<serde_json::Value>,
},
#[serde(rename = "supervisor.health")]
SupervisorHealth {
generation: u64,
modules: Vec<SupervisorHealthEntry>,
},
#[serde(rename = "supervisor.routes")]
SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
#[serde(rename = "supervisor.provenance")]
SupervisorProvenance {
daemon: SupervisorDaemonProvenance,
modules: Vec<SupervisorModuleProvenance>,
},
#[serde(rename = "supervisor.stderr_tail")]
SupervisorStderrTail {
module_id: String,
#[serde(flatten)]
tail: StderrTail,
},
#[serde(rename = "supervisor.terminals")]
SupervisorTerminals {
module_id: String,
#[serde(flatten)]
terminals: TerminalHistory,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "op")]
pub enum ClientControlPush {
#[serde(rename = "route.closing")]
RouteClosing {
module_id: String,
reason: RouteCloseReason,
},
#[serde(rename = "route.closed")]
RouteClosed {
module_id: String,
reason: RouteCloseReason,
drained: bool,
abandoned: u32,
#[serde(default)]
excluded_subscriptions: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
terminal: Option<bool>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SpawnCursor {
pub daemon_incarnation: String,
pub seq: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LiveSpawn {
pub module_id: String,
pub spawn_generation: u64,
pub pid: u32,
pub spawned_at_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SpawnSnapshot {
pub cursor: SpawnCursor,
pub ring_bound: u64,
pub live: Vec<LiveSpawn>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SpawnEventKind {
Spawned,
Exited,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SpawnEvent {
pub cursor: SpawnCursor,
pub kind: SpawnEventKind,
pub module_id: String,
pub spawn_generation: u64,
pub pid: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_signal: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StderrTail {
pub capture: StderrCaptureState,
pub entries: Vec<StderrTailEntry>,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub dropped_lines: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SupervisorRouteModule {
pub module_id: String,
pub routes: Vec<SupervisorRoute>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SupervisorRoute {
pub consumer: SupervisorRouteConsumer,
pub age_ms: u64,
pub draining: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub drain_reason: Option<RouteCloseReason>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SupervisorModuleProvenance {
pub module_id: String,
pub module_declared: ModuleDeclaredProvenance,
pub daemon_observed: SupervisorObservedProcess,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ModuleDeclaredProvenance {
Reported {
build: ManifestProvenance,
},
Unverifiable,
Unknown {
tag: String,
body: OrderedJsonObject,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SupervisorObservedProcess {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spawned_at_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spawned_from: Option<PathBuf>,
pub running_image: RunningImageAgreement,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SupervisorDaemonProvenance {
pub daemon_build: DaemonBuildProvenance,
pub daemon_observed: DaemonObservedProcess,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DaemonBuildProvenance {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_git_sha: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_lock_digest: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DaemonObservedProcess {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub started_at_ms: Option<u64>,
pub running_image: RunningImageAgreement,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RunningImageAgreement {
Match {
evidence: RunningImageEvidence,
},
Mismatch {
running: RunningImageEvidence,
disk: RunningImageEvidence,
},
Unavailable {
reason: RunningImageUnavailableReason,
},
Unknown {
tag: String,
body: OrderedJsonObject,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum RunningImageEvidence {
LinuxProcSha256 {
digest: String,
},
MacosSpawnInode {
device: u64,
inode: u64,
},
Unknown {
tag: String,
body: OrderedJsonObject,
},
}
open_string_enum! {
RunningImageUnavailableReason {
NotRunning => "not_running",
UnsupportedPlatform => "unsupported_platform",
RunningExecutableUnreadable => "running_executable_unreadable",
SpawnedPathUnreadable => "spawned_path_unreadable",
HashFailed => "hash_failed",
ProcessIdentityUnconfirmed => "process_identity_unconfirmed",
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SupervisorRouteConsumer {
Reserved {
module_id: String,
},
Direct {
connection_id: u64,
},
Unknown {
tag: String,
body: OrderedJsonObject,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum StderrCaptureState {
Captured,
Incomplete { reason: String },
NotCaptured { reason: String },
Unknown {
tag: String,
body: OrderedJsonObject,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum StderrTailEntry {
Line {
text: String,
truncated: bool,
},
ProcessStart,
Unknown {
tag: String,
body: OrderedJsonObject,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
enum ModuleDeclaredProvenanceWire {
Reported { build: ManifestProvenance },
Unverifiable,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
enum RunningImageAgreementWire {
Match {
evidence: RunningImageEvidence,
},
Mismatch {
running: RunningImageEvidence,
disk: RunningImageEvidence,
},
Unavailable {
reason: RunningImageUnavailableReason,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "method", rename_all = "snake_case")]
enum RunningImageEvidenceWire {
LinuxProcSha256 { digest: String },
MacosSpawnInode { device: u64, inode: u64 },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum SupervisorRouteConsumerWire {
Reserved { module_id: String },
Direct { connection_id: u64 },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
enum StderrCaptureStateWire {
Captured,
Incomplete { reason: String },
NotCaptured { reason: String },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum StderrTailEntryWire {
Line {
text: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
truncated: bool,
},
ProcessStart,
}
#[derive(Debug, Clone, PartialEq)]
pub enum OrderedJsonValue {
Null,
Bool(bool),
Number(serde_json::Number),
String(String),
Array(Vec<Self>),
Object(OrderedJsonObject),
}
#[derive(Debug, Clone, PartialEq)]
pub struct OrderedJsonObject(Vec<(String, OrderedJsonValue)>);
impl OrderedJsonObject {
pub fn as_entries(&self) -> &[(String, OrderedJsonValue)] {
&self.0
}
fn into_value(self) -> serde_json::Value {
serde_json::Value::Object(
self.0
.into_iter()
.map(|(key, value)| (key, value.into_value()))
.collect(),
)
}
}
impl OrderedJsonValue {
fn into_value(self) -> serde_json::Value {
match self {
Self::Null => serde_json::Value::Null,
Self::Bool(value) => serde_json::Value::Bool(value),
Self::Number(value) => serde_json::Value::Number(value),
Self::String(value) => serde_json::Value::String(value),
Self::Array(values) => {
serde_json::Value::Array(values.into_iter().map(Self::into_value).collect())
}
Self::Object(value) => value.into_value(),
}
}
}
impl Serialize for OrderedJsonValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Null => serializer.serialize_unit(),
Self::Bool(value) => serializer.serialize_bool(*value),
Self::Number(value) => value.serialize(serializer),
Self::String(value) => serializer.serialize_str(value),
Self::Array(values) => values.serialize(serializer),
Self::Object(value) => value.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for OrderedJsonValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct OrderedValueVisitor;
impl<'de> Visitor<'de> for OrderedValueVisitor {
type Value = OrderedJsonValue;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a JSON value with ordered object members")
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(OrderedJsonValue::Null)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(OrderedJsonValue::Null)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
OrderedJsonValue::deserialize(deserializer)
}
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(OrderedJsonValue::Bool(value))
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(OrderedJsonValue::Number(value.into()))
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(OrderedJsonValue::Number(value.into()))
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
serde_json::Number::from_f64(value)
.map(OrderedJsonValue::Number)
.ok_or_else(|| E::custom("non-finite JSON number"))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(OrderedJsonValue::String(value.to_owned()))
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(OrderedJsonValue::String(value))
}
fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut values = Vec::new();
while let Some(value) = sequence.next_element()? {
values.push(value);
}
Ok(OrderedJsonValue::Array(values))
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut entries = Vec::new();
while let Some((key, value)) = map.next_entry()? {
entries.push((key, value));
}
Ok(OrderedJsonValue::Object(OrderedJsonObject(entries)))
}
}
deserializer.deserialize_any(OrderedValueVisitor)
}
}
impl Serialize for OrderedJsonObject {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (key, value) in &self.0 {
map.serialize_entry(key, value)?;
}
map.end()
}
}
impl<'de> Deserialize<'de> for OrderedJsonObject {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct OrderedObjectVisitor;
impl<'de> Visitor<'de> for OrderedObjectVisitor {
type Value = OrderedJsonObject;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("an object with ordered JSON members")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut entries = Vec::new();
while let Some((key, value)) = map.next_entry()? {
entries.push((key, value));
}
Ok(OrderedJsonObject(entries))
}
}
deserializer.deserialize_map(OrderedObjectVisitor)
}
}
fn read_tagged<'de, D>(
deserializer: D,
field: &'static str,
) -> Result<(String, OrderedJsonObject), D::Error>
where
D: Deserializer<'de>,
{
let body = OrderedJsonObject::deserialize(deserializer)?;
let mut tag = None;
for (key, value) in body.as_entries() {
if key != field {
continue;
}
if tag.is_some() {
return Err(D::Error::custom(format!(
"tagged object has duplicate `{field}` field"
)));
}
let OrderedJsonValue::String(value) = value else {
return Err(D::Error::custom(format!(
"tagged object has no string `{field}` field"
)));
};
tag = Some(value);
}
let Some(tag) = tag else {
return Err(D::Error::custom(format!(
"tagged object has no string `{field}` field"
)));
};
Ok((tag.to_string(), body))
}
fn read_ordered_tagged(
value: OrderedJsonValue,
field: &'static str,
) -> Result<(String, OrderedJsonObject), String> {
let OrderedJsonValue::Object(body) = value else {
return Err(format!("expected tagged object with `{field}` field"));
};
let mut tag = None;
for (key, value) in body.as_entries() {
if key != field {
continue;
}
if tag.is_some() {
return Err(format!("tagged object has duplicate `{field}` field"));
}
let OrderedJsonValue::String(value) = value else {
return Err(format!("tagged object has no string `{field}` field"));
};
tag = Some(value);
}
let Some(tag) = tag else {
return Err(format!("tagged object has no string `{field}` field"));
};
Ok((tag.to_string(), body))
}
fn ordered_field<'a>(body: &'a OrderedJsonObject, field: &str) -> Option<&'a OrderedJsonValue> {
body.as_entries()
.iter()
.find_map(|(key, value)| (key == field).then_some(value))
}
fn ordered_string(body: &OrderedJsonObject, field: &str) -> Result<String, String> {
match ordered_field(body, field) {
Some(OrderedJsonValue::String(value)) => Ok(value.clone()),
Some(_) => Err(format!("tagged object field `{field}` is not a string")),
None => Err(format!("tagged object has no `{field}` field")),
}
}
fn decode_running_image_evidence(value: OrderedJsonValue) -> Result<RunningImageEvidence, String> {
let (tag, body) = read_ordered_tagged(value, "method")?;
match tag.as_str() {
"linux_proc_sha256" => Ok(RunningImageEvidence::LinuxProcSha256 {
digest: ordered_string(&body, "digest")?,
}),
"macos_spawn_inode" => {
let device = ordered_field(&body, "device")
.and_then(|value| match value {
OrderedJsonValue::Number(number) => number.as_u64(),
_ => None,
})
.ok_or_else(|| "tagged object has no unsigned `device` field".to_string())?;
let inode = ordered_field(&body, "inode")
.and_then(|value| match value {
OrderedJsonValue::Number(number) => number.as_u64(),
_ => None,
})
.ok_or_else(|| "tagged object has no unsigned `inode` field".to_string())?;
Ok(RunningImageEvidence::MacosSpawnInode { device, inode })
}
_ => Ok(RunningImageEvidence::Unknown { tag, body }),
}
}
impl Serialize for ModuleDeclaredProvenance {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Reported { build } => ModuleDeclaredProvenanceWire::Reported {
build: build.clone(),
}
.serialize(serializer),
Self::Unverifiable => ModuleDeclaredProvenanceWire::Unverifiable.serialize(serializer),
Self::Unknown { body, .. } => body.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for ModuleDeclaredProvenance {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let (tag, value) = read_tagged(deserializer, "status")?;
match tag.as_str() {
"reported" => match serde_json::from_value(value.into_value())
.map_err(D::Error::custom)?
{
ModuleDeclaredProvenanceWire::Reported { build } => Ok(Self::Reported { build }),
ModuleDeclaredProvenanceWire::Unverifiable => unreachable!(),
},
"unverifiable" => {
match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
ModuleDeclaredProvenanceWire::Unverifiable => Ok(Self::Unverifiable),
ModuleDeclaredProvenanceWire::Reported { .. } => unreachable!(),
}
}
_ => Ok(Self::Unknown { tag, body: value }),
}
}
}
impl Serialize for RunningImageAgreement {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Match { evidence } => RunningImageAgreementWire::Match {
evidence: evidence.clone(),
}
.serialize(serializer),
Self::Mismatch { running, disk } => RunningImageAgreementWire::Mismatch {
running: running.clone(),
disk: disk.clone(),
}
.serialize(serializer),
Self::Unavailable { reason } => RunningImageAgreementWire::Unavailable {
reason: reason.clone(),
}
.serialize(serializer),
Self::Unknown { body, .. } => body.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for RunningImageAgreement {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let (tag, value) = read_tagged(deserializer, "status")?;
match tag.as_str() {
"match" => Ok(Self::Match {
evidence: decode_running_image_evidence(
ordered_field(&value, "evidence")
.cloned()
.ok_or_else(|| D::Error::custom("tagged object has no `evidence` field"))?,
)
.map_err(D::Error::custom)?,
}),
"mismatch" => Ok(Self::Mismatch {
running: decode_running_image_evidence(
ordered_field(&value, "running")
.cloned()
.ok_or_else(|| D::Error::custom("tagged object has no `running` field"))?,
)
.map_err(D::Error::custom)?,
disk: decode_running_image_evidence(
ordered_field(&value, "disk")
.cloned()
.ok_or_else(|| D::Error::custom("tagged object has no `disk` field"))?,
)
.map_err(D::Error::custom)?,
}),
"unavailable" => Ok(Self::Unavailable {
reason: serde_json::from_value(
ordered_field(&value, "reason")
.cloned()
.ok_or_else(|| D::Error::custom("tagged object has no `reason` field"))?
.into_value(),
)
.map_err(D::Error::custom)?,
}),
_ => Ok(Self::Unknown { tag, body: value }),
}
}
}
impl Serialize for RunningImageEvidence {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::LinuxProcSha256 { digest } => RunningImageEvidenceWire::LinuxProcSha256 {
digest: digest.clone(),
}
.serialize(serializer),
Self::MacosSpawnInode { device, inode } => RunningImageEvidenceWire::MacosSpawnInode {
device: *device,
inode: *inode,
}
.serialize(serializer),
Self::Unknown { body, .. } => body.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for RunningImageEvidence {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let (tag, value) = read_tagged(deserializer, "method")?;
match tag.as_str() {
"linux_proc_sha256" => {
match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
RunningImageEvidenceWire::LinuxProcSha256 { digest } => {
Ok(Self::LinuxProcSha256 { digest })
}
_ => unreachable!(),
}
}
"macos_spawn_inode" => {
match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
RunningImageEvidenceWire::MacosSpawnInode { device, inode } => {
Ok(Self::MacosSpawnInode { device, inode })
}
_ => unreachable!(),
}
}
_ => Ok(Self::Unknown { tag, body: value }),
}
}
}
impl Serialize for SupervisorRouteConsumer {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Reserved { module_id } => SupervisorRouteConsumerWire::Reserved {
module_id: module_id.clone(),
}
.serialize(serializer),
Self::Direct { connection_id } => SupervisorRouteConsumerWire::Direct {
connection_id: *connection_id,
}
.serialize(serializer),
Self::Unknown { body, .. } => body.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for SupervisorRouteConsumer {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let (tag, value) = read_tagged(deserializer, "kind")?;
match tag.as_str() {
"reserved" => {
match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
SupervisorRouteConsumerWire::Reserved { module_id } => {
Ok(Self::Reserved { module_id })
}
_ => unreachable!(),
}
}
"direct" => {
match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
SupervisorRouteConsumerWire::Direct { connection_id } => {
Ok(Self::Direct { connection_id })
}
_ => unreachable!(),
}
}
_ => Ok(Self::Unknown { tag, body: value }),
}
}
}
impl Serialize for StderrCaptureState {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Captured => StderrCaptureStateWire::Captured.serialize(serializer),
Self::Incomplete { reason } => StderrCaptureStateWire::Incomplete {
reason: reason.clone(),
}
.serialize(serializer),
Self::NotCaptured { reason } => StderrCaptureStateWire::NotCaptured {
reason: reason.clone(),
}
.serialize(serializer),
Self::Unknown { body, .. } => body.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for StderrCaptureState {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let (tag, value) = read_tagged(deserializer, "state")?;
match tag.as_str() {
"captured" => {
match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
StderrCaptureStateWire::Captured => Ok(Self::Captured),
_ => unreachable!(),
}
}
"incomplete" => match serde_json::from_value(value.into_value())
.map_err(D::Error::custom)?
{
StderrCaptureStateWire::Incomplete { reason } => Ok(Self::Incomplete { reason }),
_ => unreachable!(),
},
"not_captured" => match serde_json::from_value(value.into_value())
.map_err(D::Error::custom)?
{
StderrCaptureStateWire::NotCaptured { reason } => Ok(Self::NotCaptured { reason }),
_ => unreachable!(),
},
_ => Ok(Self::Unknown { tag, body: value }),
}
}
}
impl Serialize for StderrTailEntry {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Line { text, truncated } => StderrTailEntryWire::Line {
text: text.clone(),
truncated: *truncated,
}
.serialize(serializer),
Self::ProcessStart => StderrTailEntryWire::ProcessStart.serialize(serializer),
Self::Unknown { body, .. } => body.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for StderrTailEntry {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let (tag, value) = read_tagged(deserializer, "kind")?;
match tag.as_str() {
"line" => match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
StderrTailEntryWire::Line { text, truncated } => Ok(Self::Line { text, truncated }),
_ => unreachable!(),
},
"process_start" => {
match serde_json::from_value(value.into_value()).map_err(D::Error::custom)? {
StderrTailEntryWire::ProcessStart => Ok(Self::ProcessStart),
_ => unreachable!(),
}
}
_ => Ok(Self::Unknown { tag, body: value }),
}
}
}
fn is_zero_u64(value: &u64) -> bool {
*value == 0
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TerminalHistory {
pub daemon_started_at_ms: u64,
pub entries: Vec<TerminalEntry>,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub dropped: u64,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub journal_skipped_lines: u64,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub journal_read_errors: u64,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub journal_write_failures: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TerminalEntry {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub daemon_incarnation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_signal: Option<i32>,
pub at_ms: u64,
pub disposition: TerminalDisposition,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_kind: Option<TerminalExitKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disposition_detail: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TerminalExitKind {
Clean,
Crash,
DeliberateSeverance,
Unknown(String),
}
impl TerminalExitKind {
fn wire_name(&self) -> &str {
match self {
Self::Clean => "clean",
Self::Crash => "crash",
Self::DeliberateSeverance => "deliberate_severance",
Self::Unknown(value) => value,
}
}
}
impl Serialize for TerminalExitKind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.wire_name())
}
}
impl<'de> Deserialize<'de> for TerminalExitKind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(match value.as_str() {
"clean" => Self::Clean,
"crash" => Self::Crash,
"deliberate_severance" => Self::DeliberateSeverance,
_ => Self::Unknown(value),
})
}
}
open_string_enum! {
TerminalDisposition {
Stopped => "stopped",
Disabled => "disabled",
Failed => "failed",
Restarting => "restarting",
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PollKind {
Status,
Liveness,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CatalogEntry {
pub module_id: String,
#[serde(default = "default_true")]
pub ready: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub module_version: Option<String>,
pub roles: Vec<ProviderRole>,
pub control_ops: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<CapabilityDeclarations>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub self_signals: Option<Vec<SelfSignalDeclaration>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CapabilityRequirementStatus {
pub consumer: String,
pub capability: String,
pub need: String,
pub verdict: String,
pub episode_seq: u64,
pub config_satisfiable: bool,
pub runtime_available: bool,
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SupervisorRescanResult {
pub added: Vec<String>,
pub removed: Vec<String>,
pub changed_pending_reload: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub enabled_changes: Vec<String>,
pub unchanged: u32,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub preview: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub restart_required: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capability_warnings: Vec<String>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ModuleProtocol {
#[default]
Subc,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SupervisorEntry {
pub module_id: String,
pub state: String,
pub enabled: bool,
pub live: bool,
#[serde(default)]
pub protocol: ModuleProtocol,
pub health: SupervisorHealthStatus,
#[serde(default)]
pub last_probe_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_exit_code: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_exit_signal: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_exit_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_exit_kind: Option<TerminalExitKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub restart_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_restarts: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lifetime_restarts: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spawn_generation: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub restart_window_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub drain_timeout_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub restart_backoff_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub restart_max_backoff_ms: Option<u64>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SupervisorHealthStatus {
Ok,
Degraded,
Failing,
Unresponsive,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SupervisorHealthEntry {
pub module_id: String,
pub status: SupervisorHealthStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metrics: Option<serde_json::Value>,
pub consecutive_failures: u32,
#[serde(default)]
pub late_answer_count: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_late_answer_latency_ms: Option<u64>,
#[serde(default)]
pub last_action: Option<String>,
#[serde(default)]
pub last_action_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_probe_ms: Option<u64>,
}
#[cfg(test)]
mod tests {
use super::*;
use subc_protocol::{BindIdentity, RouteTarget};
#[test]
fn legacy_terminal_decoder_ignores_deliberate_severance_kind() {
let entry = TerminalEntry {
daemon_incarnation: Some("daemon-before-restart".into()),
exit_code: Some(1),
exit_signal: None,
at_ms: 1_700_000_000_123,
disposition: TerminalDisposition::Restarting,
exit_kind: Some(TerminalExitKind::DeliberateSeverance),
disposition_detail: None,
};
let wire = serde_json::to_string(&entry).expect("terminal entry serializes");
assert_eq!(
serde_json::from_str::<serde_json::Value>(&wire).expect("terminal entry is JSON")
["exit_kind"],
"deliberate_severance"
);
#[derive(serde::Deserialize)]
struct LegacyTerminalEntry {
exit_code: Option<i32>,
exit_signal: Option<i32>,
at_ms: u64,
disposition: TerminalDisposition,
}
let decoded: LegacyTerminalEntry =
serde_json::from_str(&wire).expect("legacy decoder keeps the terminal record");
assert_eq!(decoded.exit_code, Some(1));
assert_eq!(decoded.exit_signal, None);
assert_eq!(decoded.at_ms, 1_700_000_000_123);
assert_eq!(decoded.disposition, TerminalDisposition::Restarting);
let future_wire = wire.replace("deliberate_severance", "future_exit_kind");
let future: TerminalEntry =
serde_json::from_str(&future_wire).expect("new decoder keeps a future terminal kind");
assert_eq!(
future.exit_kind,
Some(TerminalExitKind::Unknown("future_exit_kind".to_string()))
);
}
#[test]
fn terminal_incarnation_is_optional_for_older_daemons() {
let entry: TerminalEntry = serde_json::from_value(serde_json::json!({
"at_ms": 123,
"disposition": "stopped"
}))
.unwrap();
let encoded = serde_json::to_value(&entry).unwrap();
assert_eq!(
(entry.daemon_incarnation, encoded.get("daemon_incarnation")),
(None, None)
);
}
#[test]
fn route_poll_uses_kind_field() {
let body = serde_json::to_value(ClientControlRequest::RoutePoll {
route_channel: 7,
route_epoch: 11,
kind: PollKind::Status,
})
.unwrap();
assert_eq!(body["op"], "route.poll");
assert_eq!(body["route_epoch"], 11);
assert_eq!(body["kind"], "status");
assert!(body.get("op").is_some());
}
#[test]
fn route_open_is_internally_tagged() {
let request = ClientControlRequest::RouteOpen {
target: RouteTarget::ToolProvider {
module_id: "aft".to_string(),
},
identity: BindIdentity::new("/tmp/project", "opencode", "session-1"),
consumer_identity: None,
consumer_capabilities: None,
admission_facts: None,
};
let body = serde_json::to_value(request).unwrap();
assert_eq!(body["op"], "route.open");
assert_eq!(body["target"]["kind"], "tool_provider");
assert!(body.get("consumer_identity").is_none());
assert!(body.get("consumer_capabilities").is_none());
}
#[test]
fn route_open_without_optional_fields_still_decodes() {
let body = serde_json::json!({
"op": "route.open",
"target": { "kind": "tool_provider", "module_id": "aft" },
"identity": {
"project_root": "/tmp/project",
"harness": "opencode",
"session": "session-1"
}
});
let decoded: ClientControlRequest = serde_json::from_value(body).unwrap();
let ClientControlRequest::RouteOpen {
consumer_identity,
consumer_capabilities,
admission_facts,
..
} = decoded
else {
panic!("decoded wrong request variant");
};
assert_eq!(consumer_identity, None);
assert_eq!(consumer_capabilities, None);
assert_eq!(admission_facts, None);
}
#[test]
fn new_route_closed_decoder_defaults_fields_absent_from_old_daemon() {
let old_wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0}"#;
let decoded: ClientControlPush = serde_json::from_str(old_wire).unwrap();
match decoded {
ClientControlPush::RouteClosed {
excluded_subscriptions,
terminal,
..
} => {
assert_eq!(excluded_subscriptions, 0);
assert_eq!(terminal, None);
}
other => panic!("unexpected push: {other:?}"),
}
assert!(!serde_json::to_string(&decoded)
.unwrap()
.contains("terminal"));
}
#[test]
fn old_route_closed_decoder_ignores_new_terminal_field() {
#[derive(serde::Deserialize)]
#[serde(tag = "op")]
enum LegacyClientControlPush {
#[serde(rename = "route.closed")]
RouteClosed {
module_id: String,
reason: RouteCloseReason,
drained: bool,
abandoned: u32,
},
}
let wire = r#"{"op":"route.closed","module_id":"aft-tools","reason":"crash","drained":false,"abandoned":0,"excluded_subscriptions":3,"terminal":true}"#;
let decoded: LegacyClientControlPush = serde_json::from_str(wire).unwrap();
match decoded {
LegacyClientControlPush::RouteClosed {
module_id,
reason,
drained,
abandoned,
} => {
assert_eq!(module_id, "aft-tools");
assert_eq!(reason, RouteCloseReason::Crash);
assert!(!drained);
assert_eq!(abandoned, 0);
}
}
}
#[test]
fn supervisor_routes_is_a_control_plane_request() {
let body = serde_json::json!({
"op": "supervisor.routes",
"module_id": "aft"
});
let request: ClientControlRequest = serde_json::from_value(body.clone()).unwrap();
assert_eq!(serde_json::to_value(request).unwrap(), body);
}
#[test]
fn diagnostic_string_enums_retain_unknown_wire_values() {
let reason: RunningImageUnavailableReason =
serde_json::from_str("\"future_reason\"").unwrap();
let disposition: TerminalDisposition =
serde_json::from_str("\"future_disposition\"").unwrap();
assert_eq!(
reason,
RunningImageUnavailableReason::Unknown("future_reason".to_string())
);
assert_eq!(
disposition,
TerminalDisposition::Unknown("future_disposition".to_string())
);
}
#[test]
fn diagnostic_string_enums_preserve_existing_wire_names() {
let names = [
(RunningImageUnavailableReason::NotRunning, "not_running"),
(
RunningImageUnavailableReason::UnsupportedPlatform,
"unsupported_platform",
),
(
RunningImageUnavailableReason::RunningExecutableUnreadable,
"running_executable_unreadable",
),
(
RunningImageUnavailableReason::SpawnedPathUnreadable,
"spawned_path_unreadable",
),
(RunningImageUnavailableReason::HashFailed, "hash_failed"),
(
RunningImageUnavailableReason::ProcessIdentityUnconfirmed,
"process_identity_unconfirmed",
),
];
for (value, expected) in names {
let wire = serde_json::to_string(&value).unwrap();
assert_eq!(wire, format!("\"{expected}\""));
let decoded: RunningImageUnavailableReason = serde_json::from_str(&wire).unwrap();
assert_eq!(decoded, value);
}
for (value, expected) in [
(TerminalDisposition::Stopped, "stopped"),
(TerminalDisposition::Disabled, "disabled"),
(TerminalDisposition::Failed, "failed"),
(TerminalDisposition::Restarting, "restarting"),
] {
let wire = serde_json::to_string(&value).unwrap();
assert_eq!(wire, format!("\"{expected}\""));
let decoded: TerminalDisposition = serde_json::from_str(&wire).unwrap();
assert_eq!(decoded, value);
}
}
#[test]
fn diagnostic_string_enums_reject_non_string_bodies() {
assert!(serde_json::from_str::<RunningImageUnavailableReason>("42").is_err());
assert!(serde_json::from_str::<TerminalDisposition>("{\"value\":\"failed\"}").is_err());
}
#[test]
fn unknown_provenance_reason_does_not_discard_healthy_siblings() {
let body = serde_json::json!({
"op": "supervisor.provenance",
"daemon": {
"daemon_build": {},
"daemon_observed": {
"running_image": {
"status": "unavailable",
"reason": "not_running"
}
}
},
"modules": [
{
"module_id": "future",
"module_declared": { "status": "unverifiable" },
"daemon_observed": {
"running_image": {
"status": "unavailable",
"reason": "future_reason"
}
}
},
{
"module_id": "healthy-a",
"module_declared": { "status": "unverifiable" },
"daemon_observed": {
"running_image": {
"status": "match",
"evidence": {
"method": "linux_proc_sha256",
"digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
}
}
},
{
"module_id": "healthy-b",
"module_declared": { "status": "unverifiable" },
"daemon_observed": {
"running_image": {
"status": "unavailable",
"reason": "unsupported_platform"
}
}
}
]
});
let decoded: ClientControlResponse = serde_json::from_value(body).unwrap();
let ClientControlResponse::SupervisorProvenance { modules, .. } = decoded else {
panic!("decoded wrong response variant");
};
assert_eq!(modules.len(), 3);
assert_eq!(modules[0].module_id, "future");
assert_eq!(
modules[0].daemon_observed.running_image,
RunningImageAgreement::Unavailable {
reason: RunningImageUnavailableReason::Unknown("future_reason".to_string())
}
);
assert_eq!(modules[1].module_id, "healthy-a");
assert_eq!(modules[2].module_id, "healthy-b");
}
#[test]
fn tagged_unknown_values_retain_tag_and_body() {
macro_rules! assert_unknown_round_trip {
($ty:ident, $field:literal, $value:expr) => {
let value = $value;
let wire = serde_json::to_string(&value).unwrap();
let decoded: $ty = serde_json::from_str(&wire).unwrap();
match decoded {
$ty::Unknown { tag, body } => {
assert_eq!(tag, value[$field].as_str().unwrap());
assert_eq!(serde_json::to_value(&body).unwrap(), value);
}
_ => panic!("decoded known variant"),
}
};
}
assert_unknown_round_trip!(
ModuleDeclaredProvenance,
"status",
serde_json::json!({"status": "future", "build": {"version": 7}})
);
assert_unknown_round_trip!(
RunningImageAgreement,
"status",
serde_json::json!({"status": "future", "evidence": {"digest": "abc"}})
);
assert_unknown_round_trip!(
RunningImageEvidence,
"method",
serde_json::json!({"method": "future", "digest": "abc"})
);
assert_unknown_round_trip!(
SupervisorRouteConsumer,
"kind",
serde_json::json!({"kind": "future", "module_id": "m"})
);
assert_unknown_round_trip!(
StderrCaptureState,
"state",
serde_json::json!({"state": "future", "reason": "because"})
);
assert_unknown_round_trip!(
StderrTailEntry,
"kind",
serde_json::json!({"kind": "future", "text": "line"})
);
}
#[test]
fn tagged_unknown_values_round_trip_the_original_json() {
let wire = r#"{"kind":"future_consumer","detail":{"z":1}}"#;
let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
}
#[test]
fn tagged_unknown_values_round_trip_trailing_tag() {
let route_wire = r#"{"detail":{"z":1},"kind":"future_consumer"}"#;
let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
let stderr_wire = r#"{"reason":"because","state":"future_state"}"#;
let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
}
#[test]
fn tagged_unknown_values_round_trip_middle_tag() {
let route_wire = r#"{"a":1,"kind":"future_x","b":2}"#;
let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
let stderr_wire = r#"{"a":1,"state":"future_state","b":2}"#;
let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
}
#[test]
fn tagged_unknown_values_round_trip_deep_payload() {
let route_wire = r#"{"a":{"n":[1,2]},"kind":"future_x","zz":"s","b":null}"#;
let route: SupervisorRouteConsumer = serde_json::from_str(route_wire).unwrap();
assert_eq!(serde_json::to_string(&route).unwrap(), route_wire);
let stderr_wire = r#"{"a":{"n":[1,2]},"state":"future_state","zz":"s","b":null}"#;
let stderr: StderrCaptureState = serde_json::from_str(stderr_wire).unwrap();
assert_eq!(serde_json::to_string(&stderr).unwrap(), stderr_wire);
}
#[test]
fn tagged_unknown_values_reject_non_object_bodies() {
for wire in ["42", r#""future""#, "[]"] {
assert!(serde_json::from_str::<SupervisorRouteConsumer>(wire).is_err());
assert!(serde_json::from_str::<StderrCaptureState>(wire).is_err());
}
}
#[test]
fn duplicate_discriminators_reject_without_panicking() {
assert_eq!(
serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"unverifiable"}"#)
.unwrap(),
ModuleDeclaredProvenance::Unverifiable
);
match serde_json::from_str::<ModuleDeclaredProvenance>(r#"{"status":"future_thing"}"#)
.unwrap()
{
ModuleDeclaredProvenance::Unknown { tag, .. } => assert_eq!(tag, "future_thing"),
_ => panic!("future discriminator decoded as a known variant"),
}
let wires = [
r#"{"status":"reported","status":"unverifiable"}"#,
r#"{"status":"unverifiable","status":"reported"}"#,
r#"{"status":"reported","build":{},"status":"unverifiable"}"#,
r#"{"status":"unverifiable","build":{},"status":"reported"}"#,
];
for wire in wires {
let result =
std::panic::catch_unwind(|| serde_json::from_str::<ModuleDeclaredProvenance>(wire));
assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
assert!(
result.unwrap().is_err(),
"duplicate discriminator decoded: {wire}"
);
}
let wire = r#"{"state":"captured","state":"incomplete","reason":"x"}"#;
let result = std::panic::catch_unwind(|| serde_json::from_str::<StderrCaptureState>(wire));
assert!(result.is_ok(), "duplicate discriminator panicked: {wire}");
assert!(
result.unwrap().is_err(),
"duplicate discriminator decoded: {wire}"
);
}
#[test]
fn nested_unknown_values_round_trip_without_normalizing_member_order() {
let known_wire =
r#"{"status":"match","evidence":{"method":"linux_proc_sha256","digest":"abc"}}"#;
let known: RunningImageAgreement = serde_json::from_str(known_wire).unwrap();
assert_eq!(serde_json::to_string(&known).unwrap(), known_wire);
for wire in [
r#"{"kind":"future_x","detail":{"zeta":1,"alpha":2}}"#,
r#"{"kind":"future_x","d":{"b":{"zz":1,"aa":2}}}"#,
] {
let decoded: SupervisorRouteConsumer = serde_json::from_str(wire).unwrap();
assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
}
for wire in [
r#"{"status":"match","evidence":{"method":"future_probe","zz":1,"aa":2}}"#,
r#"{"status":"match","evidence":{"method":"future_probe","d":{"zz":1,"aa":2}}}"#,
] {
let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
}
let wire = r#"{"status":"mismatch","running":{"detail":{"z":1},"method":"future_running"},"disk":{"method":"future_disk","detail":{"z":1}}}"#;
let decoded: RunningImageAgreement = serde_json::from_str(wire).unwrap();
assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
let wire = r#"{"capture":{"state":"captured"},"entries":[{"detail":{"z":1,"a":2},"kind":"future_line"},{"kind":"future_restart","meta":{"b":{"zz":1,"aa":2}}}]}"#;
let decoded: StderrTail = serde_json::from_str(wire).unwrap();
assert_eq!(serde_json::to_string(&decoded).unwrap(), wire);
}
#[test]
fn tagged_unknown_member_does_not_discard_known_siblings() {
let body = serde_json::json!({
"modules": [{
"module_id": "target",
"routes": [
{"consumer": {"kind": "future_consumer", "module_id": "m", "detail": {"retry": true}}, "age_ms": 0, "draining": false},
{"consumer": {"kind": "direct", "connection_id": 7}, "age_ms": 0, "draining": false}
]
}]
});
let decoded: ClientControlResponse = serde_json::from_value(
serde_json::json!({"op": "supervisor.routes", "modules": body["modules"]}),
)
.unwrap();
let ClientControlResponse::SupervisorRoutes { modules } = decoded else {
panic!("decoded wrong response variant");
};
assert_eq!(modules[0].routes.len(), 2);
assert_eq!(
modules[0].routes[1].consumer,
SupervisorRouteConsumer::Direct { connection_id: 7 }
);
}
}