aex_audit/lib.rs
1//! Tamper-evident audit log.
2//!
3//! Every business-meaningful action in the Agent Exchange Protocol (AEX) — agent
4//! registration, transfer initiation, scanner verdict, policy decision,
5//! delivery, revocation — writes an [`Event`] to an [`AuditLog`].
6//!
7//! # Integrity model
8//!
9//! Events are **hash-chained**: each event stores the hash of the previous
10//! event. This means any retroactive modification of an event breaks the
11//! chain for every event that followed — you cannot rewrite history without
12//! producing a visibly different chain head.
13//!
14//! Chain-head hashes are the basis for future Merkle-batching and Sigstore
15//! Rekor submission (Phase G1). When that lands, the interface here does
16//! not change — [`AuditLog::current_head`] just starts being submitted to
17//! Rekor periodically.
18//!
19//! # Implementations
20//!
21//! - [`MemoryAuditLog`] — in-memory, used by tests and M1 demo.
22//! - [`FileAuditLog`] — append-only JSONL file, one line per event. Used
23//! by the dev-tier control plane.
24//! - *(Phase 2)* `PostgresAuditLog` — events in Postgres with a maintained
25//! `chain_head` table for fast reads.
26//! - *(Phase G1)* `RekorAnchoredAuditLog<Inner>` — wraps any inner log and
27//! periodically submits chain heads to the Sigstore Rekor transparency
28//! log.
29
30pub mod error;
31pub mod event;
32pub mod file_log;
33pub mod memory_log;
34pub mod rekor;
35
36pub use error::{AuditError, AuditResult};
37pub use event::{Event, EventKind, EventReceipt};
38pub use file_log::FileAuditLog;
39pub use memory_log::MemoryAuditLog;
40pub use rekor::{
41 LoggingRekorSubmitter, RekorAnchoredAuditLog, RekorReceipt, RekorSubmitter, StubRekorSubmitter,
42};
43
44use async_trait::async_trait;
45
46/// Core audit log trait.
47///
48/// Implementations must be internally synchronized — concurrent callers
49/// must see a serialized view of the chain. No external locking required.
50#[async_trait]
51#[allow(clippy::len_without_is_empty)]
52pub trait AuditLog: Send + Sync {
53 /// Append an event to the log. Returns a receipt the caller can keep
54 /// as proof the event is recorded (contains event id + chain head at
55 /// the time of append).
56 async fn append(&self, event: Event) -> AuditResult<EventReceipt>;
57
58 /// The current chain head: hex-encoded hash of the last appended event,
59 /// or the genesis sentinel if the log is empty.
60 async fn current_head(&self) -> AuditResult<String>;
61
62 /// Replay the full chain, verifying every stored hash against the
63 /// canonical bytes of the event. Returns `Ok(())` if the chain is
64 /// intact; errors identify the first event at which verification failed.
65 async fn verify_chain(&self) -> AuditResult<()>;
66
67 /// Total number of events appended since genesis.
68 async fn len(&self) -> AuditResult<u64>;
69}
70
71/// Sentinel value used as the `prev_hash` of the first event in a fresh
72/// chain. Chosen as the all-zeros 32-byte hash encoded as hex — sha256 of
73/// the empty string would also work, but all-zeros is unambiguous and does
74/// not accidentally match any real event.
75pub const GENESIS_HEAD: &str = "0000000000000000000000000000000000000000000000000000000000000000";