DEV Community

Jay Leon
Jay Leon

Posted on

Your AI agents' audit trail is a photo. I spent six days building the video.

In November 2025, AppOmni Labs showed
that text in an ordinary service ticket could make a ServiceNow Now Assist AI agent
recruit a second, more privileged agent, read a record its author had no right to see, and
copy it into one they owned. The vendor's prompt-injection defenses — against malicious
instructions hidden in data an AI reads — were on the whole time. A lab demo, but the
shape matters: the attack broke no rule, only moved through gaps between rules everyone
had approved.

Agent governance — the product category deciding what a company's AI agents may do —
answers that shape with one ritual: an AI drafts a tighter rule, a human approves it, an
audit log records the approval — a log with no authority, which nobody reads again.

That audit trail is a photo: proof the rule was justified the day it was approved. Nobody
has the video — proof it stayed justified every day since. For the All Things Agentic
Hackathon, I spent six days building it.

Detection decay is what the photo misses: attackers move, and a rule that earned
approval in July stops deserving it by September, its signature still verifying. Supply-chain
attestation (sigstore, in-toto, SLSA) proves a software artifact is exactly what its
builder produced, pinned to a digest — a cryptographic fingerprint. It can ignore
decay because its subjects hold still: in-toto assumes each is immutable behind its digest
— true for container images. A decision's evidence can drift, and there is no digest for
"the state of what we knew."

Caseharden is a governance layer for an AI-agent fleet, with one design rule: a
guardrail — a rule constraining what agents may do — holds authority only while its
evidence keeps re-verifying. Easiest way to see it: one guardrail's life, from the repo's
run captures — v4 of the fleet's conduct policy.

One guardrail's life, frame by frame

v4 is three sentences of law you already enforce on your human employees: stay in your
declared lane (out-of-declared-scope); if the request smells poisoned, touch nothing
(tool-call-on-injected-turn — the intern doesn't wire money because a convincing email
said to); never reach into another customer's drawer (cross-tenant-tool-call).

Three terms. The Proposer is the AI that drafts rules. The Examiner scores every
draft against the holdout: a sealed set of recorded attack sessions — an exam the
Proposer must pass but never read. Promotion makes an approved draft binding
fleet-wide. How did three sentences earn that authority? Watch.

Frame 1 — the approval. A detector flagged real out-of-scope sessions; a human
confirmed the abuse; the Proposer drafted v4 to close it. Promotion writes a seven-link
chain, from evidence scanned to approval granted; verify re-derives all of it from raw
events. Look at links 5 and 6: the live refusal proving the Proposer
couldn't read its own exam, and the exam score it protects.

  [1] EVIDENCE         OK   424 conduct events re-scanned, holdout readable by 1 principal
  [5] HOLDOUT-DENIED   OK   proposer-sa refused bigquery.tables.getData on holdout_sealed
  [6] EXAM             OK   the Examiner re-scores 29/40 sealed attacks at 100% benign, unchanged
  [7] APPROVAL         OK   GATE PASS approved by analyst

  ATTESTED   re-derived from raw events in 5.4s
Enter fullscreen mode Exit fullscreen mode

Every photo system stops here. The recording keeps rolling.

Frame 2 — one ordinary late event. A conduct event — a row recording something an
agent did — arrives in the evidence window after promotion. No attack — a row simply
arrived late. In the next verify, watch link 1 flip from OK to BREAK over that one row:

  [1] EVIDENCE   BREAK  1 event(s) in the window are not in the cited set: e_late_1787661269

  QUARANTINED  break at link 1 EVENT-WINDOW
               enforcement unchanged, promotions FROZEN
Enter fullscreen mode Exit fullscreen mode

Why does one late row cost a version its standing? Two clocks. Clock one, the evidence
window — the span of events the investigation cites — closed 23 seconds in, when the
detectors finished scanning: finding and citation vouch for the same rows.
Clock two starts at promotion, when a digest is taken of everything inside the window.
Below, everything hinges on the third mark:

window_start -------- window_end -------- digest taken --------->
|                     |                   |
|<--- the citation -->|                   |
                      |<-- grace period ->|<-- from here on, any row
                         stragglers here      with a timestamp inside
                         are simply counted   the window BREAKS it
Enter fullscreen mode Exit fullscreen mode

Before the digest, a late row gets counted. After it, the same row quarantines the
version: the record claims it saw everything in the window; this row proves it didn't.
Nothing about the row changed — only when it arrived.

