Inspiration
Every agent-governance product I have seen ends the same way: the AI drafts a rule, a human approves it, and the system writes an audit log. The log describes the promotion. It has no authority over it. Nobody reads it again.
The failure mode this defends against is documented, not invented. In November 2025 AppOmni Labs showed that text submitted inside an ordinary service ticket could make a ServiceNow Now Assist agent recruit a second, more privileged agent, read a record the submitter had no right to, and copy its contents into a record the submitter owned. The vendor's own prompt-injection protection was enabled while that worked. Screening the input was not the control. It is a vendor lab demonstration rather than an incident in the wild, and the boundary crossed was an ACL boundary inside one instance, so it is cited here for the shape of the attack and not for its scale.
The supply-chain world already solved the neighbouring problem. sigstore, in-toto and SLSA bind an artifact to its build inputs and verify the signature before use. Kyverno and OPA Gatekeeper refuse to admit an artifact whose attestation does not verify.
But a signature proves nobody edited the claim. It does not prove the claim is still true. That gap is what Caseharden is about.
Two pieces of work got there first, and they are named in the README's prior-art table rather than left for a judge to find. Microsoft's Agent Governance Toolkit, MIT-licensed since April 2026, already intercepts every tool call before it executes, with Cedar and Rego behind it. Salfeld-Nebgen's Governing Actions, Not Agents (arXiv 2606.26298, June 2026) already states the authority-from-attested-evidence model. Neither addresses what happens after the evidence moves. That is the part this entry builds: the quarantine state itself — enforcement retained, standing lost, promotion frozen. A late-arriving event can quarantine a version no attacker ever touched, which no signature scheme detects.
What it does
Caseharden governs what an enterprise's AI agents may do, and how that answer is allowed to change.
The friction it removes is specific. A security team tightens a rule that governs an AI agent, and learns in production whether the tightening broke legitimate work. Caseharden makes that answer a precondition of the change rather than a consequence of it.
The human appears exactly twice in the loop, to judge and to approve. Everything else runs unattended.
A support agent does ordinary work with two tools, a tenant-scoped read and a refund write. Every tool call is screened by Model Armor and written as one structured conduct event to BigQuery. An orchestrator then reads Agent Registry at container start, names no detector anywhere in its source, and fans an investigation out over A2A (the agent-to-agent protocol) to whatever the roster lists. Four detectors answer in parallel. Each returns a finding and the BigQuery job id that produced it, so a reviewer re-runs the query instead of trusting a summary. W3C trace context rides the fan-out, so one investigation is one Cloud Trace trace. Every conduct row carries the trace id that opens it, the fleet proof asserts a 60-span DAG resolves in Cloud Trace, and the build log records one fan-out at 361 spans across the orchestrator and all four detectors.
The finding reaches a human analyst, who records one verdict in the Agent Development Kit's (ADK) own chat window. That is the first human turn; the second is the approval that ends the loop. Between them a Proposer agent reads the fleet's own review history out of Memory Bank, drafts a guardrail change in a small deny-only DSL (a rule language that can only forbid), and is refused by BigQuery with a real 403 when it reaches for its own exam. That denial is sealed into every promoted chain as its own link, so the record attests to what was prevented. A deterministic Examiner, not a model, scores the draft against that sealed holdout under a different service account, over 40 labelled attack sessions and 640 legitimate turns, and applies a three-leg gate: blocked attacks must go up, the benign pass rate must not fall, and the new version must only narrow authority. A failed candidate is never promoted; when a later attempt passes, the failures are sealed into the same chain beside the approval, and when none passes the chain stays untouched.
Why these are separate agents. Most agent systems split for tidiness, and tidiness merges back without losing anything. Here each boundary is a control. Merge the Proposer into the Examiner and the drafter can read the exam it is scored against, because BigQuery's access control binds to a principal and two principals need two processes; the 403 sealed into link 5 is that boundary seen from the inside. Merge the Notary into the Proposer and the agent writing the record becomes the agent the record is about. Merge the detectors into the orchestrator and a finding no longer carries a job id run under its own identity, so a reviewer re-runs a claim instead of a query. The fan-out is delegation; the split is the mechanism.
The gate holds at 30 of 40, and that is a property of the language rather than a shortfall.
The sealed holdout carries ten attack sessions for each of four check families. Three families
are expressible as rules over a single conduct event. The fourth is a read and a write inside
one session, which is a self-join and not a field, so no predicate in this DSL reaches it. No
version of the policy can deny those ten without also blocking legitimate work: every
candidate that has caught any of them was refused by the gate on the benign leg
(captures/day5-gate-refuses-the-proposer.txt,
captures/day5-gate-refuses-overblocking.txt). The ceiling sits under the best-evidenced
attack shape in the field — the AppOmni sequence from Inspiration is a privileged read
followed by a write — which is why the session-scoped predicate is first in What's next, and
why THREATS.md names this hole twice. The system knows where its own ceiling is and can show
the arithmetic.
Every step is a hash-linked record in a BigQuery chain, append-only by convention, and a hash
chain alone proves little: anyone holding the writer's credentials can rewrite it end to end.
The anchor is the root, sealed at promotion into a Cloud Storage object under a locked 30-day
retention policy and annotated onto the Agent Registry entry. The bucket refuses delete,
overwrite, and removal of the policy itself, from the project owner, and the lock is
irreversible. What the lock buys is narrower than tamper-proof, and THREATS.md says so
plainly: a credentialed writer who reseals a rewritten chain to a new certificate still passes
verify, so the locked object is the durable witness a rewrite cannot erase, not a gate a
rewrite cannot pass.
The product is not the detection. The product is the attestation. A guardrail version is
served as attested only while caseharden verify re-derives its evidence and its exam from
the raw conduct events and re-walks every link in its chain. When the evidence changes, the version is quarantined: it keeps blocking, but
it loses its standing as justified, and nothing can be promoted on top of it until it
re-derives.
How I built it
Nine private Cloud Run services in europe-west3. Eight run from one image and differ only by
environment; the ninth is adk deploy cloud_run --with_ui, unmodified: the agent surface is
ADK's own and the two tools behind it are the part I wrote. The analyst also gets a local
operator console that reads the chain, the registry and the finding under review, and routes
its one write through the unmodified Copilot, so it sits outside the trust boundary: it
offers a verdict and stores nothing itself. It runs against a committed fixture with no
credentials at all; the README carries the full trust-boundary argument.
This is the Fortified Enterprise Fleet track. Its five themes, one row each:
| Track theme | In this entry | Proof |
|---|---|---|
| Corporate agent discovery | the orchestrator reads Agent Registry at boot and names no detector in its source; each entry carries its policy version's chain root | captures/day7-fleet-proof-all-held.txt |
| Multi-agent orchestration | one investigation fans out over A2A to four detectors in parallel, each answering with a BigQuery job id | captures/day4-fleet-proof.txt |
| Long-term state persistence | the Proposer reads the fleet's own review history out of Memory Bank before every draft | agents/proposer/agent.py |
| Runtime observability | every conduct row carries a trace id; the fleet proof asserts its 60-span DAG resolves in Cloud Trace | captures/day7-fleet-proof-all-held.txt |
| Security posture enforcement | the three-leg gate, Model Armor verdicts as DSL predicates, the IAM 403, the locked retention root | captures/day2-gate-two-sided.txt · captures/day1-retention-refuses-delete.txt |
- Agent Registry is the roster and the discovery layer. The orchestrator's source names no detector. Each entry carries the chain root of the policy version it was registered against, so the platform's own discovery layer carries the audit anchor.
- Per-agent IAM identities are load-bearing rather than decorative.
proposer-saandexaminer-saare distinct principals. No principal holds a project-wide BigQuery data role, andholdout_sealed's access list has exactly one entry. - Model Armor screens in both directions, and its verdict fields are first-class predicates in the policy DSL, so policy composes with it instead of re-implementing it.
- Memory Bank holds the fleet's own review history, and the Proposer reads it before drafting.
- BigQuery holds the conduct events, the sealed holdout, the benign corpus, the chain and the policy registry. Cloud Storage with a locked retention policy holds the certificates. Gemini 3.5 Flash only, no Pro. The Examiner and the Notary make no model calls at all.
Challenges I ran into
Re-attestation was quietly changing which policy the fleet enforced. Re-pointing a version at its new certificate went through the promotion path, which marks its own version active and every other one inactive. Re-attesting an old version therefore put that version back in force. It happened on the live project and the Policy Server reported it truthfully, which is the only reason it was caught.
A deploy script had been writing empty public URLs. A mangled format argument made every
URL lookup fail, the failure was swallowed by a shell assignment, and four services then
advertised localhost in their agent cards.
Spans did not reach Cloud Trace from Cloud Run, for three days, and the transport was never
the problem. Four real faults were found and fixed on the way there and none of them was the
cause. It was the sampler: Cloud Run puts a traceparent on every inbound request with the
sampled flag off, OpenTelemetry's default sampler honours that, and every request span was
created with a valid context and never recorded. current_trace_id() then wrote a real span
id for a trace that was never written, so nothing failed anywhere. ALWAYS_ON on the provider
is the whole fix, and what made it findable was emitting one parentless span at container boot:
that span landed while every request span vanished, and the asymmetry separated "the transport
is broken" from "the spans are never recorded".
Accomplishments I am proud of
The gate refused the real Proposer's real candidate. It caught one more sealed attack than the active version and blocked two legitimate turns, and the benign leg alone was enough to throw it out. No fixture was involved.
It happened again on the final live run, and this time nothing passed. The Proposer got a
capped four attempts: three drafts scored no improvement on sealed attacks and the fourth
returned no candidate at all, so the loop stopped and wrote nothing to the chain — when no
attempt passes, the record stays untouched. Each attempt's feedback named only the failed
leg, never a holdout row, and the run was not re-taken: drafting against the sealed exam
until it passes is the tuning the Proposer and Examiner split exists to prevent. The version
the fleet enforces today is the one the gate let through, not the newest one drafted
(captures/day8-gate-refuses-v6-no-improvement.txt).
Every number published is measured. verify p95 is 3.66s against a 5s target. 567 tests. 84
mutations broken on purpose and all 84 caught, including one that survived its first run and
now has two tests.
What I learned
An assertion that cannot fail is worse than no assertion. Three of the defects above were sitting behind checks that were passing: a trace check that accepted any 32-character string, a memory check that any earlier run could satisfy, and a distinction between attested and unattested that was false for every block ever taken.
What's next for Caseharden
A session-scoped predicate in the DSL. The policy language is per-turn; the attacks that
survive the current policy spread their steps across a session. The sealed holdout already
carries ten session-shaped attacks, and THREATS.md records the one decision that gate needs
first: which of the two session patterns the family means.
An exam for the second policy line. The registry and the serving layer already hold two
lines — conduct-policy, examined and enforced, and payments-policy, a one-rule
human-granted floor — and the deny-only DSL makes their composition provable: a promotion in
any line can only narrow the fleet's total authority (THREATS.md entry 11). What the
second line is missing is its own sealed holdout, and building one at scale is a curation
problem: detectors reduce millions of conduct events to findings, a Curator agent — a
distinct identity that must never share anything with the Proposer — clusters them, drafts
attack variations from confirmed patterns, and proposes the near-boundary benign sessions
that make the BENIGN leg mean something, with every exam addition chained the way promotions
are today. The exam evolving is the evidence moving, and the system already knows what to do
when the evidence moves.
Then a second host. Every agent is on Cloud Run today, and putting one on Agent Runtime would show that the roster and the enforcement callback do not care where a worker lives.
Built with
google-adk · Agent Registry · Model Armor · Memory Bank · Vertex AI Gemini 3.5 Flash ·
Cloud Run · BigQuery · Cloud Storage retention lock · Cloud IAM · Cloud Trace · Cloud Build ·
Python · OpenTelemetry
Try it out
- Repository: https://github.com/japsdeleon/caseharden
- Specification:
docs/PLAN.md· Daily build log with measured numbers:BUILD_LOG.md - Terminal captures for the claims above:
captures/ - Check the record yourself, no credentials, no network, seconds:
python3 -m caseharden.recheck fixtures/v5— seventeen checks, including a full Examiner replay against corpora regenerated from the committed seeded generator - Open the analyst console on the same fixture, also credential-free:
python3 -m caseharden.workbench --fixture fixtures/v5 - The same recheck runs on GitHub's own runners on every push (Actions tab), so a machine that is not mine vouches for the record
- The two guarantees reproduce without trusting this code:
captures/day1-proposer-403-on-holdout.txt(BigQuery refuses the Proposer its own exam) andcaptures/day1-retention-refuses-delete.txt(Cloud Storage refuses a delete from the project owner) - Threat model, including ten holes stated plainly:
THREATS.md
Clean-room disclosure
No employer code, data, schemas, table names or infrastructure. All data is synthetic and generated by a seeded script committed to the repository. The build machine also holds an unrelated employer's Application Default Credentials, so nothing in this project reaches for ADC without checking the project first, and the guard reads ADC precisely in order to refuse it.
Built With
- cloud-iam
- cloud-run
- cloud-trace
- gemini
- google-adk
- google-bigquery
- google-cloud
- google-cloud-build
- vertex
Log in or sign up for Devpost to join the conversation.