Inspiration

I went looking for the field in Google's Agent Registry that records what code an agent is running, and there isn't one.

The writable resource is Service. It has name, displayName, description, interfaces[], registryResource, createTime, updateTime, and one of agentSpec / mcpServerSpec / endpointSpec. That's the lot. No state, no approvalState, no labels or annotations I could stash one in. The read-only Agent resource has an attributes map but it's output-only with reserved keys, so I can't write a verdict there either.

Agent Identity doesn't help. Its SPIFFE ID is, in Google's words, "mapped directly to the resource URI where the agent is hosted". It tells you where an agent runs. It says nothing about what's running there.

Which means "enterprise-approved" currently just means somebody with the right IAM role deployed it.

So I tried it. I redeployed an agent with completely different code, under the same name and the same service account. The SPIFFE ID didn't change. The IAM grants didn't change. The Agent Registry entry came back byte-identical. Google's platform noticed nothing at all.

That's the problem I wanted to fix.

Who it's for

Not the engineer who wants to run an agent fleet. The person who has to approve it.

Every organisation has someone whose job is to sign off before something reaches production. They're not on the agent team, they usually can't read the agent's code, and they are the reason most of these fleets are still sitting in a staging project nobody talks about. The only question they can currently get a real answer to is "who deployed this", because that's the only question the platform answers. The question they actually need answered is "what is it running", and nobody can tell them.

So they say no. Not to be difficult. Saying yes would mean vouching for something they have no way of checking.

I built this for that person. It turns an approval they'd have to take on trust into one they can verify themselves, in a browser, with no account and no access to anything of mine. That's why the admission records are world-readable and why the verifier runs client-side rather than behind an API I control. An approval only I can read is just me asserting something.

What it does

Every agent runs as a workload on Google Confidential Space, so the hardware will attest which image is running. When an agent wants to join the registry it has to hand over that attestation. My control plane checks it offline against a Google root certificate pinned into the binary at build time, twenty checks, and only then calls services.create on the real Agent Registry.

I split approval from proof, because they answer different questions:

  • an operator approves a digest up front, stored in policies/admission
  • the attestation proves the code actually running is that digest

Either one alone is useless. An allowlist with no attestation is just a database row that anyone with write access can satisfy. An attestation with no allowlist is trust-on-first-use, where whoever launches a workload first gets to admit themselves.

After admission each agent re-attests every 60 seconds. If the digest stops matching what it was admitted under, the admission is revoked and the reason records both digests.

The fleet

I picked cloud estate hygiene, because it's a case where you genuinely can't run the agents without solving this first.

Agent Job Cloud access
Warden Orchestrates and holds the policy no estate access
Surveyor Inventories the estate, emits findings broad read, no write
Assessor Works out blast radius no estate access
Executor Performs one approved remediation write, very narrow

No agent has two of those powers, and the split is IAM rather than instruction text. Executor gets compute.storageAdmin, so it can delete a disk and not an instance, and it is the only agent in the project with any mutating permission at all. Assessor gets aiplatform.user so it can call Gemini, plus the three housekeeping roles every workload here needs, and nothing that lets it see the estate. It reasons about findings it is handed and has no way to go and check them, which is the point. Every grant is in deploy/00-bootstrap.sh and I would rather you read it than take the table's word for it.

Executor can delete production resources, and Assessor decides what Executor gets told to delete. Their IAM permissions are worth nothing unless you can prove what code is sitting behind them, and right now nobody can.

That's the friction I'm removing. Not a chore, but the security review that stops fleets like this reaching production in the first place.

The workers are RemoteA2aAgent, so every delegation is a network hop that can be checked. In-process sub-agents would have been simpler and would have proved nothing, because the entire claim is that a worker's admission gets checked on the hop, and there is no hop if they share a process.

What happens when a worker misbehaves

This is the part I'd most like someone to actually read the code for, because it's the question I found hardest. A fleet where one agent tells another agent what to delete is only as good as what it does when a worker returns nonsense.

