The Audit Problem
When an AI agent calls a tool, who decides it was safe to do so? And how do you prove that decision wasn't tampered with later?
These questions matter because AI agents are becoming autonomous decision-makers. They read files, execute code, call APIs, and interact with internal systems. Every tool call represents a security-sensitive action.
Most guardrail implementations today lack a critical property: tamper evidence. They log decisions to a database, but if someone modifies that log — or if an attacker gains write access — there's no way to detect it.
Content-Addressed Decision IDs
The solution is borrowed from content-addressed storage (Git, IPFS, blockchain): the identifier of a decision is derived from its content.
Here's the core algorithm:
import hashlib
import json
def canonical_json(obj: dict) -> str:
"""Deterministic JSON serialization with sorted keys."""
return json.dumps(obj, sort_keys=True, separators=(",", ":"))
def compute_decision_id(claims: dict, expires_at: float | None = None) -> str:
"""
decision_id = SHA-256(canonical_json(claims ∪ {_expires_at}))
"""
preimage = dict(claims)
if expires_at is not None:
preimage["_expires_at"] = expires_at
canonical = canonical_json(preimage)
return hashlib.sha256(canonical.encode()).hexdigest()
Every decision ID is a deterministic hash of its claims. This means:
- Same claims → same decision ID (reproducible audit)
- Modified claims → different decision ID (tamper detection)
- No central sequencer needed (decentralized verification)
Why This Matters for Tamper Detection
Compare two approaches to audit logging:
| Property | Traditional Log | Content-Addressed Decision |
|---|---|---|
| Storage | Database row | Deterministic hash + claims |
| Integrity check | None (trust the DB) | Recompute hash, compare |
| Tamper detection | Requires WAL/checksum | Self-verifying |
| Fork resolution | Impossible | Pick the chain with valid hashes |
| Cross-system verification | Requires trusted third party | Any verifier can recompute |
With content-addressed decisions, verifying integrity is trivial:
class GuardrailDecisionV1:
def __init__(self, decision_id: str, authorized: bool, claims: dict, expires_at: float | None = None):
self.decision_id = decision_id
self.authorized = authorized
self.claims = claims
self.expires_at = expires_at
def verify_integrity(self) -> bool:
"""Recompute the expected ID and compare."""
expected = compute_decision_id(self.claims, self.expires_at)
return self.decision_id == expected
If an attacker changes claims["tool_name"] from "read_file" to "rm_-rf", the recomputed hash won't match. The tamper is immediately detectable.
Including Expiry in the Preimage
One subtle design: we include expires_at in the hash preimage.
Without this, an attacker could take an authorized decision and extend its validity indefinitely:
# Original: authorized to read /data, expires in 1 hour
decision_id = SHA-256({tool: "read_file", path: "/data"})
# → "a3f8b2..."
# Attacker modifies claims to change path, but keeps same decision_id
# Allowed because decision_id was computed from claims only
# Worse: attacker keeps decision_id, removes expires_at
# Decision never expires — permanent authorization
By including _expires_at in the hash:
decision_id = SHA-256(canonical_json({
tool: "read_file",
path: "/data",
_expires_at: 1742515200 # Unix timestamp
}))
Now both the claims AND the expiry window are bound to the decision ID. Neither can be changed without detection.
Practical Implementation: GuardrailProvider Protocol
Here's how this translates into a real guardrail system — the GuardrailProvider protocol we shipped in v4.1.0 across Python, Go, and TypeScript SDKs:
from correctover.guardrail import (
ToolListGuardrailProvider,
AuditTrail,
make_guardrail_hook,
)
# Define policy
provider = ToolListGuardrailProvider(allowed_tools=["read_file", "search"])
# Create audit trail
trail = AuditTrail()
# Install hook
hook = make_guardrail_hook(provider, trail=trail)
# Every tool call is now:
# 1. Authorized by provider
# 2. Recorded with content-addressed decision_id
# 3. Verifiable by recomputing the hash
# Later: verify integrity
report = trail.export()
report.verified # True if no tampering detected
Six Providers, One Protocol
All providers implement the same GuardrailProvider interface, ensuring all decisions are content-addressed:
| Provider | Decision Basis |
|---|---|
AllowAllGuardrailProvider |
Always authorized, logged with tool_name + agent_id |
DenyAllGuardrailProvider |
Always denied, logged |
ToolListGuardrailProvider |
Allowlist or denylist by tool name |
CKGGuardrailProvider |
Predicate-based evaluation (tool, agent, params) |
EnvProtectionProvider |
Pattern-matches tool args for credential leakage |
CompositeGuardrailProvider |
AND/OR composition of multiple providers |
Cross-Language Verification
Since decision IDs are deterministic SHA-256 hashes, you can verify a decision made in Python from Go or TypeScript:
# Python: authorize a tool call
decision = provider.authorize({"tool_name": "read_file", "agent_id": "alice"})
# decision_id = "a3f8b2..."
# Later, in Go: verify the same decision
// decision_id == sha256(claims) → must match
expected := ccs.ComputeDecisionID(claims, expiresAt)
if decision.DecisionID != expected {
// Tampering detected!
}
This is critical for cross-platform audit pipelines where decisions flow across language boundaries.
Real-World: Auditing the MCP Marketplace
Going back to the MCP marketplace problem — content-addressed guardrails solve a specific pain point for platforms like LobeHub (10,000+ plugins):
- Plugin installation: Each plugin registers its tool capabilities
- Runtime guardrail: Every tool call authorized by GuardrailProvider
- Tamper-proof audit: Decision IDs bound to both claims and expiry
- Post-mortem verification: Audit trail can be verified even if the system was compromised
Without content addressing, a compromised marketplace server could silently modify audit logs to hide malicious activity. With it, any tampering creates a detectable hash mismatch.
Getting Started
GuardrailProvider is open source and available now:
| Language | Package | Version |
|---|---|---|
| Python | pip install correctover |
v2.2.0 |
| Go | go get github.com/correctover/ccs-sdk/go/ccs |
v4.1.0 |
| TypeScript | npm install @correctover/ccs-sdk |
v4.1.0 |
The content-addressed audit protocol is specified in the CCS v1.0 Standard (DOI: 10.5281/zenodo.21271910).
Correctover is an open-source reliability runtime for AI agents — provides verified failover, MCP security validation, and content-addressed guardrail audit. GitHub | Docs
Top comments (0)