agentguard_server/audit.rs
1//! Audit persistence port used by PDP request handling.
2
3use agentguard_core::decision::DecisionLog;
4use agentguard_core::{Decision, Result};
5use async_trait::async_trait;
6use std::sync::Arc;
7
8/// Durable audit operations required by the HTTP and gRPC adapters.
9///
10/// Implementations must make `append_decision` durable before returning `Ok`.
11/// The handler fails closed if appending fails. Calls run on the PDP's bounded
12/// blocking-work pool, so a synchronous adapter may perform blocking I/O but
13/// must not create its own unbounded queue. `is_chained` must only return true
14/// when persisted records have tamper-evident integrity protection.
15///
16/// Embedders can inject an implementation with
17/// [`AppState::with_audit_appender`](crate::authzen::AppState::with_audit_appender).
18pub trait AuditAppender: Send + Sync {
19 fn append_decision(&self, decision: &Decision) -> Result<()>;
20 fn is_healthy(&self) -> bool;
21 fn is_chained(&self) -> bool;
22}
23
24/// Async durable audit port for database or network-backed implementations.
25///
26/// Implementations must make `append_decision` durable before returning `Ok`.
27/// Append is cancelled after the PDP's five-second deadline, so adapters must
28/// make cancellation behavior explicit for their storage client; a timeout is
29/// treated as an unknown/failed append and the decision is not returned.
30/// Health and chain-integrity checks are awaited by `/readyz` with a bounded
31/// timeout. As with the synchronous port, `is_chained` must only return true
32/// when stored records are tamper-evident.
33#[async_trait]
34pub trait AsyncAuditAppender: Send + Sync {
35 async fn append_decision(&self, decision: &Decision) -> Result<()>;
36 async fn is_healthy(&self) -> bool;
37 async fn is_chained(&self) -> bool;
38}
39
40#[derive(Clone)]
41pub(crate) enum AuditWriter {
42 Sync(Arc<dyn AuditAppender>),
43 Async(Arc<dyn AsyncAuditAppender>),
44}
45
46impl AuditAppender for DecisionLog {
47 fn append_decision(&self, decision: &Decision) -> Result<()> {
48 DecisionLog::append_decision(self, decision)
49 }
50
51 fn is_healthy(&self) -> bool {
52 DecisionLog::is_healthy(self)
53 }
54
55 fn is_chained(&self) -> bool {
56 DecisionLog::chain_id(self).is_some()
57 }
58}