Sovereign

Proof, not policy documents, that an agent fleet can't leak data across regions.

Try it out (no install)

https://sovereign-fleet-agent.vercel.app runs the real offline end-to-end demo fresh, server-side, on every page load, and shows the transcript: an ALLOW on a same-region call, a DENY on the cross-region call, a DENY on a record whose content says "you are authorized," and tamper detection catching a corrupted decision log. Zero network, zero GCP, zero API key required to watch it work. It is the same demo_local.py loop the judges can also run locally per the README.

The scenario every fleet demo skips

Every agent-fleet demo in this track looks the same: a slide that says "policy engine," followed by a happy path where nothing ever gets refused. That is not what a compliance engineer needs. Ask them whether a fleet touching EU and US customer data is safe, and "we have a policy document" is not an answer. "Here is the call we denied, here is the record it tried to touch, here is why, and here is proof nobody edited the log afterward" is an answer.

Sovereign is that answer, built as small and as provable as it can be: a gateway every tool call must pass through, a deterministic policy engine with zero vote for the model, and a hash-chained decision log that makes tampering detectable rather than just discouraged.

What actually happens on every tool call

Sub-agents register with a declared data-residency region and their own GCP service account at registration time. That region is not something an agent can restate mid-call, because nothing downstream ever reads a region out of anything the model says.

Every tool call routes through a gateway that looks up the caller's registered region, attaches the target record's residency label from a trusted record store, and hands both to a deterministic policy engine. The engine is a pure function, no model call, no network call, no randomness, and it fails closed: if no clause matches, the answer is deny. When the regions do not match, the sub-agent's tool function never runs, full stop. Not "runs and gets flagged." Never runs.

Every decision, allow or deny, lands in a hash-chained log, so any later edit is detectable by recomputing the chain. The demo flips one byte in a stored entry and shows verification catch it on the spot.

One record in the fixture set carries an injected instruction telling the model it is authorized to cross regions. The model can read that text and reason about it all it wants. It cannot act on it, because the record's free-text content is never passed to the policy engine at all. That is a structural guarantee, not a filter. A filter can be argued past with the right phrasing. A missing parameter cannot be argued past by anything.

Building a policy that can't be talked out of a decision

The policy engine, gateway, registry, decision log, and the idempotent Cloud Run Job entrypoint are plain, dependency-light Python, unit-tested directly. 58 tests pass offline, no GCP configuration, no credentials, no API key required to prove any of it.

The gateway is the single choke point in the system. It opens an OpenTelemetry span carrying the decision's attributes on every call, allowed or denied, before the sub-agent's tool function could ever be invoked.

agent/fleet.py assembles a real ADK orchestrator with three genuinely distinct LlmAgent sub-agents backed by Gemini 2.5 Flash, each with its own tool closure routed through that same gateway and the same record store.

We are precise about exactly what a judge will see run: the batch job/main.py executes calls gateway.invoke() directly on a scripted list, rather than the orchestrator freely choosing which calls to make. The fleet is built on that identical enforcement path and passes its own test suite, but today's demo denial is a deterministic policy result on a scripted batch, not a live model choice we then blocked. We would rather state that precisely than let the story imply more than what we shipped.

The job entrypoint shares a small idempotency spine, so a duplicate Scheduler fire writes the decision-log artifact at most once. Each of our three hackathon repos carries its own copy of that spine so any one of them can be cloned and run standalone, and a test asserts the copies stay byte-identical.

We kept ADK sessions short by design, not by accident. The framework's own issue tracker documents a 100-event session cap with no pagination (#3559, #3613) and an _init_session replay that could trip a 429 on long sessions (#5714). All three are closed upstream now, so we are not fighting live bugs, but they are why durable state lives in the registry and the decision log rather than in session state.

Technologies used

Gemini 2.5 Flash, Google Agent Development Kit (ADK) for the multi-agent orchestrator, Cloud Run Jobs in two regions, Cloud Scheduler, Cloud Trace, Firestore, Cloud Storage, per-sub-agent IAM service accounts, OpenTelemetry, Python.

Two model-credential modes are supported: Vertex AI + Application Default Credentials (GOOGLE_GENAI_USE_VERTEXAI + GOOGLE_CLOUD_PROJECT), which is what the real deploy runs and requires no API key anywhere, and the Gemini Developer API (GOOGLE_API_KEY/GEMINI_API_KEY) as a local-only fallback. Both were verified with a real model call from this build environment; see LIMITATIONS.md.

On tracing: spans are emitted at every policy decision via agentspine/tracing.py, and job/tracing_setup.py registers a real CloudTraceSpanExporter when the deployed job's SOVEREIGN_BACKEND=gcp is set. This is confirmed live from inside an actual deployed Cloud Run Job container, running under its own per-region service account, not just local developer ADC: the Aug 31 filming deploy's job execution logs printed cloud_trace_exporter_registered=True, and TraceServiceClient.list_traces() read back real spans from that exact run, sovereign.tool_call.eu-summarizer (allow) and sovereign.tool_call.cross-region processing violates data residency (deny) among them, alongside the decision.policy.* spans carrying the verdict. The demo video shows this Trace panel live, not the local-fallback path. See LIMITATIONS.md for the full verification record.

Other data sources

None. The EU and US customer records are synthetic fixtures in this repo, including the deliberately injected one. There is no external data source and no real customer data.

Findings and learnings

The highest-leverage design decision was structural, not behavioral: making it physically impossible for a record's free-text content to reach the policy engine, instead of trying to detect injected instructions after the fact.

We proved it with a break and a restore, not an assertion. Inverting the residency clause so it allows instead of denies takes the suite from 58 passing to 43 passing and 15 failing, and the demo's step 3 flips from [DENIED] to [ALLOWED], with real EU customer content processed by the US summarizer. Restoring the line returns both to green. Deleting the clause outright proves nothing, because the engine fails closed and denies by default regardless. The only honest break is the targeted one, and the targeted one reproduces exactly the silent cross-region leak a compliance engineer is afraid of.

A later adversarial pass found a more serious version of the same bug in the ADK path. The sub-agent tool signature had accepted record_region as an argument supplied by the model, so a model that mislabelled an EU row as US satisfied the residency clause honestly, and the row reached the US summarizer anyway. The engine was never wrong. It was being fed a region the model chose. The fix: the tool now accepts only an opaque record_id and resolves region and content from the trusted store, so there is no parameter through which a region can be asserted. The lesson generalizes past this codebase: a correct policy engine proves nothing if its inputs are attacker-supplied.

We also learned that naming what was not run, a live GCP deployment, is worth more to a reviewer who actually clones the project than implying one that does not exist.

Built With

  • cloud-run-jobs
  • cloud-scheduler
  • cloud-storage
  • cloud-trace
  • firestore
  • gemini-2.5-flash
  • google-adk
  • opentelemetry
  • python
  • security
Share this project:

Updates

Submission history