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)]
225pub struct CircuitBreaker {
226 pub identical_failures: u32,
227}
228
229#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
231pub struct Bindings {
232 pub storage: StorageBinding,
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)]
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 ModuleManifest {
276 module_id: "aft".to_string(),
277 module_version: "0.39.2".to_string(),
278 protocol_ver: 1,
279 trust_tier: TrustTier::FirstParty,
280 provides: vec![ProviderRole::ToolProvider {
281 tools: vec![
282 Tool {
283 name: "read".to_string(),
284 description: None,
285 execution_mode: ExecutionMode::Pure,
286 schema: json!({"type": "object"}),
287 },
288 Tool {
289 name: "grep".to_string(),
290 description: None,
291 execution_mode: ExecutionMode::Pure,
292 schema: json!({"type": "object"}),
293 },
294 Tool {
295 name: "outline".to_string(),
296 description: None,
297 execution_mode: ExecutionMode::Pure,
298 schema: json!({"type": "object"}),
299 },
300 Tool {
301 name: "semantic_search".to_string(),
302 description: None,
303 execution_mode: ExecutionMode::Pure,
304 schema: json!({"type": "object"}),
305 },
306 Tool {
307 name: "edit".to_string(),
308 description: None,
309 execution_mode: ExecutionMode::Mutating,
310 schema: json!({"type": "object"}),
311 },
312 Tool {
313 name: "write".to_string(),
314 description: None,
315 execution_mode: ExecutionMode::Mutating,
316 schema: json!({"type": "object"}),
317 },
318 Tool {
319 name: "bash".to_string(),
320 description: None,
321 execution_mode: ExecutionMode::Unfenceable,
322 schema: json!({"type": "object"}),
323 },
324 ],
325 identity_scope: vec![IdentityScope::Session, IdentityScope::Project],
326 concurrency: Concurrency::ModuleManaged,
327 emits_push: true,
328 sub_supervises: true,
329 }],
330 consumes: vec![ConsumerRole::ServiceClient {
331 of: vec!["embedding.v2".to_string()],
332 }],
333 scheduled_tasks: vec![],
334 bindings: Bindings {
335 storage: StorageBinding {
336 kind: StorageKind::Sqlite,
337 scope: StorageScope::Project,
338 owns_schema: true,
339 },
340 vault_grants: vec![VaultGrant {
341 secret: "provider_api_key".to_string(),
342 reason: "cortexkit_native auth".to_string(),
343 }],
344 identity: IdentityBinding {
345 requires: vec![IdentityScope::Project],
346 optional: vec![IdentityScope::Session],
347 },
348 },
349 }
350 }
351
352 #[test]
353 fn serde_round_trips_representative_manifest() {
354 let manifest = aft_manifest_fixture();
355 let serialized = serde_json::to_string_pretty(&manifest).unwrap();
356 let decoded: ModuleManifest = serde_json::from_str(&serialized).unwrap();
357
358 assert_eq!(manifest, decoded);
359 }
360
361 #[test]
362 fn aft_manifest_fixture_matches_v1_contract() {
363 let manifest = aft_manifest_fixture();
364
365 assert_eq!(manifest.module_id, "aft");
366 let ProviderRole::ToolProvider {
367 tools,
368 identity_scope,
369 concurrency,
370 emits_push,
371 sub_supervises,
372 } = &manifest.provides[0]
373 else {
374 panic!("AFT fixture must expose one tool_provider role");
375 };
376
377 assert_eq!(*concurrency, Concurrency::ModuleManaged);
378 assert!(*emits_push);
379 assert!(*sub_supervises);
380 assert_eq!(
381 identity_scope,
382 &vec![IdentityScope::Session, IdentityScope::Project]
383 );
384 assert_eq!(
385 tools
386 .iter()
387 .map(|tool| (tool.name.as_str(), tool.execution_mode))
388 .collect::<Vec<_>>(),
389 vec![
390 ("read", ExecutionMode::Pure),
391 ("grep", ExecutionMode::Pure),
392 ("outline", ExecutionMode::Pure),
393 ("semantic_search", ExecutionMode::Pure),
394 ("edit", ExecutionMode::Mutating),
395 ("write", ExecutionMode::Mutating),
396 ("bash", ExecutionMode::Unfenceable),
397 ]
398 );
399 }
400
401 #[test]
402 fn tool_provider_role_tag_serializes_as_snake_case() {
403 let manifest = aft_manifest_fixture();
404 let value = serde_json::to_value(&manifest).unwrap();
405
406 assert_eq!(value["provides"][0]["role"], "tool_provider");
407 }
408}