Warden's loop is in agents/warden/agent.py. One of these defences is permissions, one is code, and three are instructions to a model, and that difference matters enough that I've labelled each one. An instruction is a request. A permission is not.

  • PERMISSIONS. Assessor cannot act on its own reasoning. It has no estate access, so the agent doing the thinking has no way to touch anything. A hallucination there produces a wrong opinion and nothing else. This one holds even if the model ignores every word I wrote.
  • CODE. before_tool_callback refuses to delegate to a worker Warden believes is revoked. It runs on every delegation, in refuse_unadmitted_workers.
  • INSTRUCTION. A vague assessment isn't an assessment. Warden is told to accept one only if it cites the same finding id that was sent and reaches an explicit safe-or-unsafe conclusion with a stated reason, and to discard anything else. A model can disregard this. It cannot disregard the fact that Assessor had no credentials.
  • INSTRUCTION. Findings go across verbatim, because Assessor can't see the estate and anything Warden paraphrases is information Assessor will never have.
  • INSTRUCTION. No retries and no routing around a failed worker. Working around a worker that can't prove what it's running is the failure this project exists to prevent, so the instruction forbids it in those words, and doing nothing is named as the always-safe branch.

I'd rather label those three honestly than let the list imply more enforcement than there is. The permissions layer is what makes the fleet safe to run. The instructions are what make it useful.

There are two layers of admission checking and today only one of them runs. Warden's callback fails open on purpose, because it sits inside the agent whose behaviour is in question and anything that compromises Warden removes it. The design puts the real check at the gateway, which fails closed and runs somewhere the caller doesn't control. That gateway check is built and registered but not bound, so right now the delegation hop is guarded by the fast-fail callback and by IAM, and not by the gateway. I go into why below, and it isn't a story that flatters me.

Proof

Four agents in Google's Agent Registry, each one there because an attestation checked out:

surveyor -> https://surveyor.agents.agent-attest.internal
assessor -> https://assessor.agents.agent-attest.internal
warden   -> https://warden.agents.agent-attest.internal
executor -> https://executor.agents.agent-attest.internal

An admission record, which anyone can read with no account and no credentials:

imageDigest         sha256:7ed646755d8d02338baac93d614c487757495c8da320e54c96a30ba7199d604f
platform            GCP_AMD_SEV
swname              CONFIDENTIAL_SPACE
dbgstat             disabled-since-boot
admitted            true
lastAttestedDigest  sha256:7ed646755d8d…    (matches, heartbeat healthy)
attestationFailures 0

Then deploy/40-drift-demo.sh redeploys one agent with different code. Same name, same service account, same registry entry, and a build nobody approved:

--- Google Agent Registry: STILL IDENTICAL. Nothing here changed. ---
 name: projects/agent-attest-2026/locations/us-central1/services/assessor
 registryResource: projects/746874807798/.../agentregistry-00000000-0000-0000-1e9d-7c2701a54ee6

--- agent-attest admission record ---
 revoked : True
 reason  : Presented sha256:4452b7b78c8fd38f… but was admitted as sha256:7ed646755d8d…
           The identity is unchanged and the registry entry is unchanged. The code is not.

Caught in about sixty seconds, unattended, on live infrastructure.

The video is that same run, live and unedited, in one take. The terminal, the project and the console are all the real ones, and the Confidential Space instances and the Cloud Run revision are both on screen. There's no cut between asking the question and getting the answer.

Don't take my word for it

The verifier has no DOM dependency and no Node-only dependency, so the same source that runs in my Cloud Run control plane also runs in a browser. web/ bundles it into a page where you paste an attestation token and recompute my verdict yourself, locally, against the pinned root. You don't have to trust my servers for any of it. The bundle is about 66 kB gzipped, measured by web/build.sh rather than guessed at. If you check it you'll get a number within a couple of hundred bytes of mine, because the exact figure moves between gzip implementations, which is itself a small lesson in stating precision you can't reproduce.

The admission records are world-readable in Firestore for the same reason.

I didn't write a separate browser implementation. Two implementations that agree would only prove I can write two things that agree.

How I built it

TypeScript control plane on Cloud Run, and it's the only service account in the project granted roles/agentregistry.admin. Python for the Agent Gateway authorization extension, because Google ships ext_authz SDKs for Python, Go and Java and only the Python one implements the authorization protocol. I'd have preferred to keep it in one language, but hand-rolling proto generation and H2C for a fail-closed security control on a deadline is the kind of decision that looks clever for two days.

ADK agents in Python on Confidential Space, an orchestrator plus remote A2A workers. Firestore for the records. Gemini 3.5 Flash through Vertex AI.

