Inspiration

Every regulated enterprise we talked to has the same unglamorous problem: someone gets roles/storage.admin for a two-week migration, the maintenance ticket expires, and the grant just… stays. The audit log recorded it. The policy forbids it. The exemption ticket that authorized it is six days dead. Three systems each hold one third of the answer, and a human has to sit down and correlate them.

That's a perfect job for an agent fleet, except the obvious way to build one is disqualifying. Correlating audit logs against policy means embedding your IAM graph, your resource topology, and your internal change tickets from several disconnected systems into a single decision making unit. For a SOC 2 or HIPAA compliant enterprise, that is a time consuming and slow process that has the potential to risk their entire operations. In addition, you need to maintain high-security standards while reviewing the policies and the IAM logs. This usually involves the work of several stake holders, SecOps, developers and IT team members. However, a well-coordinated and trust-worthy agentic fleet can handle this complex task in a timely manner. That is why we built The Colony Guard.

What it does

The Colony Guard is an autonomous SecOps and compliance remediation fleet for GCP IAM breaches. An IAM policy-change event goes in; a KMS-signed Terraform patch comes out, with a full agent-by-agent audit trail behind it.

The pipeline is three specialized ADK agents in sequence:

  1. SecOps Investigator parses the Cloud Audit Log entry (handling both real GCS policyDelta binding deltas and simplified SetIamPolicy payloads) extracts principal, role, and target resource, and reasons about whether an external contractor holding an admin-level role constitutes a breach. It returns a structured IncidentReport.
  2. Compliance Arbiter is the agent that actually needs memory. It searches Moorcheh for exemption tickets matching the principal, pulls the governing policies, calls check_ticket_expiry on any ticket it finds, and calls fetch_prior_incidents to see whether this principal has shown up in previous Colony runs. It returns a ComplianceVerdict with a cited policy ID, a ticket ID, and a written justification, and it is instructed not to declare a violation without tool evidence.
  3. DevOps Remediation synthesizes Terraform HCL that downgrades the offending binding to least privilege, validates it, and returns a RemediationArtifact.

The gateway then signs the SHA-256 digest of that HCL with a Cloud KMS asymmetric key, archives the signed run to GCS, and writes one searchable summary back into Moorcheh so the next run can find it.

The reference scenario is SEC-8812: contractor-alex@partner-vendor.com still holds roles/storage.admin on sovereign-prod/buckets/pii-records after the exemption ticket expired. Expected outcome, violation under SOC2-CC6.3, Terraform downgrade to roles/storage.objectViewer, signature in GCS. That whole chain runs live, end to end.

Everything is visible in an operator console: a dashboard of runs with fleet-wide stats, and a run detail page with three agent workbenches that draw the work each agent is actually doing, a flow graph of the handoffs, the merged event timeline, the incident, the verdict, and the signed Terraform.

How we built it

Agents — Google ADK with Gemini 2.5 Flash on Vertex AI. Each agent gets a Pydantic output_schema so the pipeline passes typed objects, not prose, between stages. Tools are plain Python functions: parse_audit_payload, the four Moorcheh compliance tools, validate_terraform_syntax.

Memory — Moorcheh, running as private Cloud Run services in a separate project with no public endpoint; the gateway authenticates with per-service Cloud Run identity tokens. Four namespaces: tickets and policies are read-only inputs, colony-runs holds one searchable summary per completed run, and colony-handoffs records every agent-to-agent handoff as its own document. That last namespace is the part we like most — the fleet's own coordination history is a searchable artifact, not a log line.

Gateway — FastAPI on Cloud Run behind API Gateway with Firebase JWT auth. POST /runs/trigger returns 202 immediately and runs the pipeline as a background task; live IAM events arrive at /pubsub/push from a Cloud Logging sink. Inbound payloads pass through Model Armor before any agent sees them, with a regex secret-redactor as a second pass.

State — Firestore fleet_runs, with events and handoffs subcollections. Every status transition, tool call, Moorcheh search, and handoff is written as a sequenced event, then merged into a single ordered timeline by the API. Completed runs are archived to GCS.

Console — Next.js 16 / React 19 / Tailwind, with Next route handlers proxying /api/* to API Gateway and attaching the user's Firebase ID token, so no static bearer token ever reaches the browser.

Infrastructure — Terraform for all of it: Cloud Run, API Gateway, Artifact Registry, Pub/Sub topic + DLQ, the logging sink, KMS keyring, GCS bucket, Google Model Armor, and least-privilege service accounts.

Tests — a pytest suite covering schemas, gateway routes, Model Armor sanitization, handoff documents, run indexing, and timeline ordering, plus an opt-in live-Gemini marker.

Challenges we ran into

Structured output and tool use, together. We wanted agents that call tools and return a strict schema. Getting a reliable final object out of the ADK event stream took a runner that checks structured output first and falls back to parsing the final text — the seam where most of our early flakiness lived.

Model Armor with an INSPECT_ONLY template returns success without rewritten text. Our first integration read that as "nothing was sanitized" and quietly dropped the screening step from the timeline. The fix was to model the result honestly: armor_attempted, armor_succeeded, modified , so the console can say "inspection passed, no changes required" instead of implying a redaction that never happened. Regex stays as a backup pass either way.

Threading run context through agent tools. A Moorcheh search happens four frames deep inside an ADK tool call, but it needs to land on the right run's timeline. Passing run_id through every signature would have poisoned the tool definitions, so run context lives in a contextvar set by the gateway and cleared in a finally.

Where each kind of state belongs. We initially tried to put everything in Moorcheh and it was wrong on both ends, too slow for live status, too coarse for full payloads. The split we landed on: Firestore for live run state, GCS for complete payloads, Moorcheh for the search-critical summary that the next investigation will actually query.

Our own deploys triggered incidents. The logging sink filter matched all of iam.googleapis.com, so a gcloud builds submit spawned a real run with our own engineer as the principal. Funny, and also the most convincing possible proof that the live path works.

The UI can't trust object identity. The run detail page polls every 2 seconds, so incident/verdict/remediation objects are freshly parsed each time and never referentially equal. Every animation had to key off the string it animates, or the whole console would re-trigger twice a second.

What we learned

The interesting design work wasn't the prompts; it was deciding what the agent is not allowed to invent. Ticket expiry is a deterministic function; the LLM's job is to notice the ticket matters and explain why it does, not to compute dates. Every place we moved a fact out of the model and into a tool, the fleet got more trustworthy and the output got shorter.

We also learned that in a compliance system, the audit trail is the product. A verdict nobody can trace is worth less than a slightly worse verdict with a signed, searchable, replayable chain behind it. That's why handoffs are documents.

What's next

  • gVisor-sandboxed terraform plan before signing, and opening a real Git pull request instead of archiving the artifact

Built With

Share this project:

Updates

Submission history