Skip to main content

aidens_plan_kit/
lib.rs

1//! Execution-plan assembly helpers for AiDENs product surfaces.
2//!
3//! This crate owns only construction of AiDENs app-layer plans. Canonical
4//! memory, kernel, repair, verification, evidence, and federation semantics
5//! remain delegated to their canonical crates.
6
7use aidens_contracts::{AiDENsAppPlanV1, MemoryModeV1, ReportLevelV1, RiskDisclosureV1};
8use thiserror::Error;
9
10pub const PLAN_KIT_SCOPE: &str = "execution-plan-assembly-only";
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ExecutionPlanAssemblyInputV1 {
14    pub app_id: String,
15    pub profile_id: String,
16    pub provider_required: bool,
17    pub memory_mode: MemoryModeV1,
18    pub receipt_level: ReportLevelV1,
19    pub dangerous_auto_approval: bool,
20    pub risk_disclosures: Vec<RiskDisclosureV1>,
21    pub enabled_tool_bundles: Vec<String>,
22    pub disabled_tool_bundles: Vec<String>,
23}
24
25impl From<AiDENsAppPlanV1> for ExecutionPlanAssemblyInputV1 {
26    fn from(plan: AiDENsAppPlanV1) -> Self {
27        Self {
28            app_id: plan.app_id,
29            profile_id: plan.profile_id,
30            provider_required: plan.provider_required,
31            memory_mode: plan.memory_mode,
32            receipt_level: plan.receipt_level,
33            dangerous_auto_approval: plan.dangerous_auto_approval,
34            risk_disclosures: plan.risk_disclosures,
35            enabled_tool_bundles: plan.enabled_tool_bundles,
36            disabled_tool_bundles: plan.disabled_tool_bundles,
37        }
38    }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct ExecutionPlanAssemblyReportV1 {
43    pub scope: &'static str,
44    pub plan: AiDENsAppPlanV1,
45    pub reason_codes: Vec<String>,
46}
47
48#[derive(Debug, Error, PartialEq, Eq)]
49pub enum PlanAssemblyError {
50    #[error("app_id must not be empty")]
51    EmptyAppId,
52    #[error("profile_id must not be empty")]
53    EmptyProfileId,
54    #[error("invalid assembled plan: {0}")]
55    InvalidPlan(String),
56}
57
58pub fn assemble_execution_plan(
59    input: ExecutionPlanAssemblyInputV1,
60) -> Result<ExecutionPlanAssemblyReportV1, PlanAssemblyError> {
61    if input.app_id.trim().is_empty() {
62        return Err(PlanAssemblyError::EmptyAppId);
63    }
64    if input.profile_id.trim().is_empty() {
65        return Err(PlanAssemblyError::EmptyProfileId);
66    }
67    let plan = AiDENsAppPlanV1 {
68        app_id: input.app_id,
69        profile_id: input.profile_id,
70        provider_required: input.provider_required,
71        memory_mode: input.memory_mode,
72        receipt_level: input.receipt_level,
73        dangerous_auto_approval: input.dangerous_auto_approval,
74        risk_disclosures: input.risk_disclosures,
75        enabled_tool_bundles: input.enabled_tool_bundles,
76        disabled_tool_bundles: input.disabled_tool_bundles,
77    };
78    plan.validate().map_err(PlanAssemblyError::InvalidPlan)?;
79    let reason_codes = vec![
80        format!("scope:{PLAN_KIT_SCOPE}"),
81        format!("profile:{}", plan.profile_id),
82        format!("memory-mode:{}", plan.memory_mode),
83        format!("receipt-level:{}", plan.receipt_level),
84        format!("enabled-tool-bundles:{}", plan.enabled_tool_bundles.len()),
85        format!("disabled-tool-bundles:{}", plan.disabled_tool_bundles.len()),
86    ];
87    Ok(ExecutionPlanAssemblyReportV1 {
88        scope: PLAN_KIT_SCOPE,
89        plan,
90        reason_codes,
91    })
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    fn input() -> ExecutionPlanAssemblyInputV1 {
99        ExecutionPlanAssemblyInputV1 {
100            app_id: "agent".into(),
101            profile_id: "coding-agent".into(),
102            provider_required: true,
103            memory_mode: MemoryModeV1::Disabled,
104            receipt_level: ReportLevelV1::Full,
105            dangerous_auto_approval: false,
106            risk_disclosures: Vec::new(),
107            enabled_tool_bundles: vec!["repo-read".into()],
108            disabled_tool_bundles: vec!["shell-auto".into()],
109        }
110    }
111
112    #[test]
113    fn assembles_app_layer_plan_only() {
114        let report = assemble_execution_plan(input()).unwrap();
115
116        assert_eq!(report.scope, PLAN_KIT_SCOPE);
117        assert_eq!(report.plan.app_id, "agent");
118        assert_eq!(report.plan.enabled_tool_bundles, ["repo-read"]);
119        assert!(report
120            .reason_codes
121            .contains(&"scope:execution-plan-assembly-only".into()));
122    }
123
124    #[test]
125    fn rejects_empty_identity() {
126        let mut input = input();
127        input.app_id.clear();
128
129        assert_eq!(
130            assemble_execution_plan(input).unwrap_err(),
131            PlanAssemblyError::EmptyAppId
132        );
133    }
134
135    #[test]
136    fn rejects_dangerous_auto_approval() {
137        let mut input = input();
138        input.dangerous_auto_approval = true;
139
140        let error = assemble_execution_plan(input).unwrap_err();
141
142        assert!(matches!(error, PlanAssemblyError::InvalidPlan(_)));
143    }
144}