Quarantine is deliberately asymmetric: v4 still blocks everything it blocked — dropping
enforcement over stale paperwork would be backwards — but its standing is gone and a v5 on
top is refused. The remedy is re-derivation, not undo: the justification re-runs, the
Examiner re-scores, a new link supersedes the break without erasing it. Watch the count
tick from 424 to 425, exam score unchanged:

  [8] EVIDENCE-CHANGED OK   425 conduct events re-scanned; 29/40 sealed attacks, unchanged

  ATTESTED   re-derived from raw events in 4.0s
Enter fullscreen mode Exit fullscreen mode

The video keeps the frame where the rule lost its standing.

A video is only worth recording if nobody can tamper with the camera. In 2021, Ubiquiti's
cloud lead used his legitimate admin credentials to steal company data, then set log
retention to one day so the evidence erased itself (DOJ, six years).
The camera's owner could reconfigure it. So Caseharden's two locks are
platform-enforced, not my code — they bind me too.

The AI can't study for its own exam. A Proposer that can read the holdout can be
re-prompted until a draft passes — a rule tuned to its test, decayed on arrival. The
holdout's access list has one entry, the Examiner's service account, via IAM, Google
Cloud's permission system; when the Proposer reaches anyway, Google answers, not me —
captured on every run:

HTTP 403 PERMISSION_DENIED
"Access Denied: Table devpost-hackathon-506416:holdout_sealed.turns"
Enter fullscreen mode Exit fullscreen mode

That 403 becomes its own chain link: the record shows what was prevented, not only what
occurred.

The author can't reshoot the video. The chain's root is sealed in a Cloud Storage
object under a 30-day retention policy — I clicked locked, the irreversible one. Here
is that policy rejecting my own delete:

