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