use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct ModuleManifest {
pub module_id: String,
pub module_version: String,
pub protocol_ver: u8,
pub trust_tier: TrustTier,
pub provides: Vec<ProviderRole>,
pub consumes: Vec<ConsumerRole>,
pub scheduled_tasks: Vec<ScheduledTask>,
pub bindings: Bindings,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TrustTier {
FirstParty,
Reviewed,
Untrusted,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(tag = "role", rename_all = "snake_case")]
pub enum ProviderRole {
ToolProvider {
tools: Vec<Tool>,
identity_scope: Vec<IdentityScope>,
concurrency: Concurrency,
emits_push: bool,
sub_supervises: bool,
},
PipelineStage {
stage: PipelineStageKind,
applies_to: PipelineAppliesTo,
interface: String,
declares_frozen_floor: bool,
needs_signals: Vec<String>,
conformance_class: String,
},
ManagementSurface {
operations: Vec<ManagementOperation>,
config_schema: Value,
observability: Vec<ObservabilitySurface>,
identity_scope: Vec<IdentityScope>,
},
InternalService {
service_id: String,
transport: InternalTransport,
agent_facing: bool,
operations: Vec<String>,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionMode {
Pure,
Mutating,
Unfenceable,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Tool {
pub name: String,
pub execution_mode: ExecutionMode,
pub schema: Value,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Concurrency {
Serial,
ModuleManaged,
StatelessParallel,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum IdentityScope {
Session,
Project,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PipelineStageKind {
Transform,
Codec,
Auth,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct PipelineAppliesTo {
pub provider: String,
pub model: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct ManagementOperation {
pub name: String,
pub kind: ManagementOperationKind,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ManagementOperationKind {
Query,
Mutate,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct ObservabilitySurface {
pub name: String,
pub kind: ObservabilityKind,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ObservabilityKind {
Snapshot,
Stream,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum InternalTransport {
Bulk,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(tag = "role", rename_all = "snake_case")]
pub enum ConsumerRole {
ToolClient { of: Vec<String> },
LlmClient { via: String, auth: String },
ServiceClient { of: Vec<String> },
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct ScheduledTask {
pub task_id: String,
pub eligibility: TaskEligibility,
pub lease_scope: LeaseScope,
pub renews_during_calls: bool,
pub toolset: Vec<String>,
pub model_policy: ModelPolicy,
pub step_cap: u32,
pub circuit_breaker: CircuitBreaker,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct TaskEligibility {
pub cooldown: String,
pub window: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum LeaseScope {
Project,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct ModelPolicy {
pub tier: String,
pub fallback_chain: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct CircuitBreaker {
pub identical_failures: u32,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Bindings {
pub storage: StorageBinding,
pub config: ConfigBinding,
pub vault_grants: Vec<VaultGrant>,
pub identity: IdentityBinding,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StorageBinding {
pub kind: StorageKind,
pub scope: StorageScope,
pub owns_schema: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum StorageKind {
Sqlite,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum StorageScope {
Project,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct ConfigBinding {
pub source: ConfigSource,
pub tiers: Vec<String>,
pub expansion: BTreeMap<String, Vec<TokenExpansion>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ConfigSource {
SubcMediated,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TokenExpansion {
Env,
File,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct VaultGrant {
pub secret: String,
pub reason: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct IdentityBinding {
pub requires: Vec<IdentityScope>,
pub optional: Vec<IdentityScope>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn aft_manifest_fixture() -> ModuleManifest {
let expansion = BTreeMap::from([
(
"user".to_string(),
vec![TokenExpansion::Env, TokenExpansion::File],
),
("project".to_string(), vec![]),
]);
ModuleManifest {
module_id: "aft".to_string(),
module_version: "0.39.2".to_string(),
protocol_ver: 1,
trust_tier: TrustTier::FirstParty,
provides: vec![ProviderRole::ToolProvider {
tools: vec![
Tool {
name: "read".to_string(),
execution_mode: ExecutionMode::Pure,
schema: json!({"type": "object"}),
},
Tool {
name: "grep".to_string(),
execution_mode: ExecutionMode::Pure,
schema: json!({"type": "object"}),
},
Tool {
name: "outline".to_string(),
execution_mode: ExecutionMode::Pure,
schema: json!({"type": "object"}),
},
Tool {
name: "semantic_search".to_string(),
execution_mode: ExecutionMode::Pure,
schema: json!({"type": "object"}),
},
Tool {
name: "edit".to_string(),
execution_mode: ExecutionMode::Mutating,
schema: json!({"type": "object"}),
},
Tool {
name: "write".to_string(),
execution_mode: ExecutionMode::Mutating,
schema: json!({"type": "object"}),
},
Tool {
name: "bash".to_string(),
execution_mode: ExecutionMode::Unfenceable,
schema: json!({"type": "object"}),
},
],
identity_scope: vec![IdentityScope::Session, IdentityScope::Project],
concurrency: Concurrency::ModuleManaged,
emits_push: true,
sub_supervises: true,
}],
consumes: vec![ConsumerRole::ServiceClient {
of: vec!["embedding.v2".to_string()],
}],
scheduled_tasks: vec![],
bindings: Bindings {
storage: StorageBinding {
kind: StorageKind::Sqlite,
scope: StorageScope::Project,
owns_schema: true,
},
config: ConfigBinding {
source: ConfigSource::SubcMediated,
tiers: vec!["user".to_string(), "project".to_string()],
expansion,
},
vault_grants: vec![VaultGrant {
secret: "provider_api_key".to_string(),
reason: "cortexkit_native auth".to_string(),
}],
identity: IdentityBinding {
requires: vec![IdentityScope::Project],
optional: vec![IdentityScope::Session],
},
},
}
}
#[test]
fn serde_round_trips_representative_manifest() {
let manifest = aft_manifest_fixture();
let serialized = serde_json::to_string_pretty(&manifest).unwrap();
let decoded: ModuleManifest = serde_json::from_str(&serialized).unwrap();
assert_eq!(manifest, decoded);
}
#[test]
fn aft_manifest_fixture_matches_v1_contract() {
let manifest = aft_manifest_fixture();
assert_eq!(manifest.module_id, "aft");
let ProviderRole::ToolProvider {
tools,
identity_scope,
concurrency,
emits_push,
sub_supervises,
} = &manifest.provides[0]
else {
panic!("AFT fixture must expose one tool_provider role");
};
assert_eq!(*concurrency, Concurrency::ModuleManaged);
assert!(*emits_push);
assert!(*sub_supervises);
assert_eq!(
identity_scope,
&vec![IdentityScope::Session, IdentityScope::Project]
);
assert_eq!(
tools
.iter()
.map(|tool| (tool.name.as_str(), tool.execution_mode))
.collect::<Vec<_>>(),
vec![
("read", ExecutionMode::Pure),
("grep", ExecutionMode::Pure),
("outline", ExecutionMode::Pure),
("semantic_search", ExecutionMode::Pure),
("edit", ExecutionMode::Mutating),
("write", ExecutionMode::Mutating),
("bash", ExecutionMode::Unfenceable),
]
);
}
#[test]
fn tool_provider_role_tag_serializes_as_snake_case() {
let manifest = aft_manifest_fixture();
let value = serde_json::to_value(&manifest).unwrap();
assert_eq!(value["provides"][0]["role"], "tool_provider");
}
}