gglib_core/ports/
council_approvals.rs

1//! Port trait for the orchestrator approval registry.
2//!
3//! The approval registry is a **process-local** store that pairs an
4//! `approval_id` with a `tokio::oneshot::Sender<ApprovalDecision>`.  When the
5//! executor reaches a HITL gate it:
6//!
7//! 1. Creates a oneshot channel and calls [`CouncilApprovalRegistryPort::register`]
8//!    with the sender.
9//! 2. Emits [`crate::domain::council::events::CouncilEvent::AwaitingApproval`]
10//!    carrying the `approval_id`.
11//! 3. Awaits the receiver.
12//!
13//! An external actor (the Axum handler or the CLI prompter) retrieves and
14//! resolves the decision by calling
15//! [`CouncilApprovalRegistryPort::resolve`].
16
17use async_trait::async_trait;
18use tokio::sync::oneshot;
19
20use crate::domain::council::task_graph::TaskGraph;
21
22// =============================================================================
23// ApprovalDecision
24// =============================================================================
25
26/// The decision a human makes at a HITL gate.
27#[derive(Debug)]
28pub enum ApprovalDecision {
29    /// Proceed as-is.
30    Approve,
31    /// Proceed with a user-supplied modified graph.
32    ///
33    /// Only meaningful for `Plan` gates; for `Node` / `Tool` gates the
34    /// executor ignores the graph and treats this as a plain `Approve`.
35    ApproveWithEdits(Box<TaskGraph>),
36    /// Reject and terminate the run.
37    Reject(String),
38}
39
40// =============================================================================
41// CouncilApprovalRegistryPort
42// =============================================================================
43
44/// Process-local store for pending HITL approval requests.
45///
46/// Implementations MUST be `Send + Sync + 'static` so the registry can be
47/// shared across the Axum router, spawned executor tasks, and CLI prompts.
48///
49/// # Acknowledged limitation
50///
51/// The registry is intentionally **process-local** in v1.  Cross-process
52/// approval coordination (e.g. multiple server instances) is out of scope.
53#[async_trait]
54pub trait CouncilApprovalRegistryPort: Send + Sync + 'static {
55    /// Register a pending approval gate.
56    ///
57    /// The `approval_id` is a UUID v4 string generated by the executor.
58    /// The `sender` will be consumed by the next `resolve()` call with
59    /// the same id.
60    fn register(&self, approval_id: String, sender: oneshot::Sender<ApprovalDecision>);
61
62    /// Resolve a pending approval gate.
63    ///
64    /// Removes the entry keyed by `approval_id` from the registry and sends
65    /// `decision` on the stored oneshot channel.
66    ///
67    /// Returns `true` if the approval existed and was resolved, `false` if
68    /// the id was not found (e.g. the run already completed or the sender was
69    /// dropped).
70    fn resolve(&self, approval_id: &str, decision: ApprovalDecision) -> bool;
71
72    /// Returns `true` if an approval with the given id is currently pending.
73    fn is_pending(&self, approval_id: &str) -> bool;
74}