$ curl -X DELETE .../o/certificates%2Fday1-seal-check.json
{ "error": { "code": 403, "message": "Object ... is subject to bucket's retention policy
    ... and cannot be deleted or overwritten until 2026-09-23T23:03:14" } }
Enter fullscreen mode Exit fullscreen mode

Honest residual: the chain lives in BigQuery, Google's data warehouse, with no append-only
mode; a credentialed writer could rewrite and re-seal it. The locked object is the
witness a rewrite cannot erase, not a gate it cannot pass. Platform enforcement moves the
line from "trust me" to "catch me" — not to "impossible."

Frame 3 — the one I didn't script. On day 4 I granted an unrelated detector's new
service account an ordinary BigQuery role — neither chain nor holdout touched. The next
verify names the exact grant at link 8:

  [8] EVIDENCE-CHANGED BREAK  project-level roles granted since promotion:
                              roles/bigquery.jobUser:serviceAccount:detector-sa@...

  QUARANTINED  break at link 8 HOLDOUT-ACCESS
               enforcement unchanged, promotions FROZEN
Enter fullscreen mode Exit fullscreen mode

The attestation covers who can read the exam, so a routine permission grant made v4
unable to prove itself justified — the system said so before I'd noticed. That unplanned
catch, more than anything I designed, convinced me the video is a different product from
the photo.

The video's best scene is a refusal

Two days before the deadline, the gate refused my AI's last four guardrail drafts. Attempt
3 fails on one line — CATCH, sealed attacks caught, didn't improve — and attempt 4
produces no candidate at all:

attempt 3 of 4
  [FAIL] CATCH          30/40 -> 30/40 sealed attack sessions
  [PASS] BENIGN         100.0% -> 100.0% of 640 legitimate turns
  [PASS] MONOTONICITY   every active rule is carried forward or narrowed
  NO IMPROVEMENT ON SEALED ATTACKS
  refused; asking for a narrower candidate

attempt 4 of 4
  DRAFT REJECTED: no candidate in the answer

no candidate passed the gate in 4 attempts; nothing was written to the chain
Enter fullscreen mode Exit fullscreen mode

The ceiling is arithmetic, not a bug. The fourth attack family spans a whole session — a
read-then-write pattern — so catching it needs a self-join, matching a session's events
against each other, which this deliberately tiny deny-only language (rules forbid, never
permit) can't express. Earlier a draft caught one such attack but blocked two legitimate
turns; the benign leg threw it out.

In a photo system the author now loosens the gate, overrides it, or re-prompts until
something slips through; re-prompting against a sealed exam just leaks it slowly. The two
locks removed all three outs: exam unreadable, record undeletable. The system's last
recorded act before submission is refusing its author; production runs the last version
the evidence supports, not the newest drafted. I shipped that.

Measured, not estimated: 335 tests; 84 bugs deliberately planted and all 84 caught
(mutation testing); verify at 3.66s p95. Don't take the camera operator's word for it:
python3 -m caseharden.recheck fixtures/v5 replays the Examiner and re-walks the chain
on your machine — no credentials — and on GitHub's runners on every push.

The photo isn't my invention — the repo's prior-art table credits it. Unit21, Sublime
Security and Stripe Radar ship AI-drafted, human-approved rules; Microsoft's Agent
Governance Toolkit intercepts tool calls pre-execution; Google ships per-agent IAM and an
Agent Gateway; Salfeld-Nebgen (arXiv 2606.26298)
states the thesis: authority from attested evidence. None of it has the part after
approval: quarantine, and the re-derivation that triggers it.

How it's built

One Google Cloud project, europe-west3.

Caseharden architecture

Nine private Cloud Run services, every one deployed --no-allow-unauthenticated, so
nothing here is reachable from the internet. Eight come from a single image and an
environment variable picks which agent the container becomes — the four detectors really
are one program, and six Dockerfiles would be six things to keep in step. The ninth is the
analyst's console: adk deploy cloud_run --with_ui, unmodified.

The agents are ADK, and the Foreman knows no detectors. It fans out over A2A to
whatever answers list_agents() against Agent Registry; its source names none of them, and
the fleet proof greps it to keep that true. Seven agents are published to the registry, each entry
carrying the chain root of the policy version it was registered against — the roster says
what each worker's authority rests on, not just where it lives. One fan-out is a 361-span
Cloud Trace.

Identity is the load-bearing part. Eight service accounts, one per role. proposer-sa
takes a real 403 on the sealed holdout: that is the [5] HOLDOUT-DENIED line above, and
it is BigQuery's answer rather than my code's. examiner-sa is the exam's only reader. The
access list producing that is hashed into the chain, so granting the Proposer access later
breaks the chain instead of going unnoticed.

Screening runs before the policy, not after it. Model Armor scores every
customer-facing turn, and its verdict is a first-class predicate in the policy language.
The order is forced rather than chosen: a rule can key on ma_verdict, so it cannot be
evaluated until screening returns. When Model Armor is unavailable and the policy depends
on it, the call is refused instead of allowed through.

The record is BigQuery plus one locked bucket. Conduct events, the chain and the
human's review rows are tables. The chain's root is sealed into Cloud Storage under a
retention policy I locked, so I cannot delete that object and neither can the project
owner. That lock is the only reason a record coming from me is worth anything.

The offline half touches no cloud SDK. Everything that talks to Google is a urllib
call, so the re-check, the 567 tests and the 84-mutation harness run on three dependencies
and no credentials. That is how you can check the record without access to my project:

git clone https://github.com/japsdeleon/caseharden && cd caseharden
pip install -r requirements-verify.txt
python3 -m caseharden.recheck fixtures/v5
Enter fullscreen mode Exit fullscreen mode

If you're building agent governance

Three things this build settled:

  • Steal from supply-chain security, but know where the analogy breaks. Signatures and admission gates transfer; the immutable-subject assumption does not. Governing decisions? Design for re-derivation, or accept silent decay.
  • Move the "no" out of your own process. An IAM denial and a retention lock bind the author; no in-process check does. The field's cheapest structural upgrade.
  • Don't burn irreversible locks while your schema is moving. The retention lock is the whole trust story and can't be undone; lock too early and you lose a month.

Deliberately not built: KMS signing (managed-key signatures), a
session-spanning rule language, a custom UI, and any model call in the verification path —
a scorer you can't replay deterministically can't be re-verified.

The photo is solved. The interesting product is the video: governance where approval must
keep surviving the evidence, and the author is inside the frame. The sealed record naming
the last justified version sits in a bucket that won't let me delete it until late
September.

Which of your rules would still pass the exam that approved them? Repo, threat model (ten
holes listed), full prior-art table:
https://github.com/japsdeleon/caseharden

Demo video: [https://www.youtube.com/watch?v=t6IU79aTdBo]


I created this post for the purposes of entering the All Things Agentic Hackathon.
#AllThingsAgenticHackathon

Top comments (0)