1use crate::error::LaserError;
2use crate::laser::Laser;
3use crate::provenance::AgentTopic;
4use crate::types::{ConversationId, MintUlid};
5use async_trait::async_trait;
6use laser_wire::agent::{AgentEnvelope, AgentId, RecordId, validate};
7use laser_wire::content::ContentType;
8use laser_wire::framing::{decode_named, encode_named};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13
14pub const POLICY_DECISION_OPERATION: &str = "policy_decision";
17
18#[async_trait]
29pub trait ActionGovernor: Send + Sync {
30 async fn decide(&self, action: &GovernedAction<'_>) -> Result<ActionDecision, LaserError>;
31}
32
33#[derive(Debug)]
39pub struct GovernedAction<'a> {
40 pub kind: ActionKind,
42 pub stream: &'a str,
44 pub topic: &'a str,
46 pub source: Option<&'a str>,
48 pub target: Option<&'a str>,
50 pub conversation: Option<ConversationId>,
52 pub correlation: Option<&'a str>,
54 pub operation: Option<&'a str>,
56 pub tool: Option<&'a str>,
58 pub on_behalf_of: Option<&'a str>,
60 pub purpose: Option<&'a str>,
62 pub data_classification: Option<&'a str>,
64 pub payload: &'a [u8],
66 pub signed: bool,
68 pub counters: ActionCounters,
70}
71
72#[derive(
77 Debug, Clone, Copy, PartialEq, Eq, strum::Display, strum::EnumString, strum::IntoStaticStr,
78)]
79#[strum(serialize_all = "snake_case")]
80#[non_exhaustive]
81pub enum ActionKind {
82 Send,
84 Publish,
86 Request,
88 Command,
90 Response,
92 Event,
94 Status,
96 Error,
98 MemoryWrite,
100}
101
102impl ActionKind {
103 pub fn as_str(self) -> &'static str {
105 self.into()
106 }
107}
108
109#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
112pub struct ActionCounters {
113 pub sends: u64,
115 pub requests: u64,
117 pub bytes_sent: u64,
119}
120
121#[derive(Debug, Clone, PartialEq)]
125pub struct ActionDecision {
126 pub verdict: Verdict,
128 pub reason: Option<String>,
130 pub policy: Option<PolicyRef>,
132 pub risk_score: Option<f64>,
134}
135
136impl ActionDecision {
137 pub fn allow() -> Self {
139 Self::of(Verdict::Allow)
140 }
141
142 pub fn observe() -> Self {
144 Self::of(Verdict::Observe)
145 }
146
147 pub fn block(reason: impl Into<String>) -> Self {
149 let mut decision = Self::of(Verdict::Block);
150 decision.reason = Some(reason.into());
151 decision
152 }
153
154 pub fn step_up(scope: impl Into<String>) -> Self {
158 Self::of(Verdict::StepUp {
159 scope: scope.into(),
160 })
161 }
162
163 pub fn modify(body: impl Into<Vec<u8>>) -> Self {
166 Self::of(Verdict::Modify { body: body.into() })
167 }
168
169 pub fn defer(reason: impl Into<String>) -> Self {
171 let mut decision = Self::of(Verdict::Defer);
172 decision.reason = Some(reason.into());
173 decision
174 }
175
176 #[must_use]
178 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
179 self.reason = Some(reason.into());
180 self
181 }
182
183 #[must_use]
185 pub fn with_policy(mut self, policy: PolicyRef) -> Self {
186 self.policy = Some(policy);
187 self
188 }
189
190 #[must_use]
192 pub fn with_risk_score(mut self, risk_score: f64) -> Self {
193 self.risk_score = Some(risk_score);
194 self
195 }
196
197 fn of(verdict: Verdict) -> Self {
198 Self {
199 verdict,
200 reason: None,
201 policy: None,
202 risk_score: None,
203 }
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, strum::Display, strum::IntoStaticStr)]
211#[strum(serialize_all = "snake_case")]
212pub enum Verdict {
213 Allow,
215 Observe,
217 Block,
219 StepUp { scope: String },
221 Modify { body: Vec<u8> },
223 Defer,
225}
226
227impl Verdict {
228 pub fn as_str(&self) -> &'static str {
230 self.into()
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238pub struct PolicyRef {
239 pub pack_id: String,
241 pub pack_version: String,
243 pub rule_ids: Vec<String>,
245}
246
247#[derive(
251 Debug,
252 Clone,
253 Copy,
254 Default,
255 PartialEq,
256 Eq,
257 strum::Display,
258 strum::EnumString,
259 strum::IntoStaticStr,
260)]
261#[strum(serialize_all = "snake_case")]
262pub enum GovernorMode {
263 Observe,
267 #[default]
271 Enforce,
272}
273
274impl GovernorMode {
275 pub fn as_str(self) -> &'static str {
277 self.into()
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
290pub struct PolicyEvidence {
291 pub decision_id: String,
293 pub decision: String,
295 pub mode: String,
297 pub kind: String,
299 pub stream: String,
301 pub topic: String,
303 #[serde(skip_serializing_if = "Option::is_none", default)]
304 pub source: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none", default)]
306 pub target: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none", default)]
308 pub conversation: Option<String>,
309 #[serde(skip_serializing_if = "Option::is_none", default)]
310 pub correlation: Option<String>,
311 #[serde(skip_serializing_if = "Option::is_none", default)]
312 pub operation: Option<String>,
313 #[serde(skip_serializing_if = "Option::is_none", default)]
314 pub tool: Option<String>,
315 #[serde(skip_serializing_if = "Option::is_none", default)]
316 pub on_behalf_of: Option<String>,
317 #[serde(skip_serializing_if = "Option::is_none", default)]
319 pub reason: Option<String>,
320 #[serde(skip_serializing_if = "Option::is_none", default)]
322 pub approved_scope: Option<String>,
323 #[serde(skip_serializing_if = "Option::is_none", default)]
325 pub policy: Option<PolicyRef>,
326 #[serde(skip_serializing_if = "Option::is_none", default)]
328 pub risk_score: Option<f64>,
329 pub receipt_digest: String,
331 #[serde(skip_serializing_if = "Option::is_none", default)]
333 pub previous_digest: Option<String>,
334 pub outcome: String,
338 pub at_micros: u64,
340}
341
342impl PolicyEvidence {
343 pub fn encode(&self) -> Result<Vec<u8>, LaserError> {
345 Ok(encode_named(self)?)
346 }
347
348 pub fn decode(payload: &[u8]) -> Result<Self, LaserError> {
350 Ok(decode_named(payload)?)
351 }
352
353 fn seal(mut self) -> Result<Self, LaserError> {
356 self.receipt_digest = String::new();
357 let canonical = self.encode()?;
358 self.receipt_digest = blake3::hash(&canonical).to_string();
359 Ok(self)
360 }
361}
362
363pub fn verify_evidence_chain(evidence: &[PolicyEvidence]) -> bool {
368 let mut previous: Option<&str> = None;
369 for item in evidence {
370 if item.previous_digest.as_deref() != previous {
371 return false;
372 }
373 match item.clone().seal() {
374 Ok(sealed) if sealed.receipt_digest == item.receipt_digest => {}
375 _ => return false,
376 }
377 previous = Some(item.receipt_digest.as_str());
378 }
379 true
380}
381
382impl Laser {
383 #[must_use]
391 pub fn with_governor(&self, governor: Arc<dyn ActionGovernor>, mode: GovernorMode) -> Self {
392 self.with_governor_retention(governor, mode, GovernorRetention::default())
393 }
394
395 #[must_use]
399 pub fn with_governor_retention(
400 &self,
401 governor: Arc<dyn ActionGovernor>,
402 mode: GovernorMode,
403 retention: GovernorRetention,
404 ) -> Self {
405 let mut governed = self.clone();
406 governed.governor = Some(Arc::new(GovernorState::with_retention(
407 governor, mode, retention,
408 )));
409 governed
410 }
411
412 pub(crate) async fn govern(
417 &self,
418 mut action: GovernedAction<'_>,
419 ) -> Result<Option<Vec<u8>>, LaserError> {
420 let Some(state) = self.governor.clone() else {
421 return Ok(None);
422 };
423 action.counters = state.snapshot();
424 let decision = state.governor.decide(&action).await?;
425 match action.kind {
426 ActionKind::Request => state.requests.fetch_add(1, Ordering::Relaxed),
427 _ => state.sends.fetch_add(1, Ordering::Relaxed),
428 };
429 let applied = apply(state.mode, decision.verdict.clone());
430 if applied.recorded {
431 let emitted = state
432 .emit_chained_evidence(self, &action, &decision, applied.outcome)
433 .await;
434 match state.mode {
435 GovernorMode::Observe => {
437 if let Err(error) = emitted {
438 tracing::warn!(%error, "policy evidence write failed (observe mode)");
439 }
440 }
441 GovernorMode::Enforce => match (&applied.denial, emitted) {
442 (None, Err(error)) => return Err(error),
445 (Some(_), Err(error)) => {
447 tracing::warn!(%error, "policy evidence write failed (denial stands)");
448 }
449 (_, Ok(())) => {}
450 },
451 }
452 }
453 if let Some(denial) = applied.denial {
454 return Err(denial);
455 }
456 let sent = applied.body.as_ref().map_or(action.payload.len(), Vec::len);
458 state.bytes_sent.fetch_add(sent as u64, Ordering::Relaxed);
459 Ok(applied.body)
460 }
461
462 async fn emit_evidence(
467 &self,
468 action: &GovernedAction<'_>,
469 evidence: PolicyEvidence,
470 ) -> Result<(), LaserError> {
471 let source: AgentId = action
472 .source
473 .and_then(|source| source.parse().ok())
474 .unwrap_or_else(|| "governor".parse().expect("static agent id is valid"));
475 let conversation = action.conversation.unwrap_or_default();
476 let envelope = AgentEnvelope::event(
477 RecordId::mint(),
478 conversation.into(),
479 source,
480 evidence.encode()?,
481 )
482 .with_operation(POLICY_DECISION_OPERATION);
483 validate(&envelope)?;
484 let payload = encode_named(&envelope)?;
485 let headers = crate::agent::agdx_headers(&envelope, ContentType::Cbor, false)?;
486 let message = iggy::prelude::IggyMessage::builder()
487 .payload(bytes::Bytes::from(payload))
488 .user_headers(headers)
489 .build()?;
490 let partition_key = conversation.to_string();
491 self.send_batch_on(
492 action.stream,
493 &AgentTopic::Audit.topic_string(),
494 vec![message],
495 Some(&partition_key),
496 )
497 .await
498 .map(|_| ())
499 }
500}
501
502pub(crate) struct GovernorState {
505 governor: Arc<dyn ActionGovernor>,
506 mode: GovernorMode,
507 sends: AtomicU64,
508 requests: AtomicU64,
509 bytes_sent: AtomicU64,
510 chain: tokio::sync::Mutex<HashMap<Option<u128>, Arc<ChainEntry>>>,
511 chain_released: Arc<tokio::sync::Notify>,
512 retention: GovernorRetention,
513}
514
515struct ChainEntry {
516 digest: tokio::sync::Mutex<Option<String>>,
517 touched_micros: AtomicU64,
518}
519
520struct ChainLease {
521 entry: Arc<ChainEntry>,
522 released: Arc<tokio::sync::Notify>,
523}
524
525impl Drop for ChainLease {
526 fn drop(&mut self) {
527 self.entry
528 .touched_micros
529 .store(now_micros(), Ordering::Release);
530 self.released.notify_one();
531 }
532}
533
534#[derive(Clone, Copy, Debug)]
536pub struct GovernorRetention {
537 pub capacity: usize,
539 pub idle_ttl: std::time::Duration,
541}
542
543impl Default for GovernorRetention {
544 fn default() -> Self {
545 Self {
546 capacity: 4_096,
547 idle_ttl: std::time::Duration::from_secs(3_600),
548 }
549 }
550}
551
552impl GovernorState {
553 pub(crate) fn new(governor: Arc<dyn ActionGovernor>, mode: GovernorMode) -> Self {
554 Self::with_retention(governor, mode, GovernorRetention::default())
555 }
556
557 pub(crate) fn with_retention(
558 governor: Arc<dyn ActionGovernor>,
559 mode: GovernorMode,
560 mut retention: GovernorRetention,
561 ) -> Self {
562 retention.capacity = retention.capacity.max(1);
563 Self {
564 governor,
565 mode,
566 sends: AtomicU64::new(0),
567 requests: AtomicU64::new(0),
568 bytes_sent: AtomicU64::new(0),
569 chain: tokio::sync::Mutex::new(HashMap::new()),
570 chain_released: Arc::new(tokio::sync::Notify::new()),
571 retention,
572 }
573 }
574
575 fn prune_chain(&self, chain: &mut HashMap<Option<u128>, Arc<ChainEntry>>, now_micros: u64) {
576 let ttl_micros = u64::try_from(self.retention.idle_ttl.as_micros()).unwrap_or(u64::MAX);
577 chain.retain(|_, entry| {
578 Arc::strong_count(entry) > 1
579 || now_micros.saturating_sub(entry.touched_micros.load(Ordering::Acquire))
580 < ttl_micros
581 });
582 }
583
584 async fn acquire_chain(&self, chain_key: Option<u128>) -> ChainLease {
585 loop {
586 let released = self.chain_released.notified();
587 {
588 let now = now_micros();
589 let mut chain = self.chain.lock().await;
590 self.prune_chain(&mut chain, now);
591 if let Some(entry) = chain.get(&chain_key) {
592 return ChainLease {
593 entry: Arc::clone(entry),
594 released: Arc::clone(&self.chain_released),
595 };
596 }
597 while chain.len() >= self.retention.capacity {
598 let oldest = chain
599 .iter()
600 .filter(|(_, entry)| Arc::strong_count(entry) == 1)
601 .min_by_key(|(_, entry)| entry.touched_micros.load(Ordering::Acquire))
602 .map(|(key, _)| *key);
603 let Some(oldest) = oldest else {
604 break;
605 };
606 chain.remove(&oldest);
607 }
608 if chain.len() < self.retention.capacity {
609 let entry = Arc::new(ChainEntry {
610 digest: tokio::sync::Mutex::new(None),
611 touched_micros: AtomicU64::new(now),
612 });
613 chain.insert(chain_key, Arc::clone(&entry));
614 return ChainLease {
615 entry,
616 released: Arc::clone(&self.chain_released),
617 };
618 }
619 }
620 released.await;
621 }
622 }
623
624 fn snapshot(&self) -> ActionCounters {
625 ActionCounters {
626 sends: self.sends.load(Ordering::Relaxed),
627 requests: self.requests.load(Ordering::Relaxed),
628 bytes_sent: self.bytes_sent.load(Ordering::Relaxed),
629 }
630 }
631
632 async fn emit_chained_evidence(
635 &self,
636 laser: &Laser,
637 action: &GovernedAction<'_>,
638 decision: &ActionDecision,
639 outcome: &'static str,
640 ) -> Result<(), LaserError> {
641 let chain_key = action.conversation.map(|id| id.as_u128());
642 let slot = self.acquire_chain(chain_key).await;
643 let mut previous = slot.entry.digest.lock().await;
644 let evidence = self.seal_evidence(action, decision, outcome, previous.clone())?;
645 let digest = evidence.receipt_digest.clone();
646 laser.emit_evidence(action, evidence).await?;
647 *previous = Some(digest);
648 Ok(())
649 }
650
651 fn seal_evidence(
653 &self,
654 action: &GovernedAction<'_>,
655 decision: &ActionDecision,
656 outcome: &'static str,
657 previous_digest: Option<String>,
658 ) -> Result<PolicyEvidence, LaserError> {
659 let approved_scope = match &decision.verdict {
660 Verdict::StepUp { scope } => Some(scope.clone()),
661 _ => None,
662 };
663 PolicyEvidence {
664 decision_id: ulid::Ulid::generate().to_string(),
665 decision: decision.verdict.as_str().to_owned(),
666 mode: self.mode.as_str().to_owned(),
667 kind: action.kind.as_str().to_owned(),
668 stream: action.stream.to_owned(),
669 topic: action.topic.to_owned(),
670 source: action.source.map(str::to_owned),
671 target: action.target.map(str::to_owned),
672 conversation: action.conversation.map(|id| id.to_string()),
673 correlation: action.correlation.map(str::to_owned),
674 operation: action.operation.map(str::to_owned),
675 tool: action.tool.map(str::to_owned),
676 on_behalf_of: action.on_behalf_of.map(str::to_owned),
677 reason: decision.reason.clone(),
678 approved_scope,
679 policy: decision.policy.clone(),
680 risk_score: decision.risk_score,
681 receipt_digest: String::new(),
682 previous_digest,
683 outcome: outcome.to_owned(),
684 at_micros: now_micros(),
685 }
686 .seal()
687 }
688}
689
690struct AppliedVerdict {
693 recorded: bool,
694 outcome: &'static str,
695 body: Option<Vec<u8>>,
696 denial: Option<LaserError>,
697}
698
699fn apply(mode: GovernorMode, verdict: Verdict) -> AppliedVerdict {
702 let enforced = matches!(mode, GovernorMode::Enforce);
703 match verdict {
704 Verdict::Allow => AppliedVerdict {
705 recorded: false,
706 outcome: "effected",
707 body: None,
708 denial: None,
709 },
710 Verdict::Observe => AppliedVerdict {
711 recorded: true,
712 outcome: "effected",
713 body: None,
714 denial: None,
715 },
716 Verdict::Modify { body } => AppliedVerdict {
717 recorded: true,
718 outcome: "effected",
719 body: enforced.then_some(body),
720 denial: None,
721 },
722 Verdict::Block => AppliedVerdict {
723 recorded: true,
724 outcome: if enforced { "blocked" } else { "effected" },
725 body: None,
726 denial: enforced
727 .then(|| LaserError::PolicyBlocked("the governor blocked this action".to_owned())),
728 },
729 Verdict::StepUp { scope } => AppliedVerdict {
730 recorded: true,
731 outcome: if enforced { "step_up" } else { "effected" },
732 body: None,
733 denial: enforced.then(|| LaserError::StepUpRequired { scope }),
734 },
735 Verdict::Defer => AppliedVerdict {
736 recorded: true,
737 outcome: if enforced { "deferred" } else { "effected" },
738 body: None,
739 denial: enforced.then(|| {
740 LaserError::PolicyDeferred("the governor deferred this action".to_owned())
741 }),
742 },
743 }
744}
745
746fn now_micros() -> u64 {
747 std::time::SystemTime::now()
748 .duration_since(std::time::UNIX_EPOCH)
749 .map(|elapsed| u64::try_from(elapsed.as_micros()).unwrap_or(u64::MAX))
750 .unwrap_or(0)
751}
752
753#[derive(Debug, Clone, Copy, PartialEq, Eq)]
758pub enum QuorumPolicy {
759 All,
761 Any,
763 AtLeast(usize),
765}
766
767impl QuorumPolicy {
768 fn met(self, affirmative: usize, total: usize) -> bool {
769 match self {
770 QuorumPolicy::All => affirmative == total,
771 QuorumPolicy::Any => affirmative >= 1,
772 QuorumPolicy::AtLeast(n) => affirmative >= n,
773 }
774 }
775}
776
777#[derive(Clone)]
780struct Voter {
781 name: String,
782 governor: Arc<dyn ActionGovernor>,
783 mandatory: bool,
784}
785
786#[derive(Clone)]
804pub struct QuorumGovernor {
805 voters: Vec<Voter>,
806 policy: QuorumPolicy,
807}
808
809impl QuorumGovernor {
810 pub fn new(policy: QuorumPolicy) -> Self {
813 Self {
814 voters: Vec::new(),
815 policy,
816 }
817 }
818
819 #[must_use]
822 pub fn voter(
823 mut self,
824 name: impl Into<String>,
825 governor: Arc<dyn ActionGovernor>,
826 mandatory: bool,
827 ) -> Self {
828 self.voters.push(Voter {
829 name: name.into(),
830 governor,
831 mandatory,
832 });
833 self
834 }
835}
836
837#[async_trait]
838impl ActionGovernor for QuorumGovernor {
839 async fn decide(&self, action: &GovernedAction<'_>) -> Result<ActionDecision, LaserError> {
840 if self.voters.is_empty() {
841 return Ok(ActionDecision::block(
842 "quorum governor has no configured voters",
843 ));
844 }
845 if let QuorumPolicy::AtLeast(required) = self.policy
846 && (required == 0 || required > self.voters.len())
847 {
848 return Ok(ActionDecision::block(format!(
849 "quorum threshold {required} is invalid for {} voters",
850 self.voters.len()
851 )));
852 }
853 let mut names = std::collections::HashSet::with_capacity(self.voters.len());
854 if let Some(duplicate) = self
855 .voters
856 .iter()
857 .find(|voter| !names.insert(voter.name.as_str()))
858 {
859 return Ok(ActionDecision::block(format!(
860 "quorum voter '{}' is configured more than once",
861 duplicate.name
862 )));
863 }
864 let votes = futures::future::join_all(
865 self.voters
866 .iter()
867 .map(|voter| async move { (voter, voter.governor.decide(action).await) }),
868 )
869 .await;
870
871 let mut ballot = Vec::with_capacity(votes.len());
872 let mut affirmative = 0usize;
873 let mut mandatory_denial: Option<ActionDecision> = None;
874 let mut mandatory_error: Option<&str> = None;
875 let mut best_affirmative: Option<ActionDecision> = None;
876 let mut best_denial: Option<ActionDecision> = None;
877 let mut replacement: Option<&[u8]> = None;
878 let mut conflicting_replacements = false;
879 for (voter, vote) in &votes {
880 let decision = vote.as_ref().map_err(|error| error.to_string());
881 ballot.push(format!(
882 "{}={}",
883 voter.name,
884 decision
885 .as_ref()
886 .map(|decision| decision.verdict.as_str().to_owned())
887 .unwrap_or_else(|error| format!("error({error})"))
888 ));
889 let Ok(decision) = vote else {
890 if voter.mandatory {
891 mandatory_error = mandatory_error.or(Some(voter.name.as_str()));
892 }
893 continue;
894 };
895 if voter.mandatory && !is_affirmative(&decision.verdict) {
896 mandatory_denial = mandatory_denial.or(Some(decision.clone()));
897 }
898 if is_affirmative(&decision.verdict) {
899 affirmative += 1;
900 if let Verdict::Modify { body } = &decision.verdict {
901 match replacement {
902 None => replacement = Some(body),
903 Some(existing) if existing != body => conflicting_replacements = true,
904 Some(_) => {}
905 }
906 }
907 if affirmative_rank(&decision.verdict)
908 > best_affirmative
909 .as_ref()
910 .map_or(-1, |current| affirmative_rank(¤t.verdict))
911 {
912 best_affirmative = Some(decision.clone());
913 }
914 } else if denial_rank(&decision.verdict)
915 > best_denial
916 .as_ref()
917 .map_or(-1, |current| denial_rank(¤t.verdict))
918 {
919 best_denial = Some(decision.clone());
920 }
921 }
922
923 let reason = format!("quorum({:?}): {}", self.policy, ballot.join(", "));
925
926 if let Some(voter) = mandatory_error {
927 return Ok(annotate(
928 ActionDecision::block(format!("mandatory voter '{voter}' failed")),
929 &reason,
930 ));
931 }
932 if let Some(decision) = mandatory_denial {
933 return Ok(annotate(decision, &reason));
934 }
935 if conflicting_replacements {
936 return Ok(annotate(
937 ActionDecision::block("quorum voters proposed conflicting body replacements"),
938 &reason,
939 ));
940 }
941 if self.policy.met(affirmative, self.voters.len()) {
942 return Ok(annotate(
943 best_affirmative.unwrap_or_else(ActionDecision::allow),
944 &reason,
945 ));
946 }
947 Ok(annotate(
948 best_denial
949 .unwrap_or_else(|| ActionDecision::block("no voter reached the required quorum")),
950 &reason,
951 ))
952 }
953}
954
955fn annotate(mut decision: ActionDecision, ballot: &str) -> ActionDecision {
956 decision.reason = Some(match decision.reason.take() {
957 Some(detail) => format!("{detail}; {ballot}"),
958 None => ballot.to_owned(),
959 });
960 decision
961}
962
963fn is_affirmative(verdict: &Verdict) -> bool {
964 matches!(
965 verdict,
966 Verdict::Allow | Verdict::Observe | Verdict::Modify { .. }
967 )
968}
969
970fn affirmative_rank(verdict: &Verdict) -> i8 {
973 match verdict {
974 Verdict::Modify { .. } => 2,
975 Verdict::Observe => 1,
976 Verdict::Allow => 0,
977 _ => -1,
978 }
979}
980
981fn denial_rank(verdict: &Verdict) -> i8 {
985 match verdict {
986 Verdict::Block => 2,
987 Verdict::StepUp { .. } => 1,
988 Verdict::Defer => 0,
989 _ => -1,
990 }
991}
992
993pub struct SwappableGovernor {
1003 active: std::sync::RwLock<Arc<dyn ActionGovernor>>,
1004}
1005
1006impl SwappableGovernor {
1007 pub fn new(initial: Arc<dyn ActionGovernor>) -> Self {
1009 Self {
1010 active: std::sync::RwLock::new(initial),
1011 }
1012 }
1013
1014 pub fn swap(&self, next: Arc<dyn ActionGovernor>) -> Arc<dyn ActionGovernor> {
1018 let mut active = self
1019 .active
1020 .write()
1021 .expect("governor lock is never poisoned");
1022 std::mem::replace(&mut active, next)
1023 }
1024
1025 pub fn current(&self) -> Arc<dyn ActionGovernor> {
1027 Arc::clone(&self.active.read().expect("governor lock is never poisoned"))
1028 }
1029}
1030
1031#[async_trait]
1032impl ActionGovernor for SwappableGovernor {
1033 async fn decide(&self, action: &GovernedAction<'_>) -> Result<ActionDecision, LaserError> {
1034 self.current().decide(action).await
1035 }
1036}
1037
1038#[cfg(test)]
1039mod tests {
1040 use super::*;
1041
1042 #[test]
1043 fn given_a_block_decision_when_built_then_should_carry_the_reason_and_refinements() {
1044 let decision = ActionDecision::block("wire transfers need approval")
1045 .with_policy(PolicyRef {
1046 pack_id: "finance".to_owned(),
1047 pack_version: "3".to_owned(),
1048 rule_ids: vec!["no-wires".to_owned()],
1049 })
1050 .with_risk_score(0.9);
1051 assert_eq!(decision.verdict, Verdict::Block);
1052 assert_eq!(
1053 decision.reason.as_deref(),
1054 Some("wire transfers need approval")
1055 );
1056 assert_eq!(
1057 decision.policy.as_ref().map(|p| p.pack_id.as_str()),
1058 Some("finance")
1059 );
1060 assert_eq!(decision.risk_score, Some(0.9));
1061 }
1062
1063 #[test]
1064 fn given_enforce_mode_when_verdicts_applied_then_should_map_the_full_table() {
1065 let allow = apply(GovernorMode::Enforce, Verdict::Allow);
1066 assert!(!allow.recorded && allow.denial.is_none() && allow.body.is_none());
1067
1068 let observe = apply(GovernorMode::Enforce, Verdict::Observe);
1069 assert!(observe.recorded && observe.denial.is_none());
1070 assert_eq!(observe.outcome, "effected");
1071
1072 let modify = apply(
1073 GovernorMode::Enforce,
1074 Verdict::Modify {
1075 body: b"x".to_vec(),
1076 },
1077 );
1078 assert_eq!(modify.body.as_deref(), Some(b"x".as_slice()));
1079
1080 let block = apply(GovernorMode::Enforce, Verdict::Block);
1081 assert_eq!(block.outcome, "blocked");
1082 assert!(matches!(block.denial, Some(LaserError::PolicyBlocked(_))));
1083
1084 let step_up = apply(
1085 GovernorMode::Enforce,
1086 Verdict::StepUp {
1087 scope: "storage:rotate".to_owned(),
1088 },
1089 );
1090 assert_eq!(step_up.outcome, "step_up");
1091 assert!(
1092 matches!(step_up.denial, Some(LaserError::StepUpRequired { scope }) if scope == "storage:rotate")
1093 );
1094
1095 let defer = apply(GovernorMode::Enforce, Verdict::Defer);
1096 assert_eq!(defer.outcome, "deferred");
1097 assert!(matches!(defer.denial, Some(LaserError::PolicyDeferred(_))));
1098 }
1099
1100 #[test]
1101 fn given_observe_mode_when_verdicts_applied_then_should_record_and_always_proceed() {
1102 for verdict in [
1103 Verdict::Observe,
1104 Verdict::Block,
1105 Verdict::StepUp {
1106 scope: "s".to_owned(),
1107 },
1108 Verdict::Modify {
1109 body: b"x".to_vec(),
1110 },
1111 Verdict::Defer,
1112 ] {
1113 let applied = apply(GovernorMode::Observe, verdict);
1114 assert!(applied.recorded);
1115 assert!(applied.denial.is_none());
1116 assert!(applied.body.is_none(), "shadow mode never modifies");
1117 assert_eq!(applied.outcome, "effected");
1118 }
1119 }
1120
1121 #[test]
1122 fn given_sealed_evidence_when_re_encoded_without_its_digest_then_should_reproduce_the_digest() {
1123 let evidence = sample_evidence().seal().expect("seals");
1124 let mut unsealed = evidence.clone();
1125 unsealed.receipt_digest = String::new();
1126 let canonical = unsealed.encode().expect("encodes");
1127 assert_eq!(
1128 evidence.receipt_digest,
1129 blake3::hash(&canonical).to_string()
1130 );
1131 assert_eq!(evidence.receipt_digest.len(), 64);
1132 }
1133
1134 #[test]
1135 fn given_evidence_when_round_tripped_through_cbor_then_should_decode_identically() {
1136 let evidence = sample_evidence().seal().expect("seals");
1137 let decoded =
1138 PolicyEvidence::decode(&evidence.encode().expect("encodes")).expect("decodes");
1139 assert_eq!(decoded, evidence);
1140 }
1141
1142 #[test]
1143 fn given_consecutive_decisions_in_one_conversation_when_sealed_then_should_chain_digests() {
1144 let state = GovernorState::new(Arc::new(AllowAll), GovernorMode::Enforce);
1145 let conversation = ConversationId::new();
1146 let payload = b"body";
1147 let action = GovernedAction {
1148 kind: ActionKind::Send,
1149 stream: "laser",
1150 topic: "agent.sessions",
1151 source: Some("planner"),
1152 target: None,
1153 conversation: Some(conversation),
1154 correlation: None,
1155 operation: None,
1156 tool: None,
1157 on_behalf_of: None,
1158 purpose: None,
1159 data_classification: None,
1160 payload,
1161 signed: false,
1162 counters: ActionCounters::default(),
1163 };
1164 let first = state
1165 .seal_evidence(&action, &ActionDecision::observe(), "effected", None)
1166 .expect("seals");
1167 assert_eq!(first.previous_digest, None);
1168 let second = state
1169 .seal_evidence(
1170 &action,
1171 &ActionDecision::block("no"),
1172 "blocked",
1173 Some(first.receipt_digest.clone()),
1174 )
1175 .expect("seals");
1176 assert_eq!(
1177 second.previous_digest.as_deref(),
1178 Some(first.receipt_digest.as_str())
1179 );
1180 }
1181
1182 #[test]
1183 fn given_a_chain_when_reordered_or_edited_then_should_fail_verification() {
1184 let first = sample_evidence().seal().expect("seals");
1185 let mut second = sample_evidence();
1186 second.decision_id = "01J0000000000000000000002".to_owned();
1187 second.previous_digest = Some(first.receipt_digest.clone());
1188 let second = second.seal().expect("seals");
1189 assert!(verify_evidence_chain(&[first.clone(), second.clone()]));
1190 assert!(verify_evidence_chain(&[]));
1191 assert!(!verify_evidence_chain(&[second.clone(), first.clone()]));
1192 assert!(!verify_evidence_chain(std::slice::from_ref(&second)));
1193 let mut edited = second;
1194 edited.reason = Some("edited".to_owned());
1195 assert!(!verify_evidence_chain(&[first, edited]));
1196 }
1197
1198 #[tokio::test]
1199 async fn given_a_full_chain_table_when_a_slot_is_released_then_should_admit_without_growing() {
1200 let state = Arc::new(GovernorState::with_retention(
1201 Arc::new(AllowAll),
1202 GovernorMode::Enforce,
1203 GovernorRetention {
1204 capacity: 1,
1205 idle_ttl: std::time::Duration::from_secs(60),
1206 },
1207 ));
1208 let first = state.acquire_chain(Some(1)).await;
1209 let waiting_state = Arc::clone(&state);
1210 let waiting = tokio::spawn(async move { waiting_state.acquire_chain(Some(2)).await });
1211 tokio::task::yield_now().await;
1212
1213 assert!(!waiting.is_finished());
1214 assert_eq!(state.chain.lock().await.len(), 1);
1215
1216 drop(first);
1217 let second = waiting.await.expect("chain admission task succeeds");
1218 assert_eq!(state.chain.lock().await.len(), 1);
1219 assert!(state.chain.lock().await.contains_key(&Some(2)));
1220 drop(second);
1221 }
1222
1223 #[tokio::test]
1224 async fn given_an_expired_chain_head_when_reacquired_then_should_start_a_new_local_chain() {
1225 let state = GovernorState::with_retention(
1226 Arc::new(AllowAll),
1227 GovernorMode::Enforce,
1228 GovernorRetention {
1229 capacity: 1,
1230 idle_ttl: std::time::Duration::ZERO,
1231 },
1232 );
1233 let first = state.acquire_chain(Some(1)).await;
1234 *first.entry.digest.lock().await = Some("old-head".to_owned());
1235 drop(first);
1236
1237 let second = state.acquire_chain(Some(1)).await;
1238 assert_eq!(*second.entry.digest.lock().await, None);
1239 }
1240
1241 struct AllowAll;
1242
1243 #[async_trait]
1244 impl ActionGovernor for AllowAll {
1245 async fn decide(&self, _: &GovernedAction<'_>) -> Result<ActionDecision, LaserError> {
1246 Ok(ActionDecision::allow())
1247 }
1248 }
1249
1250 fn sample_evidence() -> PolicyEvidence {
1251 PolicyEvidence {
1252 decision_id: "01J0000000000000000000000".to_owned(),
1253 decision: "block".to_owned(),
1254 mode: "enforce".to_owned(),
1255 kind: "command".to_owned(),
1256 stream: "laser".to_owned(),
1257 topic: "agent.sessions".to_owned(),
1258 source: Some("planner".to_owned()),
1259 target: Some("worker".to_owned()),
1260 conversation: None,
1261 correlation: Some("corr-1".to_owned()),
1262 operation: Some("chat".to_owned()),
1263 tool: None,
1264 on_behalf_of: Some("user-1".to_owned()),
1265 reason: Some("blocked by policy".to_owned()),
1266 approved_scope: None,
1267 policy: Some(PolicyRef {
1268 pack_id: "base".to_owned(),
1269 pack_version: "1".to_owned(),
1270 rule_ids: vec!["r1".to_owned()],
1271 }),
1272 risk_score: Some(0.5),
1273 receipt_digest: String::new(),
1274 previous_digest: None,
1275 outcome: "blocked".to_owned(),
1276 at_micros: 1_717_171_777_000_000,
1277 }
1278 }
1279
1280 struct FixedVerdict(Verdict);
1281
1282 #[async_trait]
1283 impl ActionGovernor for FixedVerdict {
1284 async fn decide(&self, _: &GovernedAction<'_>) -> Result<ActionDecision, LaserError> {
1285 let mut decision = ActionDecision::of(self.0.clone());
1286 decision.reason = Some("fixed".to_owned());
1287 Ok(decision)
1288 }
1289 }
1290
1291 struct AlwaysErrors;
1292
1293 #[async_trait]
1294 impl ActionGovernor for AlwaysErrors {
1295 async fn decide(&self, _: &GovernedAction<'_>) -> Result<ActionDecision, LaserError> {
1296 Err(LaserError::Invalid("voter unavailable".to_owned()))
1297 }
1298 }
1299
1300 fn voter(verdict: Verdict) -> Arc<dyn ActionGovernor> {
1301 Arc::new(FixedVerdict(verdict))
1302 }
1303
1304 fn probe_action() -> GovernedAction<'static> {
1305 GovernedAction {
1306 kind: ActionKind::Send,
1307 stream: "laser",
1308 topic: "agent.sessions",
1309 source: Some("planner"),
1310 target: None,
1311 conversation: None,
1312 correlation: None,
1313 operation: None,
1314 tool: None,
1315 on_behalf_of: None,
1316 purpose: None,
1317 data_classification: None,
1318 payload: b"body",
1319 signed: false,
1320 counters: ActionCounters::default(),
1321 }
1322 }
1323
1324 #[tokio::test]
1325 async fn given_every_voter_allows_under_any_policy_when_decided_then_should_allow() {
1326 let quorum = QuorumGovernor::new(QuorumPolicy::Any)
1327 .voter("a", voter(Verdict::Allow), false)
1328 .voter("b", voter(Verdict::Allow), false);
1329 let decision = quorum.decide(&probe_action()).await.expect("decides");
1330 assert_eq!(decision.verdict, Verdict::Allow);
1331 }
1332
1333 #[tokio::test]
1334 async fn given_a_mandatory_voter_blocks_when_decided_then_should_block_regardless_of_policy() {
1335 let quorum = QuorumGovernor::new(QuorumPolicy::Any)
1336 .voter("safety", voter(Verdict::Block), true)
1337 .voter("llm", voter(Verdict::Allow), false);
1338 let decision = quorum.decide(&probe_action()).await.expect("decides");
1339 assert_eq!(decision.verdict, Verdict::Block);
1340 assert!(decision.reason.as_deref().unwrap().contains("safety"));
1341 }
1342
1343 #[tokio::test]
1344 async fn given_all_policy_with_one_non_mandatory_block_when_decided_then_should_deny() {
1345 let quorum = QuorumGovernor::new(QuorumPolicy::All)
1346 .voter("a", voter(Verdict::Allow), false)
1347 .voter("b", voter(Verdict::Block), false);
1348 let decision = quorum.decide(&probe_action()).await.expect("decides");
1349 assert_eq!(decision.verdict, Verdict::Block);
1350 }
1351
1352 #[tokio::test]
1353 async fn given_any_policy_with_one_affirmative_among_blocks_when_decided_then_should_allow() {
1354 let quorum = QuorumGovernor::new(QuorumPolicy::Any)
1355 .voter("a", voter(Verdict::Block), false)
1356 .voter("b", voter(Verdict::Block), false)
1357 .voter("c", voter(Verdict::Allow), false);
1358 let decision = quorum.decide(&probe_action()).await.expect("decides");
1359 assert_eq!(decision.verdict, Verdict::Allow);
1360 }
1361
1362 #[tokio::test]
1363 async fn given_at_least_two_policy_when_only_one_voter_affirms_then_should_deny() {
1364 let quorum = QuorumGovernor::new(QuorumPolicy::AtLeast(2))
1365 .voter("a", voter(Verdict::Allow), false)
1366 .voter("b", voter(Verdict::Block), false)
1367 .voter("c", voter(Verdict::Block), false);
1368 let decision = quorum.decide(&probe_action()).await.expect("decides");
1369 assert_eq!(decision.verdict, Verdict::Block);
1370 }
1371
1372 #[tokio::test]
1373 async fn given_at_least_two_policy_when_two_voters_affirm_then_should_allow() {
1374 let quorum = QuorumGovernor::new(QuorumPolicy::AtLeast(2))
1375 .voter("a", voter(Verdict::Allow), false)
1376 .voter("b", voter(Verdict::Observe), false)
1377 .voter("c", voter(Verdict::Block), false);
1378 let decision = quorum.decide(&probe_action()).await.expect("decides");
1379 assert_ne!(decision.verdict, Verdict::Block);
1380 }
1381
1382 #[tokio::test]
1383 async fn given_a_modify_voter_among_affirmatives_when_quorum_met_then_should_apply_the_body() {
1384 let quorum = QuorumGovernor::new(QuorumPolicy::All)
1385 .voter("a", voter(Verdict::Allow), false)
1386 .voter(
1387 "redactor",
1388 voter(Verdict::Modify {
1389 body: b"redacted".to_vec(),
1390 }),
1391 false,
1392 );
1393 let decision = quorum.decide(&probe_action()).await.expect("decides");
1394 assert_eq!(
1395 decision.verdict,
1396 Verdict::Modify {
1397 body: b"redacted".to_vec()
1398 }
1399 );
1400 }
1401
1402 #[tokio::test]
1403 async fn given_no_quorum_with_a_step_up_and_a_defer_when_decided_then_should_prefer_step_up() {
1404 let quorum = QuorumGovernor::new(QuorumPolicy::All)
1405 .voter(
1406 "reviewer",
1407 voter(Verdict::StepUp {
1408 scope: "storage:rotate".to_owned(),
1409 }),
1410 false,
1411 )
1412 .voter("scheduler", voter(Verdict::Defer), false);
1413 let decision = quorum.decide(&probe_action()).await.expect("decides");
1414 assert!(matches!(decision.verdict, Verdict::StepUp { .. }));
1415 }
1416
1417 #[tokio::test]
1418 async fn given_no_voters_when_decided_then_should_block() {
1419 let quorum = QuorumGovernor::new(QuorumPolicy::All);
1420 let decision = quorum.decide(&probe_action()).await.expect("decides");
1421 assert_eq!(decision.verdict, Verdict::Block);
1422 }
1423
1424 #[tokio::test]
1425 async fn given_an_invalid_threshold_when_decided_then_should_block() {
1426 let quorum =
1427 QuorumGovernor::new(QuorumPolicy::AtLeast(0)).voter("a", voter(Verdict::Allow), false);
1428 let decision = quorum.decide(&probe_action()).await.expect("decides");
1429 assert_eq!(decision.verdict, Verdict::Block);
1430 }
1431
1432 #[tokio::test]
1433 async fn given_duplicate_voter_names_when_decided_then_should_block() {
1434 let quorum = QuorumGovernor::new(QuorumPolicy::Any)
1435 .voter("same", voter(Verdict::Allow), false)
1436 .voter("same", voter(Verdict::Allow), false);
1437 let decision = quorum.decide(&probe_action()).await.expect("decides");
1438 assert_eq!(decision.verdict, Verdict::Block);
1439 }
1440
1441 #[tokio::test]
1442 async fn given_a_mandatory_step_up_when_another_allows_then_should_step_up() {
1443 let quorum = QuorumGovernor::new(QuorumPolicy::Any)
1444 .voter(
1445 "safety",
1446 voter(Verdict::StepUp {
1447 scope: "storage:rotate".to_owned(),
1448 }),
1449 true,
1450 )
1451 .voter("llm", voter(Verdict::Allow), false);
1452 let decision = quorum.decide(&probe_action()).await.expect("decides");
1453 assert!(matches!(decision.verdict, Verdict::StepUp { .. }));
1454 }
1455
1456 #[tokio::test]
1457 async fn given_a_mandatory_error_when_another_allows_then_should_block() {
1458 let quorum = QuorumGovernor::new(QuorumPolicy::Any)
1459 .voter("safety", Arc::new(AlwaysErrors), true)
1460 .voter("llm", voter(Verdict::Allow), false);
1461 let decision = quorum.decide(&probe_action()).await.expect("decides");
1462 assert_eq!(decision.verdict, Verdict::Block);
1463 }
1464
1465 #[tokio::test]
1466 async fn given_conflicting_modifications_when_quorum_met_then_should_block() {
1467 let quorum = QuorumGovernor::new(QuorumPolicy::All)
1468 .voter(
1469 "one",
1470 voter(Verdict::Modify {
1471 body: b"one".to_vec(),
1472 }),
1473 false,
1474 )
1475 .voter(
1476 "two",
1477 voter(Verdict::Modify {
1478 body: b"two".to_vec(),
1479 }),
1480 false,
1481 );
1482 let decision = quorum.decide(&probe_action()).await.expect("decides");
1483 assert_eq!(decision.verdict, Verdict::Block);
1484 }
1485
1486 #[tokio::test]
1487 async fn given_an_erroring_voter_when_decided_then_should_not_veto_the_others() {
1488 let quorum = QuorumGovernor::new(QuorumPolicy::Any)
1489 .voter("flaky", Arc::new(AlwaysErrors), false)
1490 .voter("steady", voter(Verdict::Allow), false);
1491 let decision = quorum.decide(&probe_action()).await.expect("decides");
1492 assert_eq!(decision.verdict, Verdict::Allow);
1493 assert!(decision.reason.as_deref().unwrap().contains("error("));
1494 }
1495
1496 #[tokio::test]
1497 async fn given_an_erroring_voter_alone_when_decided_then_should_deny_for_lack_of_quorum() {
1498 let quorum =
1499 QuorumGovernor::new(QuorumPolicy::Any).voter("flaky", Arc::new(AlwaysErrors), false);
1500 let decision = quorum.decide(&probe_action()).await.expect("decides");
1501 assert_eq!(decision.verdict, Verdict::Block);
1502 }
1503
1504 #[tokio::test]
1505 async fn given_a_swappable_governor_when_decided_then_should_run_the_active_policy() {
1506 let swappable = SwappableGovernor::new(voter(Verdict::Allow));
1507 let decision = swappable.decide(&probe_action()).await.expect("decides");
1508 assert_eq!(decision.verdict, Verdict::Allow);
1509 }
1510
1511 #[tokio::test]
1512 async fn given_a_policy_swap_when_decided_again_then_should_run_the_new_policy() {
1513 let swappable = SwappableGovernor::new(voter(Verdict::Allow));
1514 swappable.swap(voter(Verdict::Block));
1515 let decision = swappable.decide(&probe_action()).await.expect("decides");
1516 assert_eq!(decision.verdict, Verdict::Block);
1517 }
1518
1519 #[tokio::test]
1520 async fn given_a_swap_when_applied_then_should_return_the_previous_policy() {
1521 let swappable = SwappableGovernor::new(voter(Verdict::Allow));
1522 let previous = swappable.swap(voter(Verdict::Block));
1523 let previous_decision = previous.decide(&probe_action()).await.expect("decides");
1524 assert_eq!(previous_decision.verdict, Verdict::Allow);
1525 let current_decision = swappable
1526 .current()
1527 .decide(&probe_action())
1528 .await
1529 .expect("decides");
1530 assert_eq!(current_decision.verdict, Verdict::Block);
1531 }
1532
1533 #[tokio::test]
1534 async fn given_clones_sharing_one_swappable_governor_when_one_swaps_then_all_should_observe_it()
1535 {
1536 let swappable = Arc::new(SwappableGovernor::new(voter(Verdict::Allow)));
1537 let clone = Arc::clone(&swappable);
1538 clone.swap(voter(Verdict::Block));
1539 let decision = swappable.decide(&probe_action()).await.expect("decides");
1540 assert_eq!(decision.verdict, Verdict::Block);
1541 }
1542}