use crate::permission::{partition_by_ledger, ApprovalLedger};
use car_verify::{FlowGateDecision, FlowViolation, FlowViolationKind};
use serde::Serialize;
pub fn flow_fingerprint(v: &FlowViolation) -> String {
let kind = match v.kind {
FlowViolationKind::SensitiveToSink => "sensitive_to_sink",
FlowViolationKind::ForbiddenSequence => "forbidden_sequence",
};
let mut actions = v.actions.clone();
actions.sort();
format!(
"flow:{kind}:{}:{}",
actions.join(","),
v.key.as_deref().unwrap_or("")
)
}
#[derive(Debug, Clone, Serialize)]
pub struct PendingFlowApproval {
pub fingerprint: String,
pub violation: FlowViolation,
}
#[derive(Debug, Clone, Serialize)]
pub struct FlowEnforcement {
pub allow: bool,
pub blocked: Vec<FlowViolation>,
pub pending: Vec<PendingFlowApproval>,
pub reason: String,
}
pub fn enforce_flow(decision: &FlowGateDecision, ledger: &ApprovalLedger) -> FlowEnforcement {
let part = partition_by_ledger(
decision.blocked.clone(),
&decision.needs_approval,
flow_fingerprint,
ledger,
);
let blocked = part.blocked;
let pending: Vec<PendingFlowApproval> = part
.pending
.into_iter()
.map(|(fingerprint, violation)| PendingFlowApproval {
fingerprint,
violation,
})
.collect();
let allow = blocked.is_empty() && pending.is_empty();
let reason = if !blocked.is_empty() {
format!(
"blocked: {} flow hazard(s) refused ({} pending approval)",
blocked.len(),
pending.len()
)
} else if !pending.is_empty() {
format!("{} flow hazard(s) await human approval", pending.len())
} else {
"no flow hazards require action — may proceed".to_string()
};
FlowEnforcement {
allow,
blocked,
pending,
reason,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::permission::{ApprovalDecision, ApprovalRecord, PermissionTier};
use car_verify::{FlowAction, FlowReport, FlowViolation, FlowViolationKind};
fn violation(kind: FlowViolationKind, actions: &[&str], key: Option<&str>) -> FlowViolation {
FlowViolation {
kind,
actions: actions.iter().map(|s| s.to_string()).collect(),
key: key.map(|s| s.to_string()),
explanation: "x".into(),
mitigation: "y".into(),
}
}
fn approval(fp: &str, decision: ApprovalDecision) -> ApprovalRecord {
ApprovalRecord {
fingerprint: fp.to_string(),
required_tier: PermissionTier::FullAccess,
decision,
reviewer: "human".into(),
reason: "test".into(),
evidence: None,
decided_at: "2026-06-28T00:00:00Z".into(),
}
}
#[test]
fn hard_block_is_blocked() {
let decision = FlowGateDecision {
action: FlowAction::Block,
blocked: vec![violation(
FlowViolationKind::SensitiveToSink,
&["a2"],
Some("s"),
)],
needs_approval: vec![],
reason: "".into(),
};
let r = enforce_flow(&decision, &ApprovalLedger::new());
assert!(!r.allow);
assert_eq!(r.blocked.len(), 1);
}
#[test]
fn novel_approval_is_pending() {
let decision = FlowGateDecision {
action: FlowAction::RequireApproval,
blocked: vec![],
needs_approval: vec![violation(
FlowViolationKind::ForbiddenSequence,
&["a1", "a2"],
None,
)],
reason: "".into(),
};
let r = enforce_flow(&decision, &ApprovalLedger::new());
assert!(!r.allow);
assert_eq!(r.pending.len(), 1);
}
#[test]
fn previously_approved_is_allowed() {
let v = violation(FlowViolationKind::ForbiddenSequence, &["a1", "a2"], None);
let fp = flow_fingerprint(&v);
let mut ledger = ApprovalLedger::new();
ledger
.record(approval(&fp, ApprovalDecision::Approved))
.unwrap();
let decision = FlowGateDecision {
action: FlowAction::RequireApproval,
blocked: vec![],
needs_approval: vec![v],
reason: "".into(),
};
let r = enforce_flow(&decision, &ledger);
assert!(
r.allow,
"a previously-approved hazard runs without re-asking"
);
assert!(r.pending.is_empty() && r.blocked.is_empty());
}
#[test]
fn previously_rejected_is_blocked() {
let v = violation(FlowViolationKind::ForbiddenSequence, &["a1", "a2"], None);
let fp = flow_fingerprint(&v);
let mut ledger = ApprovalLedger::new();
ledger
.record(approval(&fp, ApprovalDecision::Rejected))
.unwrap();
let decision = FlowGateDecision {
action: FlowAction::RequireApproval,
blocked: vec![],
needs_approval: vec![v],
reason: "".into(),
};
let r = enforce_flow(&decision, &ledger);
assert!(!r.allow);
assert_eq!(r.blocked.len(), 1);
}
#[test]
fn fingerprint_is_action_order_independent() {
let a = violation(FlowViolationKind::ForbiddenSequence, &["a1", "a2"], None);
let b = violation(FlowViolationKind::ForbiddenSequence, &["a2", "a1"], None);
assert_eq!(flow_fingerprint(&a), flow_fingerprint(&b));
}
#[test]
fn clean_report_allows() {
let report = FlowReport {
safe: true,
violations: vec![],
};
let decision = car_verify::gate_flow(&report, &Default::default());
let r = enforce_flow(&decision, &ApprovalLedger::new());
assert!(r.allow);
}
}