1use 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}