Skip to main content

subc_protocol/
session.rs

1//! Session route control wire contract.
2//!
3//! subc has two distinct channel-0 handshakes. Module registration is the
4//! module-to-subc `HELLO`/`HELLO_ACK` handshake that registers the manifest and
5//! liveness. Route bind is the client-to-subc-to-module request/response
6//! handshake that binds one client route to a module route channel.
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::{manifest::ProviderRole, BindIdentity, Principal, RouteTarget};
12
13pub const MODULE_CONTROL_OP_HEALTH_CHECK: &str = "health.check";
14pub const MODULE_TO_SUBC_OP_CATALOG_UPDATE: &str = "catalog.update";
15
16#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "snake_case")]
18pub enum HealthStatus {
19    Ok,
20    Degraded,
21    Failing,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25pub struct HealthReport {
26    pub status: HealthStatus,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub detail: Option<String>,
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub metrics: Option<Value>,
31}
32
33impl HealthReport {
34    pub fn ok() -> Self {
35        Self {
36            status: HealthStatus::Ok,
37            detail: None,
38            metrics: None,
39        }
40    }
41}
42
43/// subc-to-module channel-0 control RPC body.
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
45#[serde(tag = "op")]
46// RouteBind carries the complete bind metadata, while HealthCheck is a marker;
47// preserving the direct wire shape is more useful than boxing every bind field.
48#[allow(clippy::large_enum_variant)]
49pub enum ModuleControlRequest {
50    #[serde(rename = "route.bind")]
51    RouteBind {
52        route_channel: u16,
53        epoch: u32,
54        target: RouteTarget,
55        identity: BindIdentity,
56        /// The daemon's attestation of the consumer, and the only field here a
57        /// provider may grant privilege on.
58        ///
59        /// `Reserved` is minted at exactly one place in the daemon, on the branch
60        /// where the consumer's launch nonce matched a supervised spawn — the
61        /// function that checks is the function that mints, so the value cannot
62        /// exist without the check having run. That property is what a provider is
63        /// relying on, and it is the reason to key authority on this rather than on
64        /// `identity`, which is client-supplied and unattested (see BindIdentity).
65        ///
66        /// Absent means the daemon made no attestation, which is not the same as a
67        /// denial: it is the shape a pre-attestation peer sends. Treat it as
68        /// unattested rather than as trusted-by-default.
69        #[serde(default, skip_serializing_if = "Option::is_none")]
70        principal: Option<Principal>,
71        /// Consumer-declared reverse-request capabilities for the route. This is
72        /// an unverified declaration, not a privilege grant; if a consumer
73        /// over-declares, providers may still send reverse requests that later
74        /// time out or deny. Providers must treat an absent field as no
75        /// reverse-request capability. The vocabulary is open strings; known MCP
76        /// method-family values today are "elicitation", "sampling", and
77        /// "roots".
78        #[serde(default, skip_serializing_if = "Option::is_none")]
79        consumer_capabilities: Option<Vec<String>>,
80        /// Opaque admission facts supplied by the configured carrier module.
81        #[serde(default, skip_serializing_if = "Option::is_none")]
82        admission_facts: Option<Value>,
83    },
84    #[serde(rename = "health.check")]
85    HealthCheck {},
86}
87
88/// Module-to-subc channel-0 response body.
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
90#[serde(tag = "op")]
91pub enum ModuleControlResponse {
92    /// ACK-only success. Rejections use the `FrameType::Error` lane.
93    #[serde(rename = "route.bind")]
94    RouteBindAck {},
95    #[serde(rename = "health.check")]
96    HealthCheck {
97        status: HealthStatus,
98        #[serde(default, skip_serializing_if = "Option::is_none")]
99        detail: Option<String>,
100        #[serde(default, skip_serializing_if = "Option::is_none")]
101        metrics: Option<Value>,
102    },
103}
104
105/// Module-originated channel-0 control RPC body.
106///
107/// This is intentionally separate from [`ModuleControlRequest`]: that enum is the
108/// daemon-to-module direction (`route.bind`, `health.check`), while these bodies
109/// are sent by an already-registered module to subc on a `REQUEST` frame.
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
111#[serde(tag = "op")]
112pub enum ModuleControlRequestFromModule {
113    #[serde(rename = "catalog.update")]
114    CatalogUpdate { provides: Vec<ProviderRole> },
115}
116
117/// subc's channel-0 response body for module-originated control RPCs.
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119#[serde(tag = "op")]
120pub enum ModuleControlResponseToModule {
121    #[serde(rename = "catalog.update")]
122    CatalogUpdate {},
123}
124
125impl From<HealthReport> for ModuleControlResponse {
126    fn from(report: HealthReport) -> Self {
127        Self::HealthCheck {
128            status: report.status,
129            detail: report.detail,
130            metrics: report.metrics,
131        }
132    }
133}
134
135impl ModuleControlResponse {
136    pub fn health_report(&self) -> Option<HealthReport> {
137        match self {
138            Self::HealthCheck {
139                status,
140                detail,
141                metrics,
142            } => Some(HealthReport {
143                status: *status,
144                detail: detail.clone(),
145                metrics: metrics.clone(),
146            }),
147            Self::RouteBindAck {} => None,
148        }
149    }
150}
151
152/// Module-to-subc channel-0 push body.
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154#[serde(tag = "op")]
155pub enum ModuleControlPush {
156    #[serde(rename = "route.status")]
157    RouteStatus {
158        route_channel: u16,
159        route_epoch: u32,
160        status: String,
161    },
162}