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, RouteCloseReason, 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/// One-way subc-to-module channel-0 control command.
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93#[serde(tag = "op")]
94pub enum ModuleControlCommand {
95    #[serde(rename = "module.draining")]
96    Draining {
97        reason: RouteCloseReason,
98        /// Absolute Unix-millisecond deadline for this drain.
99        ///
100        /// WALL CLOCK, WHILE THE DAEMON ENFORCES THE CEILING ON A
101        /// SUSPEND-EXCLUDING MONOTONIC CLOCK (`Instant`, supervise.rs). Both
102        /// processes share one host so `CLOCK_REALTIME` agrees exactly, and the
103        /// two clocks diverge only across host sleep: `Instant` stops, wall does
104        /// not. So a module that sleeps mid-drain wakes to a deadline further in
105        /// the past than the daemon's own ceiling, computes LESS remaining time
106        /// than it has, and seals early.
107        ///
108        /// That direction is deliberate and is the safe one — a module stopping
109        /// early loses nothing, since the daemon kills at its own ceiling
110        /// regardless. The reverse (a module believing it has time the daemon
111        /// has already spent) is the failure this ordering avoids. A module must
112        /// therefore treat this as "no later than", never as a grant.
113        deadline_ms: u64,
114    },
115}
116
117/// Module-to-subc channel-0 response body.
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
119#[serde(tag = "op")]
120pub enum ModuleControlResponse {
121    /// ACK-only success. Rejections use the `FrameType::Error` lane.
122    #[serde(rename = "route.bind")]
123    RouteBindAck {},
124    #[serde(rename = "health.check")]
125    HealthCheck {
126        status: HealthStatus,
127        #[serde(default, skip_serializing_if = "Option::is_none")]
128        detail: Option<String>,
129        #[serde(default, skip_serializing_if = "Option::is_none")]
130        metrics: Option<Value>,
131    },
132}
133
134/// Module-originated channel-0 control RPC body.
135///
136/// This is intentionally separate from [`ModuleControlRequest`]: that enum is the
137/// daemon-to-module direction (`route.bind`, `health.check`), while these bodies
138/// are sent by an already-registered module to subc on a `REQUEST` frame.
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
140#[serde(tag = "op")]
141pub enum ModuleControlRequestFromModule {
142    #[serde(rename = "catalog.update")]
143    CatalogUpdate {
144        provides: Vec<ProviderRole>,
145        /// An attested replacement for the static capability declaration emitted
146        /// by the module's current manifest. `None` preserves the prior
147        /// declaration so existing role-only catalog updates remain byte-identical.
148        #[serde(default, skip_serializing_if = "Option::is_none")]
149        capabilities: Option<CapabilityDeclarations>,
150    },
151}
152
153/// subc's channel-0 response body for module-originated control RPCs.
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
155#[serde(tag = "op")]
156pub enum ModuleControlResponseToModule {
157    #[serde(rename = "catalog.update")]
158    CatalogUpdate {},
159}
160
161impl From<HealthReport> for ModuleControlResponse {
162    fn from(report: HealthReport) -> Self {
163        Self::HealthCheck {
164            status: report.status,
165            detail: report.detail,
166            metrics: report.metrics,
167        }
168    }
169}
170
171impl ModuleControlResponse {
172    pub fn health_report(&self) -> Option<HealthReport> {
173        match self {
174            Self::HealthCheck {
175                status,
176                detail,
177                metrics,
178            } => Some(HealthReport {
179                status: *status,
180                detail: detail.clone(),
181                metrics: metrics.clone(),
182            }),
183            Self::RouteBindAck {} => None,
184        }
185    }
186}
187
188/// Module-to-subc channel-0 push body.
189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
190#[serde(tag = "op")]
191pub enum ModuleControlPush {
192    #[serde(rename = "route.status")]
193    RouteStatus {
194        route_channel: u16,
195        route_epoch: u32,
196        status: String,
197    },
198}