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, Copy, PartialEq, Eq)]
83#[serde(rename_all = "snake_case")]
84pub enum ExecutionMode {
85 Pure,
86 Mutating,
87 Unfenceable,
88}
89
90#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
92pub struct Tool {
93 pub name: String,
94 pub execution_mode: ExecutionMode,
99 pub schema: Value,
100}
101
102#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
108#[serde(rename_all = "snake_case")]
109pub enum Concurrency {
110 Serial,
112 ModuleManaged,
115 StatelessParallel,
118}
119
120#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
122#[serde(rename_all = "snake_case")]
123pub enum IdentityScope {
124 Session,
125 Project,
126}
127
128#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
130#[serde(rename_all = "snake_case")]
131pub enum PipelineStageKind {
132 Transform,
133 Codec,
134 Auth,
135}
136
137#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
139pub struct PipelineAppliesTo {
140 pub provider: String,
141 pub model: String,
142}
143
144#[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#[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#[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#[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#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
204pub struct TaskEligibility {
205 pub cooldown: String,
206 pub window: String,
207}
208
209#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
211#[serde(rename_all = "snake_case")]
212pub enum LeaseScope {
213 Project,
214}
215
216#[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#[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#[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#[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}