Skip to main content

subc_protocol/
manifest.rs

1//! Capability manifest schema for subc modules.
2//!
3//! All v1 modules are supervised singletons: one long-lived process per
4//! per-user machine. The manifest intentionally has **no `cardinality` field**.
5//! subc routes by module kind plus channel, while any finer demultiplexing
6//! (for example, AFT's per-project actor map) remains internal to the singleton
7//! module.
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12/// A module's full declared participation in the subc mesh.
13#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
14pub struct ModuleManifest {
15    pub module_id: String,
16    pub module_version: String,
17    pub protocol_ver: u8,
18    pub trust_tier: TrustTier,
19    pub provides: Vec<ProviderRole>,
20    pub consumes: Vec<ConsumerRole>,
21    pub scheduled_tasks: Vec<ScheduledTask>,
22    pub bindings: Bindings,
23}
24
25/// Trust gate applied by subc before routing capabilities.
26#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
27#[serde(rename_all = "snake_case")]
28pub enum TrustTier {
29    FirstParty,
30    Reviewed,
31    Untrusted,
32}
33
34/// Provider capabilities exposed by a module.
35///
36/// The role set is closed for protocol v1; unknown role tags fail serde decode.
37#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
38#[serde(tag = "role", rename_all = "snake_case")]
39pub enum ProviderRole {
40    ToolProvider {
41        tools: Vec<Tool>,
42        identity_scope: Vec<IdentityScope>,
43        concurrency: Concurrency,
44        emits_push: bool,
45        sub_supervises: bool,
46    },
47    PipelineStage {
48        stage: PipelineStageKind,
49        applies_to: PipelineAppliesTo,
50        interface: String,
51        declares_frozen_floor: bool,
52        needs_signals: Vec<String>,
53        conformance_class: String,
54    },
55    ManagementSurface {
56        operations: Vec<ManagementOperation>,
57        config_schema: Value,
58        observability: Vec<ObservabilitySurface>,
59        identity_scope: Vec<IdentityScope>,
60    },
61    InternalService {
62        service_id: String,
63        transport: InternalTransport,
64        agent_facing: bool,
65        operations: Vec<String>,
66    },
67}
68
69/// How a tool's side effects are fenced for durable at-most-once handling.
70///
71/// Classified on a tool's externally-observable effects, never inferred from
72/// the module's concurrency lane:
73/// - `Pure`: no observable side effect (reads, searches, cache warming) — safe
74///   to re-run after an indeterminate outcome.
75/// - `Mutating`: a fenceable external side effect such as a file write — a
76///   re-run risks a duplicate effect, so an indeterminate outcome must not
77///   auto-retry.
78/// - `Unfenceable`: a side effect that cannot be fenced or safely replayed,
79///   such as running a shell command — never auto-re-run on an indeterminate
80///   outcome.
81#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
82#[serde(rename_all = "snake_case")]
83pub enum ExecutionMode {
84    Pure,
85    Mutating,
86    Unfenceable,
87}
88
89/// Tool-plane capability exposed by a `tool_provider`.
90#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
91pub struct Tool {
92    pub name: String,
93    /// How the tool's side effects are fenced for durable at-most-once handling.
94    /// Observability + durability metadata only; subc's thin core never acts on
95    /// this for routing, scheduling, or concurrency — the module's declared
96    /// [`Concurrency`] contract governs delivery.
97    pub execution_mode: ExecutionMode,
98    pub schema: Value,
99}
100
101/// How subc may deliver concurrent in-flight calls to the provider.
102///
103/// subc records and forwards these semantics unchanged; the dispatcher that
104/// enforces them lives in subc-core, kept separate from this frozen manifest
105/// contract.
106#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
107#[serde(rename_all = "snake_case")]
108pub enum Concurrency {
109    /// One in-flight call at a time with strict submission and response order.
110    Serial,
111    /// Concurrent in-flight calls may span channels, while subc preserves FIFO
112    /// submission within each channel; the module schedules internally.
113    ModuleManaged,
114    /// Fully parallel delivery with no ordering guarantee across or within
115    /// channels.
116    StatelessParallel,
117}
118
119/// Identity keys that route or scope a call.
120#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
121#[serde(rename_all = "snake_case")]
122pub enum IdentityScope {
123    Session,
124    Project,
125}
126
127/// Proxy-plane stage kind.
128#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
129#[serde(rename_all = "snake_case")]
130pub enum PipelineStageKind {
131    Transform,
132    Codec,
133    Auth,
134}
135
136/// Provider/model selector for a pipeline stage. `"*"` denotes wildcard.
137#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
138pub struct PipelineAppliesTo {
139    pub provider: String,
140    pub model: String,
141}
142
143/// Operation exposed on the management plane.
144#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
145pub struct ManagementOperation {
146    pub name: String,
147    pub kind: ManagementOperationKind,
148}
149
150#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
151#[serde(rename_all = "snake_case")]
152pub enum ManagementOperationKind {
153    Query,
154    Mutate,
155}
156
157/// Observable state exposed on the management plane.
158#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
159pub struct ObservabilitySurface {
160    pub name: String,
161    pub kind: ObservabilityKind,
162}
163
164#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
165#[serde(rename_all = "snake_case")]
166pub enum ObservabilityKind {
167    Snapshot,
168    Stream,
169}
170
171#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
172#[serde(rename_all = "snake_case")]
173pub enum InternalTransport {
174    Bulk,
175}
176
177/// Consumer capabilities requested by a module.
178#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
179#[serde(tag = "role", rename_all = "snake_case")]
180pub enum ConsumerRole {
181    ToolClient { of: Vec<String> },
182    LlmClient { via: String, auth: String },
183    ServiceClient { of: Vec<String> },
184}
185
186/// Scheduler-owned task declaration. The runner module executes the loop; subc
187/// owns eligibility checks and the lease.
188#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
189pub struct ScheduledTask {
190    pub task_id: String,
191    pub eligibility: TaskEligibility,
192    pub lease_scope: LeaseScope,
193    pub renews_during_calls: bool,
194    pub toolset: Vec<String>,
195    pub model_policy: ModelPolicy,
196    pub step_cap: u32,
197    pub circuit_breaker: CircuitBreaker,
198}
199
200/// Time/window gates for a scheduled task. Values are serialized policy strings
201/// (for example, durations or cron/window expressions) owned by the scheduler.
202#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
203pub struct TaskEligibility {
204    pub cooldown: String,
205    pub window: String,
206}
207
208/// Scope at which subc enforces one active scheduler lease.
209#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
210#[serde(rename_all = "snake_case")]
211pub enum LeaseScope {
212    Project,
213}
214
215/// Model selection policy for the LLM-runner that executes a scheduled task.
216#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
217pub struct ModelPolicy {
218    pub tier: String,
219    pub fallback_chain: Vec<String>,
220}
221
222#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
223pub struct CircuitBreaker {
224    pub identical_failures: u32,
225}
226
227/// External storage, vault, and identity bindings supplied through subc.
228#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
229pub struct Bindings {
230    pub storage: StorageBinding,
231    pub vault_grants: Vec<VaultGrant>,
232    pub identity: IdentityBinding,
233}
234
235/// Storage backend supplied by subc; the module owns its schema.
236#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
237pub struct StorageBinding {
238    pub kind: StorageKind,
239    pub scope: StorageScope,
240    pub owns_schema: bool,
241}
242
243#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
244#[serde(rename_all = "snake_case")]
245pub enum StorageKind {
246    Sqlite,
247}
248
249#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
250#[serde(rename_all = "snake_case")]
251pub enum StorageScope {
252    Project,
253}
254
255#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
256pub struct VaultGrant {
257    pub secret: String,
258    pub reason: String,
259}
260
261#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
262pub struct IdentityBinding {
263    pub requires: Vec<IdentityScope>,
264    pub optional: Vec<IdentityScope>,
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use serde_json::json;
271
272    fn aft_manifest_fixture() -> ModuleManifest {
273        ModuleManifest {
274            module_id: "aft".to_string(),
275            module_version: "0.39.2".to_string(),
276            protocol_ver: 1,
277            trust_tier: TrustTier::FirstParty,
278            provides: vec![ProviderRole::ToolProvider {
279                tools: vec![
280                    Tool {
281                        name: "read".to_string(),
282                        execution_mode: ExecutionMode::Pure,
283                        schema: json!({"type": "object"}),
284                    },
285                    Tool {
286                        name: "grep".to_string(),
287                        execution_mode: ExecutionMode::Pure,
288                        schema: json!({"type": "object"}),
289                    },
290                    Tool {
291                        name: "outline".to_string(),
292                        execution_mode: ExecutionMode::Pure,
293                        schema: json!({"type": "object"}),
294                    },
295                    Tool {
296                        name: "semantic_search".to_string(),
297                        execution_mode: ExecutionMode::Pure,
298                        schema: json!({"type": "object"}),
299                    },
300                    Tool {
301                        name: "edit".to_string(),
302                        execution_mode: ExecutionMode::Mutating,
303                        schema: json!({"type": "object"}),
304                    },
305                    Tool {
306                        name: "write".to_string(),
307                        execution_mode: ExecutionMode::Mutating,
308                        schema: json!({"type": "object"}),
309                    },
310                    Tool {
311                        name: "bash".to_string(),
312                        execution_mode: ExecutionMode::Unfenceable,
313                        schema: json!({"type": "object"}),
314                    },
315                ],
316                identity_scope: vec![IdentityScope::Session, IdentityScope::Project],
317                concurrency: Concurrency::ModuleManaged,
318                emits_push: true,
319                sub_supervises: true,
320            }],
321            consumes: vec![ConsumerRole::ServiceClient {
322                of: vec!["embedding.v2".to_string()],
323            }],
324            scheduled_tasks: vec![],
325            bindings: Bindings {
326                storage: StorageBinding {
327                    kind: StorageKind::Sqlite,
328                    scope: StorageScope::Project,
329                    owns_schema: true,
330                },
331                vault_grants: vec![VaultGrant {
332                    secret: "provider_api_key".to_string(),
333                    reason: "cortexkit_native auth".to_string(),
334                }],
335                identity: IdentityBinding {
336                    requires: vec![IdentityScope::Project],
337                    optional: vec![IdentityScope::Session],
338                },
339            },
340        }
341    }
342
343    #[test]
344    fn serde_round_trips_representative_manifest() {
345        let manifest = aft_manifest_fixture();
346        let serialized = serde_json::to_string_pretty(&manifest).unwrap();
347        let decoded: ModuleManifest = serde_json::from_str(&serialized).unwrap();
348
349        assert_eq!(manifest, decoded);
350    }
351
352    #[test]
353    fn aft_manifest_fixture_matches_v1_contract() {
354        let manifest = aft_manifest_fixture();
355
356        assert_eq!(manifest.module_id, "aft");
357        let ProviderRole::ToolProvider {
358            tools,
359            identity_scope,
360            concurrency,
361            emits_push,
362            sub_supervises,
363        } = &manifest.provides[0]
364        else {
365            panic!("AFT fixture must expose one tool_provider role");
366        };
367
368        assert_eq!(*concurrency, Concurrency::ModuleManaged);
369        assert!(*emits_push);
370        assert!(*sub_supervises);
371        assert_eq!(
372            identity_scope,
373            &vec![IdentityScope::Session, IdentityScope::Project]
374        );
375        assert_eq!(
376            tools
377                .iter()
378                .map(|tool| (tool.name.as_str(), tool.execution_mode))
379                .collect::<Vec<_>>(),
380            vec![
381                ("read", ExecutionMode::Pure),
382                ("grep", ExecutionMode::Pure),
383                ("outline", ExecutionMode::Pure),
384                ("semantic_search", ExecutionMode::Pure),
385                ("edit", ExecutionMode::Mutating),
386                ("write", ExecutionMode::Mutating),
387                ("bash", ExecutionMode::Unfenceable),
388            ]
389        );
390    }
391
392    #[test]
393    fn tool_provider_role_tag_serializes_as_snake_case() {
394        let manifest = aft_manifest_fixture();
395        let value = serde_json::to_value(&manifest).unwrap();
396
397        assert_eq!(value["provides"][0]["role"], "tool_provider");
398    }
399}