use serde_json::{json, Value};
use crate::audit::append_protected_audit_event;
use crate::automation_v2::governance::GovernanceApprovalStatus;
use crate::{now_ms, AppState};
use super::governance::approval_receipt_matches_tenant;
fn action_gate_field<'a>(context: &'a Value, field: &str) -> Option<&'a Value> {
context.get("action_gate")?.get(field)
}
pub(super) fn is_action_gate_context(context: &Value) -> bool {
action_gate_field(context, "action_hash")
.and_then(Value::as_str)
.is_some()
}
pub(super) fn same_action_gate_scope(left: &Value, right: &Value) -> bool {
["action_hash", "session_id", "message_id", "run_id"]
.into_iter()
.all(|field| action_gate_field(left, field) == action_gate_field(right, field))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ActionGateApprovalState {
Pending,
ApprovedAndConsumed,
Denied,
}
impl AppState {
pub(crate) async fn consume_action_gate_approval(
&self,
approval_id: &str,
tenant_context: &tandem_types::TenantContext,
) -> anyhow::Result<ActionGateApprovalState> {
let now = now_ms();
let resolution = {
let mut guard = self.automation_governance.write().await;
let request = guard
.approvals
.get_mut(approval_id)
.filter(|request| approval_receipt_matches_tenant(request, tenant_context))
.ok_or_else(|| anyhow::anyhow!("action-gate approval request not found"))?;
match request.status {
GovernanceApprovalStatus::Pending => ActionGateApprovalState::Pending,
GovernanceApprovalStatus::Denied | GovernanceApprovalStatus::Expired => {
ActionGateApprovalState::Denied
}
GovernanceApprovalStatus::Approved => {
if now >= request.expires_at_ms
|| request
.context
.pointer("/action_gate/consumed_at_ms")
.is_some()
{
ActionGateApprovalState::Denied
} else {
request.context["action_gate"]["consumed_at_ms"] = json!(now);
request.updated_at_ms = now;
guard.updated_at_ms = now;
ActionGateApprovalState::ApprovedAndConsumed
}
}
}
};
if resolution == ActionGateApprovalState::ApprovedAndConsumed {
self.persist_automation_governance().await?;
append_protected_audit_event(
self,
"automation.governance.approval.consumed",
tenant_context,
tenant_context.actor_id.clone(),
json!({ "approvalID": approval_id }),
)
.await?;
}
Ok(resolution)
}
}