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}