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/// Tool-plane capability exposed by a `tool_provider`.
71#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
72pub struct Tool {
73    pub name: String,
74    /// Observability/client metadata only. subc's thin core never acts on this
75    /// bit for routing, scheduling, or concurrency; the module's declared
76    /// [`Concurrency`] contract governs delivery.
77    pub mutates: bool,
78    pub schema: Value,
79}
80
81/// How subc may deliver concurrent in-flight calls to the provider.
82///
83/// subc honors these semantics per FR16; the dispatcher that acts on them is
84/// Epic 2, while the manifest contract is frozen here.
85#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
86#[serde(rename_all = "snake_case")]
87pub enum Concurrency {
88    /// One in-flight call at a time with strict submission and response order.
89    Serial,
90    /// Concurrent in-flight calls may span channels, while subc preserves FIFO
91    /// submission within each channel; the module schedules internally.
92    ModuleManaged,
93    /// Fully parallel delivery with no ordering guarantee across or within
94    /// channels.
95    StatelessParallel,
96}
97
98/// Identity keys that route or scope a call.
99#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
100#[serde(rename_all = "snake_case")]
101pub enum IdentityScope {
102    Session,
103    Project,
104}
105
106/// Proxy-plane stage kind.
107#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
108#[serde(rename_all = "snake_case")]
109pub enum PipelineStageKind {
110    Transform,
111    Codec,
112    Auth,
113}
114
115/// Provider/model selector for a pipeline stage. `"*"` denotes wildcard.
116#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
117pub struct PipelineAppliesTo {
118    pub provider: String,
119    pub model: String,
120}
121
122/// Operation exposed on the management plane.
123#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
124pub struct ManagementOperation {
125    pub name: String,
126    pub kind: ManagementOperationKind,
127}
128
129#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
130#[serde(rename_all = "snake_case")]
131pub enum ManagementOperationKind {
132    Query,
133    Mutate,
134}
135
136/// Observable state exposed on the management plane.
137#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
138pub struct ObservabilitySurface {
139    pub name: String,
140    pub kind: ObservabilityKind,
141}
142
143#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
144#[serde(rename_all = "snake_case")]
145pub enum ObservabilityKind {
146    Snapshot,
147    Stream,
148}
149
150#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
151#[serde(rename_all = "snake_case")]
152pub enum InternalTransport {
153    Bulk,
154}
155
156/// Consumer capabilities requested by a module.
157#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
158#[serde(tag = "role", rename_all = "snake_case")]
159pub enum ConsumerRole {
160    ToolClient { of: Vec<String> },
161    LlmClient { via: String, auth: String },
162    ServiceClient { of: Vec<String> },
163}
164
165/// Scheduler-owned task declaration. The runner module executes the loop; subc
166/// owns eligibility checks and the lease.
167#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
168pub struct ScheduledTask {
169    pub task_id: String,
170    pub eligibility: TaskEligibility,
171    pub lease_scope: LeaseScope,
172    pub renews_during_calls: bool,
173    pub toolset: Vec<String>,
174    pub model_policy: ModelPolicy,
175    pub step_cap: u32,
176    pub circuit_breaker: CircuitBreaker,
177}
178
179/// Time/window gates for a scheduled task. Values are serialized policy strings
180/// (for example, durations or cron/window expressions) owned by the scheduler.
181#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
182pub struct TaskEligibility {
183    pub cooldown: String,
184    pub window: String,
185}
186
187/// Scope at which subc enforces one active scheduler lease.
188#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
189#[serde(rename_all = "snake_case")]
190pub enum LeaseScope {
191    Project,
192}
193
194/// Model selection policy for the LLM-runner that executes a scheduled task.
195#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
196pub struct ModelPolicy {
197    pub tier: String,
198    pub fallback_chain: Vec<String>,
199}
200
201#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
202pub struct CircuitBreaker {
203    pub identical_failures: u32,
204}
205
206/// External resources and identity/config bindings supplied through subc.
207#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
208pub struct Bindings {
209    pub storage: StorageBinding,
210    pub config: ConfigBinding,
211    pub vault_grants: Vec<VaultGrant>,
212    pub identity: IdentityBinding,
213}
214
215/// Storage backend supplied by subc; the module owns its schema.
216#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
217pub struct StorageBinding {
218    pub kind: StorageKind,
219    pub scope: StorageScope,
220    pub owns_schema: bool,
221}
222
223#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
224#[serde(rename_all = "snake_case")]
225pub enum StorageKind {
226    Sqlite,
227}
228
229#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
230#[serde(rename_all = "snake_case")]
231pub enum StorageScope {
232    Project,
233}
234
235/// Layered config transport. subc stores tiered raw documents and transports
236/// literal token references; modules merge and expand them at use time.
237#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
238pub struct ConfigBinding {
239    pub source: ConfigSource,
240    pub tiers: Vec<String>,
241    pub expansion: BTreeMap<String, Vec<TokenExpansion>>,
242}
243
244#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
245#[serde(rename_all = "snake_case")]
246pub enum ConfigSource {
247    SubcMediated,
248}
249
250#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
251#[serde(rename_all = "snake_case")]
252pub enum TokenExpansion {
253    Env,
254    File,
255}
256
257#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
258pub struct VaultGrant {
259    pub secret: String,
260    pub reason: String,
261}
262
263#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
264pub struct IdentityBinding {
265    pub requires: Vec<IdentityScope>,
266    pub optional: Vec<IdentityScope>,
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use serde_json::json;
273
274    fn aft_manifest_fixture() -> ModuleManifest {
275        let expansion = BTreeMap::from([
276            (
277                "user".to_string(),
278                vec![TokenExpansion::Env, TokenExpansion::File],
279            ),
280            ("project".to_string(), vec![]),
281        ]);
282
283        ModuleManifest {
284            module_id: "aft".to_string(),
285            module_version: "0.39.2".to_string(),
286            protocol_ver: 1,
287            trust_tier: TrustTier::FirstParty,
288            provides: vec![ProviderRole::ToolProvider {
289                tools: vec![
290                    Tool {
291                        name: "read".to_string(),
292                        mutates: false,
293                        schema: json!({"type": "object"}),
294                    },
295                    Tool {
296                        name: "grep".to_string(),
297                        mutates: false,
298                        schema: json!({"type": "object"}),
299                    },
300                    Tool {
301                        name: "outline".to_string(),
302                        mutates: false,
303                        schema: json!({"type": "object"}),
304                    },
305                    Tool {
306                        name: "semantic_search".to_string(),
307                        mutates: false,
308                        schema: json!({"type": "object"}),
309                    },
310                    Tool {
311                        name: "edit".to_string(),
312                        mutates: true,
313                        schema: json!({"type": "object"}),
314                    },
315                    Tool {
316                        name: "write".to_string(),
317                        mutates: true,
318                        schema: json!({"type": "object"}),
319                    },
320                    Tool {
321                        name: "bash".to_string(),
322                        mutates: false,
323                        schema: json!({"type": "object"}),
324                    },
325                ],
326                identity_scope: vec![IdentityScope::Session, IdentityScope::Project],
327                concurrency: Concurrency::ModuleManaged,
328                emits_push: true,
329                sub_supervises: true,
330            }],
331            consumes: vec![ConsumerRole::ServiceClient {
332                of: vec!["embedding.v2".to_string()],
333            }],
334            scheduled_tasks: vec![],
335            bindings: Bindings {
336                storage: StorageBinding {
337                    kind: StorageKind::Sqlite,
338                    scope: StorageScope::Project,
339                    owns_schema: true,
340                },
341                config: ConfigBinding {
342                    source: ConfigSource::SubcMediated,
343                    tiers: vec!["user".to_string(), "project".to_string()],
344                    expansion,
345                },
346                vault_grants: vec![VaultGrant {
347                    secret: "provider_api_key".to_string(),
348                    reason: "cortexkit_native auth".to_string(),
349                }],
350                identity: IdentityBinding {
351                    requires: vec![IdentityScope::Project],
352                    optional: vec![IdentityScope::Session],
353                },
354            },
355        }
356    }
357
358    #[test]
359    fn serde_round_trips_representative_manifest() {
360        let manifest = aft_manifest_fixture();
361        let serialized = serde_json::to_string_pretty(&manifest).unwrap();
362        let decoded: ModuleManifest = serde_json::from_str(&serialized).unwrap();
363
364        assert_eq!(manifest, decoded);
365    }
366
367    #[test]
368    fn aft_manifest_fixture_matches_v1_contract() {
369        let manifest = aft_manifest_fixture();
370
371        assert_eq!(manifest.module_id, "aft");
372        let ProviderRole::ToolProvider {
373            tools,
374            identity_scope,
375            concurrency,
376            emits_push,
377            sub_supervises,
378        } = &manifest.provides[0]
379        else {
380            panic!("AFT fixture must expose one tool_provider role");
381        };
382
383        assert_eq!(*concurrency, Concurrency::ModuleManaged);
384        assert!(*emits_push);
385        assert!(*sub_supervises);
386        assert_eq!(
387            identity_scope,
388            &vec![IdentityScope::Session, IdentityScope::Project]
389        );
390        assert_eq!(
391            tools
392                .iter()
393                .map(|tool| (tool.name.as_str(), tool.mutates))
394                .collect::<Vec<_>>(),
395            vec![
396                ("read", false),
397                ("grep", false),
398                ("outline", false),
399                ("semantic_search", false),
400                ("edit", true),
401                ("write", true),
402                ("bash", false),
403            ]
404        );
405    }
406
407    #[test]
408    fn tool_provider_role_tag_serializes_as_snake_case() {
409        let manifest = aft_manifest_fixture();
410        let value = serde_json::to_value(&manifest).unwrap();
411
412        assert_eq!(value["provides"][0]["role"], "tool_provider");
413    }
414}