use crate::error::LaserError;
use crate::laser::Laser;
use crate::provenance::AgentTopic;
use crate::types::{ConversationId, MintUlid};
use async_trait::async_trait;
use laser_wire::agent::{AgentEnvelope, AgentId, RecordId, validate};
use laser_wire::content::ContentType;
use laser_wire::framing::{decode_named, encode_named};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
pub const POLICY_DECISION_OPERATION: &str = "policy_decision";
#[async_trait]
pub trait ActionGovernor: Send + Sync {
async fn decide(&self, action: &GovernedAction<'_>) -> Result<ActionDecision, LaserError>;
}
#[derive(Debug)]
pub struct GovernedAction<'a> {
pub kind: ActionKind,
pub stream: &'a str,
pub topic: &'a str,
pub source: Option<&'a str>,
pub target: Option<&'a str>,
pub conversation: Option<ConversationId>,
pub correlation: Option<&'a str>,
pub operation: Option<&'a str>,
pub tool: Option<&'a str>,
pub on_behalf_of: Option<&'a str>,
pub purpose: Option<&'a str>,
pub data_classification: Option<&'a str>,
pub payload: &'a [u8],
pub signed: bool,
pub counters: ActionCounters,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, strum::Display, strum::EnumString, strum::IntoStaticStr,
)]
#[strum(serialize_all = "snake_case")]
#[non_exhaustive]
pub enum ActionKind {
Send,
Publish,
Request,
Command,
Response,
Event,
Status,
Error,
MemoryWrite,
}
impl ActionKind {
pub fn as_str(self) -> &'static str {
self.into()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ActionCounters {
pub sends: u64,
pub requests: u64,
pub bytes_sent: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ActionDecision {
pub verdict: Verdict,
pub reason: Option<String>,
pub policy: Option<PolicyRef>,
pub risk_score: Option<f64>,
}
impl ActionDecision {
pub fn allow() -> Self {
Self::of(Verdict::Allow)
}
pub fn observe() -> Self {
Self::of(Verdict::Observe)
}
pub fn block(reason: impl Into<String>) -> Self {
let mut decision = Self::of(Verdict::Block);
decision.reason = Some(reason.into());
decision
}
pub fn step_up(scope: impl Into<String>) -> Self {
Self::of(Verdict::StepUp {
scope: scope.into(),
})
}
pub fn modify(body: impl Into<Vec<u8>>) -> Self {
Self::of(Verdict::Modify { body: body.into() })
}
pub fn defer(reason: impl Into<String>) -> Self {
let mut decision = Self::of(Verdict::Defer);
decision.reason = Some(reason.into());
decision
}
#[must_use]
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
#[must_use]
pub fn with_policy(mut self, policy: PolicyRef) -> Self {
self.policy = Some(policy);
self
}
#[must_use]
pub fn with_risk_score(mut self, risk_score: f64) -> Self {
self.risk_score = Some(risk_score);
self
}
fn of(verdict: Verdict) -> Self {
Self {
verdict,
reason: None,
policy: None,
risk_score: None,
}
}
}
#[derive(Debug, Clone, PartialEq, strum::Display, strum::IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
pub enum Verdict {
Allow,
Observe,
Block,
StepUp { scope: String },
Modify { body: Vec<u8> },
Defer,
}
impl Verdict {
pub fn as_str(&self) -> &'static str {
self.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicyRef {
pub pack_id: String,
pub pack_version: String,
pub rule_ids: Vec<String>,
}
#[derive(
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
strum::Display,
strum::EnumString,
strum::IntoStaticStr,
)]
#[strum(serialize_all = "snake_case")]
pub enum GovernorMode {
Observe,
#[default]
Enforce,
}
impl GovernorMode {
pub fn as_str(self) -> &'static str {
self.into()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PolicyEvidence {
pub decision_id: String,
pub decision: String,
pub mode: String,
pub kind: String,
pub stream: String,
pub topic: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub conversation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub correlation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub operation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub on_behalf_of: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub approved_scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub policy: Option<PolicyRef>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub risk_score: Option<f64>,
pub receipt_digest: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub previous_digest: Option<String>,
pub outcome: String,
pub at_micros: u64,
}
impl PolicyEvidence {
pub fn encode(&self) -> Result<Vec<u8>, LaserError> {
Ok(encode_named(self)?)
}
pub fn decode(payload: &[u8]) -> Result<Self, LaserError> {
Ok(decode_named(payload)?)
}
fn seal(mut self) -> Result<Self, LaserError> {
self.receipt_digest = String::new();
let canonical = self.encode()?;
self.receipt_digest = blake3::hash(&canonical).to_string();
Ok(self)
}
}
pub fn verify_evidence_chain(evidence: &[PolicyEvidence]) -> bool {
let mut previous: Option<&str> = None;
for item in evidence {
if item.previous_digest.as_deref() != previous {
return false;
}
match item.clone().seal() {
Ok(sealed) if sealed.receipt_digest == item.receipt_digest => {}
_ => return false,
}
previous = Some(item.receipt_digest.as_str());
}
true
}
impl Laser {
#[must_use]
pub fn with_governor(&self, governor: Arc<dyn ActionGovernor>, mode: GovernorMode) -> Self {
self.with_governor_retention(governor, mode, GovernorRetention::default())
}
#[must_use]
pub fn with_governor_retention(
&self,
governor: Arc<dyn ActionGovernor>,
mode: GovernorMode,
retention: GovernorRetention,
) -> Self {
let mut governed = self.clone();
governed.governor = Some(Arc::new(GovernorState::with_retention(
governor, mode, retention,
)));
governed
}
pub(crate) async fn govern(
&self,
mut action: GovernedAction<'_>,
) -> Result<Option<Vec<u8>>, LaserError> {
let Some(state) = self.governor.clone() else {
return Ok(None);
};
action.counters = state.snapshot();
let decision = state.governor.decide(&action).await?;
match action.kind {
ActionKind::Request => state.requests.fetch_add(1, Ordering::Relaxed),
_ => state.sends.fetch_add(1, Ordering::Relaxed),
};
let applied = apply(state.mode, decision.verdict.clone());
if applied.recorded {
let emitted = state
.emit_chained_evidence(self, &action, &decision, applied.outcome)
.await;
match state.mode {
GovernorMode::Observe => {
if let Err(error) = emitted {
tracing::warn!(%error, "policy evidence write failed (observe mode)");
}
}
GovernorMode::Enforce => match (&applied.denial, emitted) {
(None, Err(error)) => return Err(error),
(Some(_), Err(error)) => {
tracing::warn!(%error, "policy evidence write failed (denial stands)");
}
(_, Ok(())) => {}
},
}
}
if let Some(denial) = applied.denial {
return Err(denial);
}
let sent = applied.body.as_ref().map_or(action.payload.len(), Vec::len);
state.bytes_sent.fetch_add(sent as u64, Ordering::Relaxed);
Ok(applied.body)
}
async fn emit_evidence(
&self,
action: &GovernedAction<'_>,
evidence: PolicyEvidence,
) -> Result<(), LaserError> {
let source: AgentId = action
.source
.and_then(|source| source.parse().ok())
.unwrap_or_else(|| "governor".parse().expect("static agent id is valid"));
let conversation = action.conversation.unwrap_or_default();
let envelope = AgentEnvelope::event(
RecordId::mint(),
conversation.into(),
source,
evidence.encode()?,
)
.with_operation(POLICY_DECISION_OPERATION);
validate(&envelope)?;
let payload = encode_named(&envelope)?;
let headers = crate::agent::agdx_headers(&envelope, ContentType::Cbor)?;
let message = iggy::prelude::IggyMessage::builder()
.payload(bytes::Bytes::from(payload))
.user_headers(headers)
.build()?;
let partition_key = conversation.to_string();
self.send_batch_on(
action.stream,
&AgentTopic::Audit.topic_string(),
vec![message],
Some(&partition_key),
)
.await
.map(|_| ())
}
}
pub(crate) struct GovernorState {
governor: Arc<dyn ActionGovernor>,
mode: GovernorMode,
sends: AtomicU64,
requests: AtomicU64,
bytes_sent: AtomicU64,
chain: tokio::sync::Mutex<HashMap<Option<u128>, Arc<ChainEntry>>>,
chain_released: Arc<tokio::sync::Notify>,
retention: GovernorRetention,
}
struct ChainEntry {
digest: tokio::sync::Mutex<Option<String>>,
touched_micros: AtomicU64,
}
struct ChainLease {
entry: Arc<ChainEntry>,
released: Arc<tokio::sync::Notify>,
}
impl Drop for ChainLease {
fn drop(&mut self) {
self.entry
.touched_micros
.store(now_micros(), Ordering::Release);
self.released.notify_one();
}
}
#[derive(Clone, Copy, Debug)]
pub struct GovernorRetention {
pub capacity: usize,
pub idle_ttl: std::time::Duration,
}
impl Default for GovernorRetention {
fn default() -> Self {
Self {
capacity: 4_096,
idle_ttl: std::time::Duration::from_secs(3_600),
}
}
}
impl GovernorState {
pub(crate) fn new(governor: Arc<dyn ActionGovernor>, mode: GovernorMode) -> Self {
Self::with_retention(governor, mode, GovernorRetention::default())
}
pub(crate) fn with_retention(
governor: Arc<dyn ActionGovernor>,
mode: GovernorMode,
mut retention: GovernorRetention,
) -> Self {
retention.capacity = retention.capacity.max(1);
Self {
governor,
mode,
sends: AtomicU64::new(0),
requests: AtomicU64::new(0),
bytes_sent: AtomicU64::new(0),
chain: tokio::sync::Mutex::new(HashMap::new()),
chain_released: Arc::new(tokio::sync::Notify::new()),
retention,
}
}
fn prune_chain(&self, chain: &mut HashMap<Option<u128>, Arc<ChainEntry>>, now_micros: u64) {
let ttl_micros = u64::try_from(self.retention.idle_ttl.as_micros()).unwrap_or(u64::MAX);
chain.retain(|_, entry| {
Arc::strong_count(entry) > 1
|| now_micros.saturating_sub(entry.touched_micros.load(Ordering::Acquire))
< ttl_micros
});
}
async fn acquire_chain(&self, chain_key: Option<u128>) -> ChainLease {
loop {
let released = self.chain_released.notified();
{
let now = now_micros();
let mut chain = self.chain.lock().await;
self.prune_chain(&mut chain, now);
if let Some(entry) = chain.get(&chain_key) {
return ChainLease {
entry: Arc::clone(entry),
released: Arc::clone(&self.chain_released),
};
}
while chain.len() >= self.retention.capacity {
let oldest = chain
.iter()
.filter(|(_, entry)| Arc::strong_count(entry) == 1)
.min_by_key(|(_, entry)| entry.touched_micros.load(Ordering::Acquire))
.map(|(key, _)| *key);
let Some(oldest) = oldest else {
break;
};
chain.remove(&oldest);
}
if chain.len() < self.retention.capacity {
let entry = Arc::new(ChainEntry {
digest: tokio::sync::Mutex::new(None),
touched_micros: AtomicU64::new(now),
});
chain.insert(chain_key, Arc::clone(&entry));
return ChainLease {
entry,
released: Arc::clone(&self.chain_released),
};
}
}
released.await;
}
}
fn snapshot(&self) -> ActionCounters {
ActionCounters {
sends: self.sends.load(Ordering::Relaxed),
requests: self.requests.load(Ordering::Relaxed),
bytes_sent: self.bytes_sent.load(Ordering::Relaxed),
}
}
async fn emit_chained_evidence(
&self,
laser: &Laser,
action: &GovernedAction<'_>,
decision: &ActionDecision,
outcome: &'static str,
) -> Result<(), LaserError> {
let chain_key = action.conversation.map(|id| id.as_u128());
let slot = self.acquire_chain(chain_key).await;
let mut previous = slot.entry.digest.lock().await;
let evidence = self.seal_evidence(action, decision, outcome, previous.clone())?;
let digest = evidence.receipt_digest.clone();
laser.emit_evidence(action, evidence).await?;
*previous = Some(digest);
Ok(())
}
fn seal_evidence(
&self,
action: &GovernedAction<'_>,
decision: &ActionDecision,
outcome: &'static str,
previous_digest: Option<String>,
) -> Result<PolicyEvidence, LaserError> {
let approved_scope = match &decision.verdict {
Verdict::StepUp { scope } => Some(scope.clone()),
_ => None,
};
PolicyEvidence {
decision_id: ulid::Ulid::generate().to_string(),
decision: decision.verdict.as_str().to_owned(),
mode: self.mode.as_str().to_owned(),
kind: action.kind.as_str().to_owned(),
stream: action.stream.to_owned(),
topic: action.topic.to_owned(),
source: action.source.map(str::to_owned),
target: action.target.map(str::to_owned),
conversation: action.conversation.map(|id| id.to_string()),
correlation: action.correlation.map(str::to_owned),
operation: action.operation.map(str::to_owned),
tool: action.tool.map(str::to_owned),
on_behalf_of: action.on_behalf_of.map(str::to_owned),
reason: decision.reason.clone(),
approved_scope,
policy: decision.policy.clone(),
risk_score: decision.risk_score,
receipt_digest: String::new(),
previous_digest,
outcome: outcome.to_owned(),
at_micros: now_micros(),
}
.seal()
}
}
struct AppliedVerdict {
recorded: bool,
outcome: &'static str,
body: Option<Vec<u8>>,
denial: Option<LaserError>,
}
fn apply(mode: GovernorMode, verdict: Verdict) -> AppliedVerdict {
let enforced = matches!(mode, GovernorMode::Enforce);
match verdict {
Verdict::Allow => AppliedVerdict {
recorded: false,
outcome: "effected",
body: None,
denial: None,
},
Verdict::Observe => AppliedVerdict {
recorded: true,
outcome: "effected",
body: None,
denial: None,
},
Verdict::Modify { body } => AppliedVerdict {
recorded: true,
outcome: "effected",
body: enforced.then_some(body),
denial: None,
},
Verdict::Block => AppliedVerdict {
recorded: true,
outcome: if enforced { "blocked" } else { "effected" },
body: None,
denial: enforced
.then(|| LaserError::PolicyBlocked("the governor blocked this action".to_owned())),
},
Verdict::StepUp { scope } => AppliedVerdict {
recorded: true,
outcome: if enforced { "step_up" } else { "effected" },
body: None,
denial: enforced.then(|| LaserError::StepUpRequired { scope }),
},
Verdict::Defer => AppliedVerdict {
recorded: true,
outcome: if enforced { "deferred" } else { "effected" },
body: None,
denial: enforced.then(|| {
LaserError::PolicyDeferred("the governor deferred this action".to_owned())
}),
},
}
}
fn now_micros() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|elapsed| u64::try_from(elapsed.as_micros()).unwrap_or(u64::MAX))
.unwrap_or(0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuorumPolicy {
All,
Any,
AtLeast(usize),
}
impl QuorumPolicy {
fn met(self, affirmative: usize, total: usize) -> bool {
match self {
QuorumPolicy::All => affirmative == total,
QuorumPolicy::Any => affirmative >= 1,
QuorumPolicy::AtLeast(n) => affirmative >= n,
}
}
}
#[derive(Clone)]
struct Voter {
name: String,
governor: Arc<dyn ActionGovernor>,
mandatory: bool,
}
#[derive(Clone)]
pub struct QuorumGovernor {
voters: Vec<Voter>,
policy: QuorumPolicy,
}
impl QuorumGovernor {
pub fn new(policy: QuorumPolicy) -> Self {
Self {
voters: Vec::new(),
policy,
}
}
#[must_use]
pub fn voter(
mut self,
name: impl Into<String>,
governor: Arc<dyn ActionGovernor>,
mandatory: bool,
) -> Self {
self.voters.push(Voter {
name: name.into(),
governor,
mandatory,
});
self
}
}
#[async_trait]
impl ActionGovernor for QuorumGovernor {
async fn decide(&self, action: &GovernedAction<'_>) -> Result<ActionDecision, LaserError> {
if self.voters.is_empty() {
return Ok(ActionDecision::block(
"quorum governor has no configured voters",
));
}
if let QuorumPolicy::AtLeast(required) = self.policy
&& (required == 0 || required > self.voters.len())
{
return Ok(ActionDecision::block(format!(
"quorum threshold {required} is invalid for {} voters",
self.voters.len()
)));
}
let mut names = std::collections::HashSet::with_capacity(self.voters.len());
if let Some(duplicate) = self
.voters
.iter()
.find(|voter| !names.insert(voter.name.as_str()))
{
return Ok(ActionDecision::block(format!(
"quorum voter '{}' is configured more than once",
duplicate.name
)));
}
let votes = futures::future::join_all(
self.voters
.iter()
.map(|voter| async move { (voter, voter.governor.decide(action).await) }),
)
.await;
let mut ballot = Vec::with_capacity(votes.len());
let mut affirmative = 0usize;
let mut mandatory_denial: Option<ActionDecision> = None;
let mut mandatory_error: Option<&str> = None;
let mut best_affirmative: Option<ActionDecision> = None;
let mut best_denial: Option<ActionDecision> = None;
let mut replacement: Option<&[u8]> = None;
let mut conflicting_replacements = false;
for (voter, vote) in &votes {
let decision = vote.as_ref().map_err(|error| error.to_string());
ballot.push(format!(
"{}={}",
voter.name,
decision
.as_ref()
.map(|decision| decision.verdict.as_str().to_owned())
.unwrap_or_else(|error| format!("error({error})"))
));
let Ok(decision) = vote else {
if voter.mandatory {
mandatory_error = mandatory_error.or(Some(voter.name.as_str()));
}
continue;
};
if voter.mandatory && !is_affirmative(&decision.verdict) {
mandatory_denial = mandatory_denial.or(Some(decision.clone()));
}
if is_affirmative(&decision.verdict) {
affirmative += 1;
if let Verdict::Modify { body } = &decision.verdict {
match replacement {
None => replacement = Some(body),
Some(existing) if existing != body => conflicting_replacements = true,
Some(_) => {}
}
}
if affirmative_rank(&decision.verdict)
> best_affirmative
.as_ref()
.map_or(-1, |current| affirmative_rank(¤t.verdict))
{
best_affirmative = Some(decision.clone());
}
} else if denial_rank(&decision.verdict)
> best_denial
.as_ref()
.map_or(-1, |current| denial_rank(¤t.verdict))
{
best_denial = Some(decision.clone());
}
}
let reason = format!("quorum({:?}): {}", self.policy, ballot.join(", "));
if let Some(voter) = mandatory_error {
return Ok(annotate(
ActionDecision::block(format!("mandatory voter '{voter}' failed")),
&reason,
));
}
if let Some(decision) = mandatory_denial {
return Ok(annotate(decision, &reason));
}
if conflicting_replacements {
return Ok(annotate(
ActionDecision::block("quorum voters proposed conflicting body replacements"),
&reason,
));
}
if self.policy.met(affirmative, self.voters.len()) {
return Ok(annotate(
best_affirmative.unwrap_or_else(ActionDecision::allow),
&reason,
));
}
Ok(annotate(
best_denial
.unwrap_or_else(|| ActionDecision::block("no voter reached the required quorum")),
&reason,
))
}
}
fn annotate(mut decision: ActionDecision, ballot: &str) -> ActionDecision {
decision.reason = Some(match decision.reason.take() {
Some(detail) => format!("{detail}; {ballot}"),
None => ballot.to_owned(),
});
decision
}
fn is_affirmative(verdict: &Verdict) -> bool {
matches!(
verdict,
Verdict::Allow | Verdict::Observe | Verdict::Modify { .. }
)
}
fn affirmative_rank(verdict: &Verdict) -> i8 {
match verdict {
Verdict::Modify { .. } => 2,
Verdict::Observe => 1,
Verdict::Allow => 0,
_ => -1,
}
}
fn denial_rank(verdict: &Verdict) -> i8 {
match verdict {
Verdict::Block => 2,
Verdict::StepUp { .. } => 1,
Verdict::Defer => 0,
_ => -1,
}
}
pub struct SwappableGovernor {
active: std::sync::RwLock<Arc<dyn ActionGovernor>>,
}
impl SwappableGovernor {
pub fn new(initial: Arc<dyn ActionGovernor>) -> Self {
Self {
active: std::sync::RwLock::new(initial),
}
}
pub fn swap(&self, next: Arc<dyn ActionGovernor>) -> Arc<dyn ActionGovernor> {
let mut active = self
.active
.write()
.expect("governor lock is never poisoned");
std::mem::replace(&mut active, next)
}
pub fn current(&self) -> Arc<dyn ActionGovernor> {
Arc::clone(&self.active.read().expect("governor lock is never poisoned"))
}
}
#[async_trait]
impl ActionGovernor for SwappableGovernor {
async fn decide(&self, action: &GovernedAction<'_>) -> Result<ActionDecision, LaserError> {
self.current().decide(action).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn given_a_block_decision_when_built_then_should_carry_the_reason_and_refinements() {
let decision = ActionDecision::block("wire transfers need approval")
.with_policy(PolicyRef {
pack_id: "finance".to_owned(),
pack_version: "3".to_owned(),
rule_ids: vec!["no-wires".to_owned()],
})
.with_risk_score(0.9);
assert_eq!(decision.verdict, Verdict::Block);
assert_eq!(
decision.reason.as_deref(),
Some("wire transfers need approval")
);
assert_eq!(
decision.policy.as_ref().map(|p| p.pack_id.as_str()),
Some("finance")
);
assert_eq!(decision.risk_score, Some(0.9));
}
#[test]
fn given_enforce_mode_when_verdicts_applied_then_should_map_the_full_table() {
let allow = apply(GovernorMode::Enforce, Verdict::Allow);
assert!(!allow.recorded && allow.denial.is_none() && allow.body.is_none());
let observe = apply(GovernorMode::Enforce, Verdict::Observe);
assert!(observe.recorded && observe.denial.is_none());
assert_eq!(observe.outcome, "effected");
let modify = apply(
GovernorMode::Enforce,
Verdict::Modify {
body: b"x".to_vec(),
},
);
assert_eq!(modify.body.as_deref(), Some(b"x".as_slice()));
let block = apply(GovernorMode::Enforce, Verdict::Block);
assert_eq!(block.outcome, "blocked");
assert!(matches!(block.denial, Some(LaserError::PolicyBlocked(_))));
let step_up = apply(
GovernorMode::Enforce,
Verdict::StepUp {
scope: "storage:rotate".to_owned(),
},
);
assert_eq!(step_up.outcome, "step_up");
assert!(
matches!(step_up.denial, Some(LaserError::StepUpRequired { scope }) if scope == "storage:rotate")
);
let defer = apply(GovernorMode::Enforce, Verdict::Defer);
assert_eq!(defer.outcome, "deferred");
assert!(matches!(defer.denial, Some(LaserError::PolicyDeferred(_))));
}
#[test]
fn given_observe_mode_when_verdicts_applied_then_should_record_and_always_proceed() {
for verdict in [
Verdict::Observe,
Verdict::Block,
Verdict::StepUp {
scope: "s".to_owned(),
},
Verdict::Modify {
body: b"x".to_vec(),
},
Verdict::Defer,
] {
let applied = apply(GovernorMode::Observe, verdict);
assert!(applied.recorded);
assert!(applied.denial.is_none());
assert!(applied.body.is_none(), "shadow mode never modifies");
assert_eq!(applied.outcome, "effected");
}
}
#[test]
fn given_sealed_evidence_when_re_encoded_without_its_digest_then_should_reproduce_the_digest() {
let evidence = sample_evidence().seal().expect("seals");
let mut unsealed = evidence.clone();
unsealed.receipt_digest = String::new();
let canonical = unsealed.encode().expect("encodes");
assert_eq!(
evidence.receipt_digest,
blake3::hash(&canonical).to_string()
);
assert_eq!(evidence.receipt_digest.len(), 64);
}
#[test]
fn given_evidence_when_round_tripped_through_cbor_then_should_decode_identically() {
let evidence = sample_evidence().seal().expect("seals");
let decoded =
PolicyEvidence::decode(&evidence.encode().expect("encodes")).expect("decodes");
assert_eq!(decoded, evidence);
}
#[test]
fn given_consecutive_decisions_in_one_conversation_when_sealed_then_should_chain_digests() {
let state = GovernorState::new(Arc::new(AllowAll), GovernorMode::Enforce);
let conversation = ConversationId::new();
let payload = b"body";
let action = GovernedAction {
kind: ActionKind::Send,
stream: "laser",
topic: "agent.commands",
source: Some("planner"),
target: None,
conversation: Some(conversation),
correlation: None,
operation: None,
tool: None,
on_behalf_of: None,
purpose: None,
data_classification: None,
payload,
signed: false,
counters: ActionCounters::default(),
};
let first = state
.seal_evidence(&action, &ActionDecision::observe(), "effected", None)
.expect("seals");
assert_eq!(first.previous_digest, None);
let second = state
.seal_evidence(
&action,
&ActionDecision::block("no"),
"blocked",
Some(first.receipt_digest.clone()),
)
.expect("seals");
assert_eq!(
second.previous_digest.as_deref(),
Some(first.receipt_digest.as_str())
);
}
#[test]
fn given_a_chain_when_reordered_or_edited_then_should_fail_verification() {
let first = sample_evidence().seal().expect("seals");
let mut second = sample_evidence();
second.decision_id = "01J0000000000000000000002".to_owned();
second.previous_digest = Some(first.receipt_digest.clone());
let second = second.seal().expect("seals");
assert!(verify_evidence_chain(&[first.clone(), second.clone()]));
assert!(verify_evidence_chain(&[]));
assert!(!verify_evidence_chain(&[second.clone(), first.clone()]));
assert!(!verify_evidence_chain(std::slice::from_ref(&second)));
let mut edited = second;
edited.reason = Some("edited".to_owned());
assert!(!verify_evidence_chain(&[first, edited]));
}
#[tokio::test]
async fn given_a_full_chain_table_when_a_slot_is_released_then_should_admit_without_growing() {
let state = Arc::new(GovernorState::with_retention(
Arc::new(AllowAll),
GovernorMode::Enforce,
GovernorRetention {
capacity: 1,
idle_ttl: std::time::Duration::from_secs(60),
},
));
let first = state.acquire_chain(Some(1)).await;
let waiting_state = Arc::clone(&state);
let waiting = tokio::spawn(async move { waiting_state.acquire_chain(Some(2)).await });
tokio::task::yield_now().await;
assert!(!waiting.is_finished());
assert_eq!(state.chain.lock().await.len(), 1);
drop(first);
let second = waiting.await.expect("chain admission task succeeds");
assert_eq!(state.chain.lock().await.len(), 1);
assert!(state.chain.lock().await.contains_key(&Some(2)));
drop(second);
}
#[tokio::test]
async fn given_an_expired_chain_head_when_reacquired_then_should_start_a_new_local_chain() {
let state = GovernorState::with_retention(
Arc::new(AllowAll),
GovernorMode::Enforce,
GovernorRetention {
capacity: 1,
idle_ttl: std::time::Duration::ZERO,
},
);
let first = state.acquire_chain(Some(1)).await;
*first.entry.digest.lock().await = Some("old-head".to_owned());
drop(first);
let second = state.acquire_chain(Some(1)).await;
assert_eq!(*second.entry.digest.lock().await, None);
}
struct AllowAll;
#[async_trait]
impl ActionGovernor for AllowAll {
async fn decide(&self, _: &GovernedAction<'_>) -> Result<ActionDecision, LaserError> {
Ok(ActionDecision::allow())
}
}
fn sample_evidence() -> PolicyEvidence {
PolicyEvidence {
decision_id: "01J0000000000000000000000".to_owned(),
decision: "block".to_owned(),
mode: "enforce".to_owned(),
kind: "command".to_owned(),
stream: "laser".to_owned(),
topic: "agent.commands".to_owned(),
source: Some("planner".to_owned()),
target: Some("worker".to_owned()),
conversation: None,
correlation: Some("corr-1".to_owned()),
operation: Some("chat".to_owned()),
tool: None,
on_behalf_of: Some("user-1".to_owned()),
reason: Some("blocked by policy".to_owned()),
approved_scope: None,
policy: Some(PolicyRef {
pack_id: "base".to_owned(),
pack_version: "1".to_owned(),
rule_ids: vec!["r1".to_owned()],
}),
risk_score: Some(0.5),
receipt_digest: String::new(),
previous_digest: None,
outcome: "blocked".to_owned(),
at_micros: 1_717_171_777_000_000,
}
}
struct FixedVerdict(Verdict);
#[async_trait]
impl ActionGovernor for FixedVerdict {
async fn decide(&self, _: &GovernedAction<'_>) -> Result<ActionDecision, LaserError> {
let mut decision = ActionDecision::of(self.0.clone());
decision.reason = Some("fixed".to_owned());
Ok(decision)
}
}
struct AlwaysErrors;
#[async_trait]
impl ActionGovernor for AlwaysErrors {
async fn decide(&self, _: &GovernedAction<'_>) -> Result<ActionDecision, LaserError> {
Err(LaserError::Invalid("voter unavailable".to_owned()))
}
}
fn voter(verdict: Verdict) -> Arc<dyn ActionGovernor> {
Arc::new(FixedVerdict(verdict))
}
fn probe_action() -> GovernedAction<'static> {
GovernedAction {
kind: ActionKind::Send,
stream: "laser",
topic: "agent.commands",
source: Some("planner"),
target: None,
conversation: None,
correlation: None,
operation: None,
tool: None,
on_behalf_of: None,
purpose: None,
data_classification: None,
payload: b"body",
signed: false,
counters: ActionCounters::default(),
}
}
#[tokio::test]
async fn given_every_voter_allows_under_any_policy_when_decided_then_should_allow() {
let quorum = QuorumGovernor::new(QuorumPolicy::Any)
.voter("a", voter(Verdict::Allow), false)
.voter("b", voter(Verdict::Allow), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Allow);
}
#[tokio::test]
async fn given_a_mandatory_voter_blocks_when_decided_then_should_block_regardless_of_policy() {
let quorum = QuorumGovernor::new(QuorumPolicy::Any)
.voter("safety", voter(Verdict::Block), true)
.voter("llm", voter(Verdict::Allow), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Block);
assert!(decision.reason.as_deref().unwrap().contains("safety"));
}
#[tokio::test]
async fn given_all_policy_with_one_non_mandatory_block_when_decided_then_should_deny() {
let quorum = QuorumGovernor::new(QuorumPolicy::All)
.voter("a", voter(Verdict::Allow), false)
.voter("b", voter(Verdict::Block), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Block);
}
#[tokio::test]
async fn given_any_policy_with_one_affirmative_among_blocks_when_decided_then_should_allow() {
let quorum = QuorumGovernor::new(QuorumPolicy::Any)
.voter("a", voter(Verdict::Block), false)
.voter("b", voter(Verdict::Block), false)
.voter("c", voter(Verdict::Allow), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Allow);
}
#[tokio::test]
async fn given_at_least_two_policy_when_only_one_voter_affirms_then_should_deny() {
let quorum = QuorumGovernor::new(QuorumPolicy::AtLeast(2))
.voter("a", voter(Verdict::Allow), false)
.voter("b", voter(Verdict::Block), false)
.voter("c", voter(Verdict::Block), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Block);
}
#[tokio::test]
async fn given_at_least_two_policy_when_two_voters_affirm_then_should_allow() {
let quorum = QuorumGovernor::new(QuorumPolicy::AtLeast(2))
.voter("a", voter(Verdict::Allow), false)
.voter("b", voter(Verdict::Observe), false)
.voter("c", voter(Verdict::Block), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_ne!(decision.verdict, Verdict::Block);
}
#[tokio::test]
async fn given_a_modify_voter_among_affirmatives_when_quorum_met_then_should_apply_the_body() {
let quorum = QuorumGovernor::new(QuorumPolicy::All)
.voter("a", voter(Verdict::Allow), false)
.voter(
"redactor",
voter(Verdict::Modify {
body: b"redacted".to_vec(),
}),
false,
);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(
decision.verdict,
Verdict::Modify {
body: b"redacted".to_vec()
}
);
}
#[tokio::test]
async fn given_no_quorum_with_a_step_up_and_a_defer_when_decided_then_should_prefer_step_up() {
let quorum = QuorumGovernor::new(QuorumPolicy::All)
.voter(
"reviewer",
voter(Verdict::StepUp {
scope: "storage:rotate".to_owned(),
}),
false,
)
.voter("scheduler", voter(Verdict::Defer), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert!(matches!(decision.verdict, Verdict::StepUp { .. }));
}
#[tokio::test]
async fn given_no_voters_when_decided_then_should_block() {
let quorum = QuorumGovernor::new(QuorumPolicy::All);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Block);
}
#[tokio::test]
async fn given_an_invalid_threshold_when_decided_then_should_block() {
let quorum =
QuorumGovernor::new(QuorumPolicy::AtLeast(0)).voter("a", voter(Verdict::Allow), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Block);
}
#[tokio::test]
async fn given_duplicate_voter_names_when_decided_then_should_block() {
let quorum = QuorumGovernor::new(QuorumPolicy::Any)
.voter("same", voter(Verdict::Allow), false)
.voter("same", voter(Verdict::Allow), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Block);
}
#[tokio::test]
async fn given_a_mandatory_step_up_when_another_allows_then_should_step_up() {
let quorum = QuorumGovernor::new(QuorumPolicy::Any)
.voter(
"safety",
voter(Verdict::StepUp {
scope: "storage:rotate".to_owned(),
}),
true,
)
.voter("llm", voter(Verdict::Allow), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert!(matches!(decision.verdict, Verdict::StepUp { .. }));
}
#[tokio::test]
async fn given_a_mandatory_error_when_another_allows_then_should_block() {
let quorum = QuorumGovernor::new(QuorumPolicy::Any)
.voter("safety", Arc::new(AlwaysErrors), true)
.voter("llm", voter(Verdict::Allow), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Block);
}
#[tokio::test]
async fn given_conflicting_modifications_when_quorum_met_then_should_block() {
let quorum = QuorumGovernor::new(QuorumPolicy::All)
.voter(
"one",
voter(Verdict::Modify {
body: b"one".to_vec(),
}),
false,
)
.voter(
"two",
voter(Verdict::Modify {
body: b"two".to_vec(),
}),
false,
);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Block);
}
#[tokio::test]
async fn given_an_erroring_voter_when_decided_then_should_not_veto_the_others() {
let quorum = QuorumGovernor::new(QuorumPolicy::Any)
.voter("flaky", Arc::new(AlwaysErrors), false)
.voter("steady", voter(Verdict::Allow), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Allow);
assert!(decision.reason.as_deref().unwrap().contains("error("));
}
#[tokio::test]
async fn given_an_erroring_voter_alone_when_decided_then_should_deny_for_lack_of_quorum() {
let quorum =
QuorumGovernor::new(QuorumPolicy::Any).voter("flaky", Arc::new(AlwaysErrors), false);
let decision = quorum.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Block);
}
#[tokio::test]
async fn given_a_swappable_governor_when_decided_then_should_run_the_active_policy() {
let swappable = SwappableGovernor::new(voter(Verdict::Allow));
let decision = swappable.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Allow);
}
#[tokio::test]
async fn given_a_policy_swap_when_decided_again_then_should_run_the_new_policy() {
let swappable = SwappableGovernor::new(voter(Verdict::Allow));
swappable.swap(voter(Verdict::Block));
let decision = swappable.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Block);
}
#[tokio::test]
async fn given_a_swap_when_applied_then_should_return_the_previous_policy() {
let swappable = SwappableGovernor::new(voter(Verdict::Allow));
let previous = swappable.swap(voter(Verdict::Block));
let previous_decision = previous.decide(&probe_action()).await.expect("decides");
assert_eq!(previous_decision.verdict, Verdict::Allow);
let current_decision = swappable
.current()
.decide(&probe_action())
.await
.expect("decides");
assert_eq!(current_decision.verdict, Verdict::Block);
}
#[tokio::test]
async fn given_clones_sharing_one_swappable_governor_when_one_swaps_then_all_should_observe_it()
{
let swappable = Arc::new(SwappableGovernor::new(voter(Verdict::Allow)));
let clone = Arc::clone(&swappable);
clone.swap(voter(Verdict::Block));
let decision = swappable.decide(&probe_action()).await.expect("decides");
assert_eq!(decision.verdict, Verdict::Block);
}
}