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")]
46pub enum ModuleControlRequest {
47    #[serde(rename = "route.bind")]
48    RouteBind {
49        route_channel: u16,
50        epoch: u32,
51        target: RouteTarget,
52        identity: BindIdentity,
53        #[serde(default, skip_serializing_if = "Option::is_none")]
54        principal: Option<Principal>,
55        /// Consumer-declared reverse-request capabilities for the route. This is
56        /// an unverified declaration, not a privilege grant; if a consumer
57        /// over-declares, providers may still send reverse requests that later
58        /// time out or deny. Providers must treat an absent field as no
59        /// reverse-request capability. The vocabulary is open strings; known MCP
60        /// method-family values today are "elicitation", "sampling", and
61        /// "roots".
62        #[serde(default, skip_serializing_if = "Option::is_none")]
63        consumer_capabilities: Option<Vec<String>>,
64    },
65    #[serde(rename = "health.check")]
66    HealthCheck {},
67}
68
69/// Module-to-subc channel-0 response body.
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71#[serde(tag = "op")]
72pub enum ModuleControlResponse {
73    /// ACK-only success. Rejections use the `FrameType::Error` lane.
74    #[serde(rename = "route.bind")]
75    RouteBindAck {},
76    #[serde(rename = "health.check")]
77    HealthCheck {
78        status: HealthStatus,
79        #[serde(default, skip_serializing_if = "Option::is_none")]
80        detail: Option<String>,
81        #[serde(default, skip_serializing_if = "Option::is_none")]
82        metrics: Option<Value>,
83    },
84}
85
86/// Module-originated channel-0 control RPC body.
87///
88/// This is intentionally separate from [`ModuleControlRequest`]: that enum is the
89/// daemon-to-module direction (`route.bind`, `health.check`), while these bodies
90/// are sent by an already-registered module to subc on a `REQUEST` frame.
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92#[serde(tag = "op")]
93pub enum ModuleControlRequestFromModule {
94    #[serde(rename = "catalog.update")]
95    CatalogUpdate { provides: Vec<ProviderRole> },
96}
97
98/// subc's channel-0 response body for module-originated control RPCs.
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
100#[serde(tag = "op")]
101pub enum ModuleControlResponseToModule {
102    #[serde(rename = "catalog.update")]
103    CatalogUpdate {},
104}
105
106impl From<HealthReport> for ModuleControlResponse {
107    fn from(report: HealthReport) -> Self {
108        Self::HealthCheck {
109            status: report.status,
110            detail: report.detail,
111            metrics: report.metrics,
112        }
113    }
114}
115
116impl ModuleControlResponse {
117    pub fn health_report(&self) -> Option<HealthReport> {
118        match self {
119            Self::HealthCheck {
120                status,
121                detail,
122                metrics,
123            } => Some(HealthReport {
124                status: *status,
125                detail: detail.clone(),
126                metrics: metrics.clone(),
127            }),
128            Self::RouteBindAck {} => None,
129        }
130    }
131}
132
133/// Module-to-subc channel-0 push body.
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
135#[serde(tag = "op")]
136pub enum ModuleControlPush {
137    #[serde(rename = "route.status")]
138    RouteStatus {
139        route_channel: u16,
140        route_epoch: u32,
141        status: String,
142    },
143}