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