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