Skip to main content

Immutable Audit Logs

Because autonomous agents operate without constant human supervision, post-incident forensics are critical. AgentVault maintains cryptographically chained audit trails that make log tampering mathematically detectable.

Hash Chain Design

Every audit event references the hash of the previous event, forming a per-tenant chain:
genesis → evt_1 → evt_2 → evt_3 → ... → evt_N
   │         │        │        │
   └─hash──► └─hash─► └─hash─► └─hash──►

Hash Computation

Each event hash covers: previous hash, timestamp, trace context, event body, and participants. It excludes: observed_timestamp, delivery metadata, and the hash_chain block itself.
import hashlib, json

def compute_event_hash(event: dict, previous_hash: str) -> str:
    hash_input = {
        "previous_hash": previous_hash,
        "timestamp": event["timestamp"],
        "trace_id": event["trace_id"],
        "span_id": event["span_id"],
        "body": event["body"],
        "sender": event["attributes"].get("av.sender.entity_id"),
        "recipient": event["attributes"].get("av.recipient.entity_id"),
        "sequence_number": event["hash_chain"]["sequence_number"]
    }
    canonical = json.dumps(hash_input, sort_keys=True, separators=(",", ":"))
    return "sha256:" + hashlib.sha256(canonical.encode()).hexdigest()

Chain Properties

PropertyImplementation
AlgorithmSHA-256 (for hash chain linking) + BLAKE2b (for content hashing)
PartitioningPer-tenant — each tenant has its own independent chain
OrderingStrictly sequential by sequence_number within a tenant
Genesissha256(b"agentvault_genesis_v1") — deterministic and auditable

Traceparent Chaining

Agent actions are linked via W3C TraceContext traceparent headers:
00-{trace_id}-{span_id}-{trace_flags}
  • trace_id — deterministically derived from conversation/room ID
  • span_id — unique per message exchange
  • trace_flags — always 01 (sampled) for audit events
This enables full trace reconstruction: given a trace ID, you can reconstruct the entire conversation’s audit trail across all participants.

Event Types

EventSeverityDescription
message_deliveredINFOMessage successfully delivered to recipient
message_readINFORecipient acknowledged message
agent_connectedINFOAgent WebSocket connected
agent_disconnectedINFOAgent WebSocket disconnected
decision_madeINFOOwner resolved a decision request
policy_evaluatedINFOPolicy pipeline evaluated a request
action_executedINFOAgent executed a confirmed action
errorERRORAn error occurred during processing
security_violationFATALSecurity policy violated

Chain Verification

API Verification

POST /api/v1/audit/verify
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "tenant_id": "tnt_uuid",
  "from_sequence": 1,
  "to_sequence": 10847
}
{
  "valid": true,
  "events_verified": 10847,
  "first_hash": "sha256:...",
  "last_hash": "sha256:...",
  "verified_at": "2026-03-18T15:00:00Z"
}

Programmatic Verification

def verify_chain(events: list[dict]) -> tuple[bool, int | None]:
    """Returns (True, None) if valid or (False, break_index) if tampered."""
    for i, event in enumerate(events):
        expected_previous = GENESIS_HASH if i == 0 else events[i-1]["hash_chain"]["event_hash"]
        if event["hash_chain"]["previous_hash"] != expected_previous:
            return (False, i)
        recomputed = compute_event_hash(event, expected_previous)
        if event["hash_chain"]["event_hash"] != recomputed:
            return (False, i)
    return (True, None)
If verification fails, the break index identifies exactly which event was tampered with.

Querying Audit Trails

By Conversation/Room

GET /api/v1/audit/trace/{trace_id}
Returns all audit events for a conversation, ordered by sequence number.

By Time Range

GET /api/v1/audit/tenant?since=2026-03-01T00:00:00Z&until=2026-03-18T00:00:00Z&severity_min=17
The severity_min=17 filter returns only ERROR and above.

By Entity

GET /api/v1/audit/entity/{entity_id}?limit=100
Returns the most recent audit events involving a specific agent or user.

Storage & Retention

  • Primary storage: PostgreSQL with JSONB columns for flexibility and queryability
  • Indexes: Optimized for chain traversal, trace lookup, time-range queries, and severity filtering
  • GIN indexes on body and attributes for full-text JSONB search
  • Retention: Configurable per tenant (default: 90 days, enterprise: unlimited)
  • No external stack required: The audit system runs entirely on PostgreSQL — no ClickHouse, Elasticsearch, or external observability infrastructure needed

OTel Export

Audit events are natively OTel-compatible. When OTel export is enabled, events are dual-written:
  1. PostgreSQL — primary storage with hash chain integrity
  2. OTLP endpoint — exported to any OTel-compatible backend (Splunk, Datadog, Grafana)
The OTel export requires zero schema changes because the audit event structure already conforms to the OTel LogRecord data model. See the OTel Data Model for details.