DEV Community

Vinay Bhosle
Vinay Bhosle

Posted on

How We Built a Tamper-Evident Audit Trail for AI Agents

last week we shipped a tamper-evident audit trail for AI agents. here is why we built it and the engineering decisions behind it.

the problem: agents without receipts

an AI agent making autonomous decisions leaves no external record of what it decided, why, or how confident it was. server logs exist, but the agent can modify its own logs. if the agent deletes production data, the logs explaining why might go with it.

when a human employee makes a critical mistake, there is a paper trail — emails, slack messages, meeting notes. we can reconstruct intent. with agents? the prompt is gone, the context window is gone, the confidence level is gone.

why server logs are not enough

server logs fail in exactly the scenario where you need them most: adversarial incidents. if the agent has write access to the system it is logging to, the audit trail is documentation, not evidence. the record has to survive the agent.

the architecture

we built a three-layer system:

1. canonical event log (single write path)

every agent action above a risk threshold produces a signed event. these flow through a single event bus — one source of truth. no action can bypass the bus.

agent decision → stamp service → event bus → canonical log
Enter fullscreen mode Exit fullscreen mode

2. CQRS read models

two separate indexes consume from the same event bus:

  • compliance index: every action attempted (including denied actions)
  • execution index: only actions that executed (the causal chain)

they answer different questions from the same underlying data. an EU AI Act auditor needs the compliance view. an incident responder needs the execution view.

3. SHA-256 hash chain with Ed25519 signatures

each event in the log includes:

  • the event payload (what happened)
  • Ed25519 signature (who did it — bound to registry identity)
  • SHA-256 hash of the previous event (chain integrity)
  • timestamp

this is structurally similar to how git commits work. each event references the previous event hash, creating a chain where any modification breaks all subsequent hashes.

stamp-first verification

a key design decision: the stamp happens before the action executes, not after.

// middleware pattern
const requireStamp = (riskLevel = 'medium') => {
  return async (req, res, next) => {
    const stamp = await createVerificationEvent({
      agent_id: req.agentId,
      action: req.path,
      intent: req.body,
      risk_level: riskLevel
    });

    // stamp is recorded BEFORE execution
    req.stampId = stamp.id;
    next();
  };
};

// usage
app.post('/api/delete-records',
  requireStamp('high'),
  deleteRecordsHandler
);
Enter fullscreen mode Exit fullscreen mode

why stamp-first? if the authorization gate blocks an action, stamp-first produces a denied-receipt. gate-first produces silence. you cannot distinguish "the gate blocked it" from "it never reached the gate" without stamp-first ordering.

tombstone lifecycle closure

agents can crash, timeout, or hand off mid-task. a TTL-based credential expiry looks like lifecycle management but it is actually deferred ambiguity — you do not know if the agent completed, crashed, or handed off.

we added tombstone events as required lifecycle markers. the delta between a tombstone and a TTL expiry is a recorded gap window — the forensic artifact that turns ambiguous termination into a queryable event.

gap detection

the verify-chain endpoint walks the full hash chain and detects two types of attacks:

  1. tampered events: hash does not match payload
  2. gap attacks: missing events in the sequence (someone deleted from the middle)
GET /verify-chain/:agentId

{
  "valid": false,
  "events_checked": 847,
  "gaps_detected": 2,
  "first_gap_at": "event_index_342"
}
Enter fullscreen mode Exit fullscreen mode

EU AI Act compliance

the compliance endpoint generates a structured report from the audit trail:

GET /compliance/report/:agentId

{
  "risk_level": "limited",
  "transparency_declaration": { ... },
  "audit_summary": {
    "total_actions": 847,
    "denied_actions": 12,
    "high_risk_actions": 34,
    "chain_integrity": "valid"
  }
}
Enter fullscreen mode Exit fullscreen mode

this was not planned as a feature — it fell out naturally from having separate compliance and execution query paths. once you have the data structured correctly, the regulatory report is just a view.

what we learned

  1. the write path is the trust boundary. if you control the write path, you control the truth. a single canonical event bus eliminates divergence between indexes.

  2. denied actions are forensically important. an absent event is indistinguishable from a dropped event. stamp-first ordering makes every decision boundary visible.

  3. lifecycle closure needs active signals. TTL is a fallback, not a strategy. tombstone events turn ambiguous termination into a queryable fact.

  4. compliance and execution are different retrieval patterns against the same data. CQRS is not over-engineering here — it is the minimum viable separation for regulatory and operational queries.

the full implementation is open source at github.com/vinaybhosle/agentstamp. 188 unit tests, v2.3.0.

Top comments (0)