1use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::BTreeMap;
12
13#[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#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
28#[serde(rename_all = "snake_case")]
29pub enum TrustTier {
30 FirstParty,
31 Reviewed,
32 Untrusted,
33}
34
35#[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#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
72pub struct Tool {
73 pub name: String,
74 pub mutates: bool,
78 pub schema: Value,
79}
80
81#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
86#[serde(rename_all = "snake_case")]
87pub enum Concurrency {
88 Serial,
90 ModuleManaged,
93 StatelessParallel,
96}
97
98#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
100#[serde(rename_all = "snake_case")]
101pub enum IdentityScope {
102 Session,
103 Project,
104}
105
106#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
108#[serde(rename_all = "snake_case")]
109pub enum PipelineStageKind {
110 Transform,
111 Codec,
112 Auth,
113}
114
115#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
117pub struct PipelineAppliesTo {
118 pub provider: String,
119 pub model: String,
120}
121
122#[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#[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#[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#[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#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
182pub struct TaskEligibility {
183 pub cooldown: String,
184 pub window: String,
185}
186
187#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
189#[serde(rename_all = "snake_case")]
190pub enum LeaseScope {
191 Project,
192}
193
194#[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#[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#[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#[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}