1use super::{AgentRole, DelegatedTask};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
8#[serde(rename_all = "snake_case")]
9pub enum ApprovalState {
10 #[default]
12 NotRequired,
13 Pending,
15 Approved,
17 Rejected,
19 NeedsRevision,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum ApprovalScope {
27 PreExecution,
29 Review,
31 TestValidation,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum ApprovalActorKind {
39 User,
41 Supervisor,
43 Reviewer,
45 Tester,
47 System,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct ApprovalActor {
54 pub kind: ApprovalActorKind,
56 pub id: String,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub display_name: Option<String>,
61}
62
63impl ApprovalActor {
64 pub fn new(kind: ApprovalActorKind, id: impl Into<String>) -> Self {
66 Self {
67 kind,
68 id: id.into(),
69 display_name: None,
70 }
71 }
72
73 pub fn system(id: impl Into<String>) -> Self {
75 Self::new(ApprovalActorKind::System, id)
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, Default)]
81pub struct ApprovalRequirement {
82 pub scope: Option<ApprovalScope>,
84 #[serde(default)]
86 pub required: bool,
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub allowed_deciders: Vec<ApprovalActorKind>,
90}
91
92impl ApprovalRequirement {
93 fn new(scope: ApprovalScope, required: bool, allowed_deciders: Vec<ApprovalActorKind>) -> Self {
94 Self {
95 scope: Some(scope),
96 required,
97 allowed_deciders,
98 }
99 }
100
101 pub fn allows(&self, actor_kind: ApprovalActorKind) -> bool {
103 self.allowed_deciders.contains(&actor_kind)
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109pub struct ApprovalPolicy {
110 #[serde(default)]
112 pub pre_execution: ApprovalRequirement,
113 #[serde(default)]
115 pub review: ApprovalRequirement,
116 #[serde(default)]
118 pub test_validation: ApprovalRequirement,
119}
120
121impl ApprovalPolicy {
122 pub fn for_task(task: &DelegatedTask) -> Self {
124 Self {
125 pre_execution: ApprovalRequirement::new(
126 ApprovalScope::PreExecution,
127 task.approval_required,
128 vec![ApprovalActorKind::Supervisor, ApprovalActorKind::User],
129 ),
130 review: ApprovalRequirement::new(
131 ApprovalScope::Review,
132 task.reviewer_required,
133 vec![ApprovalActorKind::Reviewer, ApprovalActorKind::Supervisor],
134 ),
135 test_validation: ApprovalRequirement::new(
136 ApprovalScope::TestValidation,
137 task.test_required,
138 vec![ApprovalActorKind::Tester, ApprovalActorKind::Supervisor],
139 ),
140 }
141 }
142
143 pub fn requirement(&self, scope: ApprovalScope) -> &ApprovalRequirement {
145 match scope {
146 ApprovalScope::PreExecution => &self.pre_execution,
147 ApprovalScope::Review => &self.review,
148 ApprovalScope::TestValidation => &self.test_validation,
149 }
150 }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ApprovalRequest {
156 pub id: String,
158 pub scope: ApprovalScope,
160 pub requested_by: ApprovalActor,
162 pub requested_at: DateTime<Utc>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub note: Option<String>,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum ApprovalDecisionKind {
173 Approved,
175 Rejected,
177 NeedsRevision,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ApprovalDecision {
184 pub id: String,
186 pub request_id: String,
188 pub scope: ApprovalScope,
190 pub actor: ApprovalActor,
192 pub decision: ApprovalDecisionKind,
194 pub decided_at: DateTime<Utc>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub note: Option<String>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, Default)]
203pub struct TaskApprovalRecord {
204 pub state: ApprovalState,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub scope: Option<ApprovalScope>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub active_request: Option<ApprovalRequest>,
212 #[serde(default)]
214 pub policy: ApprovalPolicy,
215 #[serde(default, skip_serializing_if = "Vec::is_empty")]
217 pub requests: Vec<ApprovalRequest>,
218 #[serde(default, skip_serializing_if = "Vec::is_empty")]
220 pub decisions: Vec<ApprovalDecision>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub requested_at: Option<DateTime<Utc>>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub decided_at: Option<DateTime<Utc>>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub decided_by: Option<String>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub note: Option<String>,
233}
234
235impl TaskApprovalRecord {
236 pub fn not_required(task: &DelegatedTask) -> Self {
238 Self {
239 state: ApprovalState::NotRequired,
240 scope: None,
241 active_request: None,
242 policy: ApprovalPolicy::for_task(task),
243 requests: Vec::new(),
244 decisions: Vec::new(),
245 requested_at: None,
246 decided_at: None,
247 decided_by: None,
248 note: None,
249 }
250 }
251
252 pub fn pending(
254 task: &DelegatedTask,
255 scope: ApprovalScope,
256 requested_by: ApprovalActor,
257 note: Option<String>,
258 ) -> Self {
259 let mut record = Self::not_required(task);
260 record.request(scope, requested_by, note);
261 record
262 }
263
264 pub fn reset_for_task(&mut self, task: &DelegatedTask) {
266 self.state = ApprovalState::NotRequired;
267 self.scope = None;
268 self.active_request = None;
269 self.policy = ApprovalPolicy::for_task(task);
270 self.requested_at = None;
271 self.decided_at = None;
272 self.decided_by = None;
273 self.note = None;
274 }
275
276 pub fn request(
278 &mut self,
279 scope: ApprovalScope,
280 requested_by: ApprovalActor,
281 note: Option<String>,
282 ) {
283 let now = Utc::now();
284 let request = ApprovalRequest {
285 id: Uuid::new_v4().to_string(),
286 scope,
287 requested_by,
288 requested_at: now,
289 note: note.clone(),
290 };
291 self.state = ApprovalState::Pending;
292 self.scope = Some(scope);
293 self.active_request = Some(request.clone());
294 self.requests.push(request);
295 self.requested_at = Some(now);
296 self.decided_at = None;
297 self.decided_by = None;
298 self.note = note;
299 }
300
301 pub fn latest_decision(&self) -> Option<&ApprovalDecision> {
303 self.decisions.last()
304 }
305
306 pub fn allowed_actor_kinds(&self, scope: ApprovalScope) -> &[ApprovalActorKind] {
308 &self.policy.requirement(scope).allowed_deciders
309 }
310
311 pub fn authorize(&self, scope: ApprovalScope, actor: &ApprovalActor) -> Result<(), String> {
313 let requirement = self.policy.requirement(scope);
314 if !requirement.required {
315 return Err(format!(
316 "Approval scope '{scope:?}' is not required for this task"
317 ));
318 }
319 if !requirement.allows(actor.kind) {
320 return Err(format!(
321 "Actor '{}' with role '{:?}' is not authorized for {:?} approval",
322 actor.id, actor.kind, scope
323 ));
324 }
325 Ok(())
326 }
327
328 pub fn record_decision(
330 &mut self,
331 scope: ApprovalScope,
332 decision: ApprovalDecisionKind,
333 actor: ApprovalActor,
334 note: Option<String>,
335 ) -> Result<ApprovalDecision, String> {
336 self.authorize(scope, &actor)?;
337 let active_request = self
338 .active_request
339 .as_ref()
340 .ok_or_else(|| format!("No active approval request exists for {:?}", scope))?;
341 if active_request.scope != scope {
342 return Err(format!(
343 "Active approval request scope mismatch: expected {:?}, found {:?}",
344 scope, active_request.scope
345 ));
346 }
347
348 let decided_at = Utc::now();
349 let entry = ApprovalDecision {
350 id: Uuid::new_v4().to_string(),
351 request_id: active_request.id.clone(),
352 scope,
353 actor: actor.clone(),
354 decision,
355 decided_at,
356 note: note.clone(),
357 };
358
359 self.state = match decision {
360 ApprovalDecisionKind::Approved => ApprovalState::Approved,
361 ApprovalDecisionKind::Rejected => ApprovalState::Rejected,
362 ApprovalDecisionKind::NeedsRevision => ApprovalState::NeedsRevision,
363 };
364 self.scope = None;
365 self.active_request = None;
366 self.decisions.push(entry.clone());
367 self.decided_at = Some(decided_at);
368 self.decided_by = Some(actor.id);
369 self.note = note;
370
371 Ok(entry)
372 }
373}
374
375pub fn default_actor_kind_for_scope(scope: ApprovalScope) -> ApprovalActorKind {
377 match scope {
378 ApprovalScope::PreExecution => ApprovalActorKind::Supervisor,
379 ApprovalScope::Review => ApprovalActorKind::Reviewer,
380 ApprovalScope::TestValidation => ApprovalActorKind::Tester,
381 }
382}
383
384pub fn actor_kind_for_agent_role(role: Option<&AgentRole>) -> ApprovalActorKind {
386 match role {
387 Some(AgentRole::Reviewer | AgentRole::SecurityReviewer) => ApprovalActorKind::Reviewer,
388 Some(AgentRole::Tester) => ApprovalActorKind::Tester,
389 Some(AgentRole::Supervisor) => ApprovalActorKind::Supervisor,
390 _ => ApprovalActorKind::User,
391 }
392}