The attestation verifier came from an earlier project of mine and I reused it under MIT, with the registry binding rewritten from an on-chain lookup to Firestore. It's pure WebCrypto, no WASM, and it implements the offline PKI (x5c) path instead of fetching Google's JWKS. That's what lets the same code run in a browser without needing the network for any of the crypto.

Technologies used: Gemini 3.5 Flash via Vertex AI. Google ADK 2.6.2, an orchestrator plus three remote A2A workers. Google Agent Registry (agentregistry.googleapis.com). Google Confidential Space on AMD SEV. Agent Gateway with an Envoy ext_authz v3 authorization extension. Cloud Run, Firestore, Cloud Asset Inventory, Artifact Registry, VPC with an internal load balancer and private DNS. TypeScript on Node 22 for the control plane and verifier, Python 3.12 for the agents and the gateway callout, Docker, WebCrypto, Cloudflare Pages for the static verification page.

Other data sources used

Everything this system reasons about comes from Google Cloud or from the hardware. There are no third-party datasets, no scraped content, no external APIs and no user data anywhere in it.

  • The Confidential Space attestation service, reached over the launcher socket at /run/container_launcher/teeserver.sock. This is the only source of the claims that matter: image_id, hwmodel, swname, dbgstat and the launch policy.
  • Google's Confidential Space PKI root certificate, pinned into the build at compile time. Pinned rather than fetched, because a root you download at runtime is a root somebody else can change.
  • The live Google Cloud estate of the project itself. Surveyor reads it through Cloud Asset Inventory, which is why its only read grant is cloudasset.viewer. Executor writes through the Compute API. The orphaned disk in the demo is a real disk in a real project.
  • Google Agent Registry, read and written through agentregistry.googleapis.com.
  • Firestore, for admission records, the approved-digest policy and the attestation trail. World-readable by design.

Two tokens are committed under verifier/test/fixtures. One is real, minted by a TEE in this project, so the test suite and the demo page both work on a fresh clone with no Google Cloud account. The other is a deliberately forged chain the tests use to prove the pinned root rejects it.

Against the Fortified Enterprise Fleet component list

The track names seven platform components. Here's what I actually used, including the ones I didn't, because a checklist answered honestly is worth more than one answered generously.

Component Used What I did
Agent Registry yes Four agents in the real registry, each there because an attestation verified. This is the centre of the whole project.
Agent Identity yes, as the subject The entry is an argument about what a SPIFFE ID does and doesn't prove. It proves where an agent runs. It doesn't prove what runs there.
Agent Gateway partly Real authzExtension and gateway resources, verified against Envoy's actual ext_authz v3 protos. The policy binding never completed.
Agent Observability partly Append-only, world-readable attestation trail with per-check results on every heartbeat, so an incident is reconstructable after the fact. It's a durable audit log. It is not OpenTelemetry instrumented and I'm not going to call it that.
Agent Runtime no The workload has to mint its own hardware attestation, which means it has to be the thing running inside the TEE. That rules out a managed runtime for the fleet. The control plane does run on Cloud Run.
Memory Bank no There is no exemption store. Warden's instruction tells it to check its memory before acting on a resource a human has exempted, and nothing implements that memory, which is a gap I found writing this up rather than one I designed in. The state that genuinely has to survive weeks is the admission and attestation record, and that's in Firestore. Memory Bank is where the exemption list belongs and it's the next thing I'd build.
Model Armor no Orthogonal rather than skipped. Model Armor guards what goes into and out of a model. Attestation guards what the code is. Model Armor stops an injection changing what an agent does; attestation stops a redeploy changing what an agent is. Neither substitutes for the other, and a fleet that wants to be trusted in production needs both.

Findings and learnings: five things that aren't in the docs

1. Google's own Agent Gateway samples set failOpen: true. The field defaults to false, and the reference is clear that a failure then stops request processing. But every authz sample in the Agent Gateway docs turns it on. Copy one and your authorization control quietly becomes a suggestion, because anything that knocks out the callout also switches off the check. An attacker can arrange that.

2. CONTENT_AUTHZ can't be made fail-closed at all. Under FULL_DUPLEX_STREAMED the docs say the proxy fail-opens up to the first chunk of body data, and connection and header failures fail open regardless of what you set failOpen to. Only REQUEST_AUTHZ gives you a clean guarantee, so that's what I used.

