gestura_core/orchestrator/
approval.rs

1use super::{AgentRole, DelegatedTask};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6/// Approval state tracked by the supervisor.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
8#[serde(rename_all = "snake_case")]
9pub enum ApprovalState {
10    /// No explicit approval step is required.
11    #[default]
12    NotRequired,
13    /// Waiting for an explicit decision.
14    Pending,
15    /// Approved to proceed or complete.
16    Approved,
17    /// Rejected and should not proceed.
18    Rejected,
19    /// Revision requested before retrying.
20    NeedsRevision,
21}
22
23/// Gate scope for an approval request.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum ApprovalScope {
27    /// Approval before execution begins.
28    PreExecution,
29    /// Review approval after execution.
30    Review,
31    /// Test validation approval after review/execution.
32    TestValidation,
33}
34
35/// Actor category authorized to make approval decisions.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum ApprovalActorKind {
39    /// End-user/operator acting directly.
40    User,
41    /// Supervisor or orchestration lead.
42    Supervisor,
43    /// Reviewer acting on a review gate.
44    Reviewer,
45    /// Tester acting on a validation gate.
46    Tester,
47    /// System-generated provenance.
48    System,
49}
50
51/// Actor provenance for an approval request or decision.
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct ApprovalActor {
54    /// Actor kind.
55    pub kind: ApprovalActorKind,
56    /// Stable actor identifier or origin label.
57    pub id: String,
58    /// Optional display name.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub display_name: Option<String>,
61}
62
63impl ApprovalActor {
64    /// Build a new actor record.
65    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    /// Build the standard orchestrator/system actor.
74    pub fn system(id: impl Into<String>) -> Self {
75        Self::new(ApprovalActorKind::System, id)
76    }
77}
78
79/// Policy requirement for a single approval scope.
80#[derive(Debug, Clone, Serialize, Deserialize, Default)]
81pub struct ApprovalRequirement {
82    /// Scope this requirement applies to.
83    pub scope: Option<ApprovalScope>,
84    /// Whether the approval gate is required.
85    #[serde(default)]
86    pub required: bool,
87    /// Actor kinds allowed to decide this gate.
88    #[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    /// Returns true when the actor kind is allowed for this scope.
102    pub fn allows(&self, actor_kind: ApprovalActorKind) -> bool {
103        self.allowed_deciders.contains(&actor_kind)
104    }
105}
106
107/// End-to-end approval policy for a delegated task.
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109pub struct ApprovalPolicy {
110    /// Pre-execution approval requirement.
111    #[serde(default)]
112    pub pre_execution: ApprovalRequirement,
113    /// Review requirement after execution.
114    #[serde(default)]
115    pub review: ApprovalRequirement,
116    /// Test validation requirement after execution/review.
117    #[serde(default)]
118    pub test_validation: ApprovalRequirement,
119}
120
121impl ApprovalPolicy {
122    /// Build the default policy for a delegated task.
123    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    /// Return the configured requirement for a scope.
144    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/// Snapshot of a single approval request.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ApprovalRequest {
156    /// Request identifier.
157    pub id: String,
158    /// Gate scope being requested.
159    pub scope: ApprovalScope,
160    /// Actor that requested the gate.
161    pub requested_by: ApprovalActor,
162    /// Request timestamp.
163    pub requested_at: DateTime<Utc>,
164    /// Optional request note.
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub note: Option<String>,
167}
168
169/// Decision outcome for an approval request.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum ApprovalDecisionKind {
173    /// Gate approved.
174    Approved,
175    /// Gate explicitly rejected.
176    Rejected,
177    /// Gate requires revision before retrying.
178    NeedsRevision,
179}
180
181/// Audit entry for a single approval decision.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ApprovalDecision {
184    /// Decision identifier.
185    pub id: String,
186    /// Request identifier this decision applies to.
187    pub request_id: String,
188    /// Gate scope being decided.
189    pub scope: ApprovalScope,
190    /// Decision actor.
191    pub actor: ApprovalActor,
192    /// Decision outcome.
193    pub decision: ApprovalDecisionKind,
194    /// Decision timestamp.
195    pub decided_at: DateTime<Utc>,
196    /// Optional note.
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub note: Option<String>,
199}
200
201/// Approval details tracked per task.
202#[derive(Debug, Clone, Serialize, Deserialize, Default)]
203pub struct TaskApprovalRecord {
204    /// Current approval state.
205    pub state: ApprovalState,
206    /// Active scope being awaited, if any.
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub scope: Option<ApprovalScope>,
209    /// Active request awaiting a decision.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub active_request: Option<ApprovalRequest>,
212    /// Approval policy derived for the task.
213    #[serde(default)]
214    pub policy: ApprovalPolicy,
215    /// Historical requests.
216    #[serde(default, skip_serializing_if = "Vec::is_empty")]
217    pub requests: Vec<ApprovalRequest>,
218    /// Historical decisions.
219    #[serde(default, skip_serializing_if = "Vec::is_empty")]
220    pub decisions: Vec<ApprovalDecision>,
221    /// When approval was requested.
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    pub requested_at: Option<DateTime<Utc>>,
224    /// When a decision was made.
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub decided_at: Option<DateTime<Utc>>,
227    /// Actor that made the latest decision.
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub decided_by: Option<String>,
230    /// Optional explanatory note.
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    pub note: Option<String>,
233}
234
235impl TaskApprovalRecord {
236    /// Build a record when no explicit gate is currently pending.
237    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    /// Build a record with an active request.
253    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    /// Reset the active gate state while preserving historical audit entries.
265    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    /// Queue a new approval request while preserving prior audit entries.
277    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    /// Return the most recent decision, if any.
302    pub fn latest_decision(&self) -> Option<&ApprovalDecision> {
303        self.decisions.last()
304    }
305
306    /// Resolve the list of allowed actors for the current scope.
307    pub fn allowed_actor_kinds(&self, scope: ApprovalScope) -> &[ApprovalActorKind] {
308        &self.policy.requirement(scope).allowed_deciders
309    }
310
311    /// Ensure the actor is authorized for the given approval scope.
312    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    /// Record an explicit decision for the active request.
329    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
375/// Build the default approval actor role for a task gate scope.
376pub 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
384/// Infer the default actor kind from an agent role when possible.
385pub 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}