Inspiration

GitLab Duo agents can now run in the background, started by a trigger or the Flows API, with nobody watching. GitLab's tool governance gives a background flow two choices per tool: Always Allow or Always Deny. Always Ask cannot apply, because no one is there to answer. In my first spike, a session started by a trigger (9295846) came with all seven agent privileges pre-approved. I wanted evidence about each change to decide how much an agent may do, how deeply its work is checked, and how carefully it ships.

What it does

Governor gives every change a risk tier from T0 to T4 with a deterministic scorer: paths touched (auth, secrets, CI, infrastructure, the governor itself), size, new security findings, coverage, and the rollback history of those paths. No LLM is part of that decision. The tier sets:

  • The agent's privileges: the developer set for T0 to T2, a restricted catalog flow with no shell and no git for T3, and nothing for T4.
  • The pipeline depth: lint only at T0, up to SAST, secret detection and dependency scanning at T2 and T3.
  • The release: direct, a 25% or 10% canary, or a feature-flag dark launch at 1% with a 20-minute soak.

After a person opens an issue, Governor runs triage, delivery, verification, merge, release and close on its own. On an SLO breach it:

  • rolls back and turns the flag off;
  • opens an incident issue with the failing requests;
  • starts a fix flow at the tier of the broken files.

Every decision goes into a hash-chained ledger with a public page.

How I built it

Governor is a Python (FastAPI) service on Cloud Run. It starts every agent hop itself through the Flows API:

  • developer/v1 for T0 to T2;
  • three custom AI Catalog flows: triage, T3 restricted, and remediation for security findings.

The T3 flow's YAML toolset is its boundary: read files and GitLab, then create a branch, a commit and an MR through the API. I tested a Work item Created trigger and kept it disabled, because triggered sessions start with every privilege pre-approved.

GitLab CI reads the tier as RISK_TIER and runs the matching jobs; main always runs everything. CI builds both images, signs them with cosign keyless through Workload Identity Federation, and attests a Syft CycloneDX SBOM. It then deploys a no-traffic revision and records it in a GitLab environment.

A project webhook and a one-minute Cloud Scheduler tick wake Governor. GitLab Feature Flags drive the dark launch. Both services export OpenTelemetry to GitLab Observability.

Google Cloud: Cloud Run, Firestore (the ledger), Cloud Scheduler, Secret Manager, Artifact Registry, IAM signBlob (signed T4 records) and Workload Identity Federation, in europe-west9, provisioned with OpenTofu.

Challenges I ran into

  • The Flows API returned 404 when I sent issue_id with developer/v1 (issue #4). My client dropped the error body, so Governor repeated the same request every minute. Now no flow gets issue_id, retries back off and stop after three failures, errors are recorded, and request shapes are checked before a run.
  • The first release failed with HTTP 403 (issue #5). Moving Cloud Run traffic needs Artifact Registry read access, which Governor's service account lacked. I granted it and resumed the run myself; that resume is in the ledger. Preflight now tests release permissions as Governor's own identity.
  • A dark launch was promoted while the app could not read its flag (commit 592fb22d). The stored Unleash instance ID ended in a newline, so the flag read as off, and 200 healthy probes proved nothing. I turned the flag off and recorded that as a safety action. Now the canary must report the flag on before the soak counts, and a test replays that run.

Accomplishments that I'm proud of

  • Gate 3 run 3: issue #6 went from creation to production in 31 min 22 s, with no human steps. Later, with webhooks, issue #12 took 15 min 24 s.
  • Gate 4 run 3: a labelled, injected regression in the login path was scored T3 and dark-launched at 1%.
    • The canary showed the flag on, then 3 of 20 probes failed. Governor rolled back 2 min 59 s after the canary started, in the same tick as the breach.
    • It opened incident #11. The restricted T3 flow wrote the fix, which went live 31 minutes after the breach, with no human steps.
  • The T4 refusal: MR !4 removed the SAST include. Governor refused it at T4 with a signed record, 4 seconds after it was opened.
  • Proof anyone can check: one command each verifies the ledger's hash chain, the signed decision records, and the image signatures with SBOM attestations.

What I learned

  • Healthy probes can't tell "the new code works" from "the new code never ran".
  • An agent's boundary is what the platform enforces, not what you request. Catalog flows ignored the privileges I asked for, so their toolset became the boundary.
  • Measuring carbon honestly matters. Tiering cut pipeline carbon by 12% on this project's real changes: 88% for docs changes and 29% for small features. T2 and T3 save nothing, because that is where the risk is.

What's next for Governor

  • A T3-only CI job, such as an independent review flow, so T3 buys more checking than T2.
  • Running under a project service account instead of my personal access token.
  • Asking GitLab whether catalog flows should honour requested privileges.
  • Hourly grid intensity for SCI.

Built With

  • artifact-registry
  • cloud-scheduler
  • cosign
  • cyclonedx
  • fastapi
  • firestore
  • gitlab
  • gitlab-ci
  • gitlab-duo-agent-platform
  • gitlab-feature-flags
  • gitlab-observability
  • google-cloud-run
  • hypothesis
  • opentelemetry
  • opentofu
  • pydantic
  • pytest
  • python
  • secret-manager
  • semgrep
  • sigstore
  • syft
  • unleash
  • uv
  • workload-identity-federation
Share this project:

Updates

Submission history