3. Agent Registry silently requires interfaces[].url to be globally unique. This isn't documented, and the check happens asynchronously inside the long-running operation, so if you treat the HTTP 200 as success you see nothing wrong. The first registration always works. I had a fleet sharing one image and one default port, so agent one went in and the other three vanished. It looked like a race condition for hours. Worse, three agents ended up holding admission records that pointed at registry entries which didn't exist, which is exactly the kind of quiet divergence this whole project is meant to catch. The control plane now polls the operation to completion.

4. Agent Gateway doesn't speak A2A. The protocols enum accepts MCP or PROTOCOL_UNSPECIFIED. ADK's inter-agent transport is A2A. So Agent Gateway can't currently sit in front of agent-to-agent calls in an ADK fleet, only MCP traffic. The Fortified Enterprise Fleet brief pairs "Agent Gateway for unified routing and policy enforcement" with "a scalable network of institutional agents", and those two things don't meet yet. I only found this because the import validator prints its whole JSON schema when you give it a bad document, which turned out to be the most useful error message on the platform.

5. A googleManaged gateway is describable long before it's usable. No state field, describe finds it straight away, and binding a policy fails with "not yet provisioned in the tenant project". Still failing four hours later.

Challenges

Confidential Space failures all look the same from outside, which is what made them expensive. The VM boots, reports RUNNING, writes nothing to Cloud Logging, and stops.

Three separate things caused that. A multi-platform image manifest, which the launcher can't resolve. An env var missing from the image's allow_env_override label, which gets the workload killed while the launcher logs Workload completed, so it reads like success. And google-adk[a2a] not declaring sse-starlette, so the app builds fine and then dies during Starlette's lifespan startup, after everything watching for a crash has decided it's running.

I stopped debugging in VMs after that. The launcher now checks the image is single-platform, diffs every env var against the launch policy, and boots the container locally before it spends a VM on it. Twenty seconds of docker run instead of a four-minute cycle that ends in silence.

One more that cost me an afternoon. A private Cloud Run service answers unauthorised callers with 404, not 403, which is deliberate so private services don't leak their existence. Pair that with status.url sometimes holding a stale hostname that gcloud run services proxy also reads, and you get a perfectly healthy service reporting Ready: True and 404ing on every URL you try.

What this doesn't do

These live in the code as STANDING_CAVEATS and REGISTRY_CAVEATS and get printed with every result, including the ones that pass. I didn't write them for this page.

  • Attestation proves which image ran. It doesn't prove the code is any good. A TEE running malicious code produces a perfectly valid attestation.
  • Pinning an image pins the code, not the behaviour. It says nothing about model weights or prompts pulled in at runtime from somewhere else.
  • Revocation isn't checked. The leaf has no CRL distribution point and the intermediate's CRL is served without CORS headers, so a browser can't fetch it. With 60-day leaves and a hard root pin the residual risk is a revoked intermediate. Rare, but not zero.
  • The gateway doesn't re-attest on every request. It can't, because only the workload can mint a token and the caller isn't the workload. It checks admission, revocation and freshness, and the digest comparison happens on the heartbeat. An agent that drifts inside the 300-second window stays reachable until it misses a beat.
  • Verifying in your browser means you don't have to trust the server running the workload. You do still have to trust whoever served you the JavaScript.

And on scope. The authorization extension is a real registered Google resource and I've verified it against Envoy's actual ext_authz v3 protos. The gateway exists too. But the policy binding never completed, so the gateway isn't enforcing anything right now. The decision it would serve is live at POST /authorize, and the callout holds no policy of its own, so binding it later needs no code change. I'd rather say that than imply an enforcement path I haven't actually exercised.

What's next

Bind the policy when tenant provisioning finally goes through. Report the failOpen sample defaults and the interfaces[].url uniqueness constraint to Google as doc issues. Shorten the freshness window, which only costs Firestore writes. And if Agent Gateway picks up A2A, the same extension covers inter-agent hops with nothing to change.

Built With

  • a2a
  • agent-gateway
  • agent-registry
  • amd-sev
  • artifact-registry
  • attestation
  • cloud-run
  • confidential-computing
  • confidential-space
  • docker
  • envoy
  • firestore
  • gemini
  • google-adk
  • google-cloud
  • grpc
  • node.js
  • python
  • supply-chain-security
  • tee
  • typescript
  • vertex-ai
  • webcrypto
  • zero-trust
Share this project:

Updates

Submission history