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