klieo-ops 3.5.0

Operational layer above klieo-core: supervisor, governor, gates, escalation, worklog, handoff.
Documentation
//! `Gate` trait + decision enum.

use async_trait::async_trait;
use std::time::Duration;

/// Inputs to a gate evaluation.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct GateRequest {
    /// Tool being invoked.
    pub tool_name: String,
    /// Tool arguments (JSON).
    pub args: serde_json::Value,
}

impl GateRequest {
    /// Construct a `GateRequest`. Use this instead of struct literal
    /// syntax — `#[non_exhaustive]` blocks cross-crate struct literals.
    pub fn new(tool_name: impl Into<String>, args: serde_json::Value) -> Self {
        Self {
            tool_name: tool_name.into(),
            args,
        }
    }
}

/// Stable policy code for audit trails (Phase A: free-form string).
pub type PolicyCode = String;

/// Outcome of a gate evaluation.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum GateDecision {
    /// Permit the call.
    Allow,
    /// Refuse the call.
    Deny {
        /// Stable code identifying the rule.
        code: PolicyCode,
        /// Reason text.
        reason: String,
    },
    /// Require human approval. Phase A: surfaces as `ToolError::Permanent`
    /// because escalation primitive is Phase B.
    RequireApproval {
        /// Ticket identifier reserved for Phase B escalation wiring.
        ticket: String,
        /// Quorum count.
        quorum: u8,
    },
}

/// Reasons a gate's `wait_for_approval` can return without an Allow.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ApprovalError {
    /// Wait exceeded the supplied timeout.
    #[error("approval timed out after {millis}ms")]
    TimedOut {
        /// Wait duration in millis.
        millis: u64,
    },
    /// Approval was explicitly denied (e.g. resolution outcome = Denied).
    #[error("approval denied: {0}")]
    Denied(String),
    /// Halted by kill-switch.
    #[error("halted")]
    Halted,
    /// Gate does not implement waiting. Default-impl return value.
    #[error("gate does not support wait_for_approval")]
    NotSupported,
    /// Approver-signature verification failed (any of: math-invalid sig,
    /// approver not in registry, quorum not met after dedup).
    #[error("approver verification failed: {0}")]
    VerificationFailed(String),
}

/// Final decision returned by `wait_for_approval` after suspension.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum ApprovalOutcome {
    /// Approval came through and the original action is permitted.
    Allow,
    /// Approval was denied; downstream MUST surface a Denial.
    Deny {
        /// Stable code.
        code: String,
        /// Free-form reason.
        reason: String,
    },
}

/// Policy that governs what happens when a `wait_for_approval` call times out.
///
/// The default is `RequeueAndEscalate { max_requeues: 3 }` per spec § 2.3.
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum ApprovalTimeoutPolicy {
    /// Immediately return `ToolError::Permanent` on the first timeout.
    Deny,
    /// Retry `wait_for_approval` with the same ticket up to `max_requeues`
    /// times, bumping severity by one level on each retry; after exhaustion
    /// return `ToolError::Permanent`.
    RequeueAndEscalate {
        /// Maximum number of requeue attempts before terminal failure.
        max_requeues: u8,
    },
}

impl Default for ApprovalTimeoutPolicy {
    fn default() -> Self {
        Self::RequeueAndEscalate { max_requeues: 3 }
    }
}

/// Pre-effect policy gate.
#[async_trait]
pub trait Gate: Send + Sync {
    /// Evaluate against the supplied request.
    async fn evaluate(&self, req: GateRequest) -> GateDecision;

    /// Stable gate name (used for audit).
    fn name(&self) -> &'static str;

    /// Whether this gate may return [`GateDecision::RequireApproval`] from
    /// [`evaluate`](Self::evaluate). Default is `false`.
    ///
    /// Gates that can suspend (e.g. `FourEyesGate` in M8) override this to
    /// return `true`. `OpsRuntimeBuilder` uses this signal to enforce the
    /// invariant: if any registered gate returns `true`, the builder MUST be
    /// configured with an `Escalation` impl before [`spawn`] is called.
    ///
    /// [`spawn`]: crate::runtime::OpsRuntimeBuilder::spawn
    fn may_require_approval(&self) -> bool {
        false
    }

    /// If `evaluate` returned `RequireApproval { ticket, .. }`, the
    /// caller may invoke this method to await resolution and receive a
    /// final [`ApprovalOutcome`]. Default impl returns
    /// [`ApprovalError::NotSupported`] — gates that suspend
    /// (`FourEyesGate`) override.
    async fn wait_for_approval(
        &self,
        _ticket: String,
        _timeout: Duration,
    ) -> Result<ApprovalOutcome, ApprovalError> {
        Err(ApprovalError::NotSupported)
    }
}