1use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12#[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#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
27#[serde(rename_all = "snake_case")]
28pub enum TrustTier {
29 FirstParty,
30 Reviewed,
31 Untrusted,
32}
33
34#[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#[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#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
91pub struct Tool {
92 pub name: String,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub description: Option<String>,
95 pub execution_mode: ExecutionMode,
100 pub schema: Value,
101}
102
103#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
109#[serde(rename_all = "snake_case")]
110pub enum Concurrency {
111 Serial,
113 ModuleManaged,
116 StatelessParallel,
119}
120
121#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
123#[serde(rename_all = "snake_case")]
124pub enum IdentityScope {
125 Session,
126 Project,
127}
128
129#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
131#[serde(rename_all = "snake_case")]
132pub enum PipelineStageKind {
133 Transform,
134 Codec,
135 Auth,
136}
137
138#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
140pub struct PipelineAppliesTo {
141 pub provider: String,
142 pub model: String,
143}
144
145#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
147pub struct ManagementOperation {
148 pub name: String,
149 pub kind: ManagementOperationKind,
150}
151
152#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
153#[serde(rename_all = "snake_case")]
154pub enum ManagementOperationKind {
155 Query,
156 Mutate,
157}
158
159#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
161pub struct ObservabilitySurface {
162 pub name: String,
163 pub kind: ObservabilityKind,
164}
165
166#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
167#[serde(rename_all = "snake_case")]
168pub enum ObservabilityKind {
169 Snapshot,
170 Stream,
171}
172
173#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
174#[serde(rename_all = "snake_case")]
175pub enum InternalTransport {
176 Bulk,
177}
178
179#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
181#[serde(tag = "role", rename_all = "snake_case")]
182pub enum ConsumerRole {
183 ToolClient { of: Vec<String> },
184 LlmClient { via: String, auth: String },
185 ServiceClient { of: Vec<String> },
186}
187
188#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
191pub struct ScheduledTask {
192 pub task_id: String,
193 pub eligibility: TaskEligibility,
194 pub lease_scope: LeaseScope,
195 pub renews_during_calls: bool,
196 pub toolset: Vec<String>,
197 pub model_policy: ModelPolicy,
198 pub step_cap: u32,
199 pub circuit_breaker: CircuitBreaker,
200}
201
202#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
205pub struct TaskEligibility {
206 pub cooldown: String,
207 pub window: String,
208}
209
210#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
212#[serde(rename_all = "snake_case")]
213pub enum LeaseScope {
214 Project,
215}
216
217#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
219pub struct ModelPolicy {
220 pub tier: String,
221 pub fallback_chain: Vec<String>,
222}
223
224#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
238pub struct CircuitBreaker {
239 pub identical_failures: u32,
240}
241
242#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
244pub struct Bindings {
245 pub storage: StorageBinding,
246 pub vault_grants: Vec<VaultGrant>,
247 pub identity: IdentityBinding,
248}
249
250#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
252pub struct StorageBinding {
253 pub kind: StorageKind,
254 pub scope: StorageScope,
255 pub owns_schema: bool,
256}
257
258#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
259#[serde(rename_all = "snake_case")]
260pub enum StorageKind {
261 Sqlite,
262}
263
264#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
265#[serde(rename_all = "snake_case")]
266pub enum StorageScope {
267 Project,
268}
269
270#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
271pub struct VaultGrant {
272 pub secret: String,
273 pub reason: String,
274}
275
276#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
277pub struct IdentityBinding {
278 pub requires: Vec<IdentityScope>,
279 pub optional: Vec<IdentityScope>,
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use serde_json::json;
286
287 fn aft_manifest_fixture() -> ModuleManifest {
288 ModuleManifest {
289 module_id: "aft".to_string(),
290 module_version: "0.39.2".to_string(),
291 protocol_ver: 1,
292 trust_tier: TrustTier::FirstParty,
293 provides: vec![ProviderRole::ToolProvider {
294 tools: vec![
295 Tool {
296 name: "read".to_string(),
297 description: None,
298 execution_mode: ExecutionMode::Pure,
299 schema: json!({"type": "object"}),
300 },
301 Tool {
302 name: "grep".to_string(),
303 description: None,
304 execution_mode: ExecutionMode::Pure,
305 schema: json!({"type": "object"}),
306 },
307 Tool {
308 name: "outline".to_string(),
309 description: None,
310 execution_mode: ExecutionMode::Pure,
311 schema: json!({"type": "object"}),
312 },
313 Tool {
314 name: "semantic_search".to_string(),
315 description: None,
316 execution_mode: ExecutionMode::Pure,
317 schema: json!({"type": "object"}),
318 },
319 Tool {
320 name: "edit".to_string(),
321 description: None,
322 execution_mode: ExecutionMode::Mutating,
323 schema: json!({"type": "object"}),
324 },
325 Tool {
326 name: "write".to_string(),
327 description: None,
328 execution_mode: ExecutionMode::Mutating,
329 schema: json!({"type": "object"}),
330 },
331 Tool {
332 name: "bash".to_string(),
333 description: None,
334 execution_mode: ExecutionMode::Unfenceable,
335 schema: json!({"type": "object"}),
336 },
337 ],
338 identity_scope: vec![IdentityScope::Session, IdentityScope::Project],
339 concurrency: Concurrency::ModuleManaged,
340 emits_push: true,
341 sub_supervises: true,
342 }],
343 consumes: vec![ConsumerRole::ServiceClient {
344 of: vec!["embedding.v2".to_string()],
345 }],
346 scheduled_tasks: vec![],
347 bindings: Bindings {
348 storage: StorageBinding {
349 kind: StorageKind::Sqlite,
350 scope: StorageScope::Project,
351 owns_schema: true,
352 },
353 vault_grants: vec![VaultGrant {
354 secret: "provider_api_key".to_string(),
355 reason: "cortexkit_native auth".to_string(),
356 }],
357 identity: IdentityBinding {
358 requires: vec![IdentityScope::Project],
359 optional: vec![IdentityScope::Session],
360 },
361 },
362 }
363 }
364
365 #[test]
366 fn serde_round_trips_representative_manifest() {
367 let manifest = aft_manifest_fixture();
368 let serialized = serde_json::to_string_pretty(&manifest).unwrap();
369 let decoded: ModuleManifest = serde_json::from_str(&serialized).unwrap();
370
371 assert_eq!(manifest, decoded);
372 }
373
374 #[test]
375 fn aft_manifest_fixture_matches_v1_contract() {
376 let manifest = aft_manifest_fixture();
377
378 assert_eq!(manifest.module_id, "aft");
379 let ProviderRole::ToolProvider {
380 tools,
381 identity_scope,
382 concurrency,
383 emits_push,
384 sub_supervises,
385 } = &manifest.provides[0]
386 else {
387 panic!("AFT fixture must expose one tool_provider role");
388 };
389
390 assert_eq!(*concurrency, Concurrency::ModuleManaged);
391 assert!(*emits_push);
392 assert!(*sub_supervises);
393 assert_eq!(
394 identity_scope,
395 &vec![IdentityScope::Session, IdentityScope::Project]
396 );
397 assert_eq!(
398 tools
399 .iter()
400 .map(|tool| (tool.name.as_str(), tool.execution_mode))
401 .collect::<Vec<_>>(),
402 vec![
403 ("read", ExecutionMode::Pure),
404 ("grep", ExecutionMode::Pure),
405 ("outline", ExecutionMode::Pure),
406 ("semantic_search", ExecutionMode::Pure),
407 ("edit", ExecutionMode::Mutating),
408 ("write", ExecutionMode::Mutating),
409 ("bash", ExecutionMode::Unfenceable),
410 ]
411 );
412 }
413
414 #[test]
415 fn tool_provider_role_tag_serializes_as_snake_case() {
416 let manifest = aft_manifest_fixture();
417 let value = serde_json::to_value(&manifest).unwrap();
418
419 assert_eq!(value["provides"][0]["role"], "tool_provider");
420 }
421}