Glass Box puts AI agents into a GitLab delivery pipeline and wraps them in checks, so a human keeps the final say. One agent reviews each change and tries to break it. A CI gate reads that review and decides how much human approval the change needs. A second agent writes up what a person still has to decide. The autonomy level is Assisted: the agents propose, and a person decides.
Inspiration
I've contributed to open source, including CBOMkit's SonarCryptography scanning, where I helped add C/C++ and JavaScript/TypeScript support. On every team I've seen, writing the code was never the only slow part. Review, security scanning, approval, deployment and the check that production is really healthy are where the time goes. They're also where trust, more than effort, limits what you can hand to a machine.
Most agent demos show an agent writing a fix. I wanted to build the thing that stands next to it: a gate that reads what the agent says, decides how much a person still needs to check, and leaves a link for every step.
Who it's for
Small teams that want AI help reviewing and shipping code, but don't want an agent approving or merging its own work.
What it does
When a merge request is opened, I ask the Skeptic to review it. It reads the diff, tries to break the change, and posts up to five ways it could fail, ending with a RISK: low, medium or high line.
risk-gate, a CI job, reads that verdict. It only trusts a note from the Skeptic's own service account, and it prints how much human approval the change needs. Then I ask the Reporter for a status note that quotes the verdict and lists what a person still has to decide. I merge.
After the merge, the pipeline runs the tests, builds a container image and an SBOM, runs GitLab's SAST, secret detection and dependency scanning, and deploys to Google Cloud Run with Workload Identity Federation, so there is no stored key. A smoke test then checks the live service, and a schedule runs it again every hour.
Why this matters
Unit tests only catch what someone thought to test. I made eight small seeded changes to the app: five that introduce a real problem and three that are harmless. My 10 unit tests failed on three of the five unsafe changes. Two passed every test: removing the cap on stored links, and generating short codes with Math.random. Those are the changes a second reader, a person or an agent, is there to notice.
I have run the Skeptic on two of the eight so far. On the removed link cap (which passes all 10 tests), it named the problem as its first finding, unbounded memory growth, and rated the change RISK: high. On a pure rename it found no behavior change and rated it RISK: low. The other six are not run yet, so this is two data points, not a score. The changes and a script that reproduces the test results are in the repo's benchmark/ folder.
How it works
┌──────────────────────────────────────────────┐
│ 1. Merge request │
│ I open it and mention the Skeptic │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 2. GitLab Duo Agent Platform (comments only) │
│ Skeptic: reads the diff, tries to break it, │
│ ends with RISK: low, medium or high │
│ Reporter: writes what a human must decide │
└──────────────────────────────────────────────┘
│ verdict as an MR note
▼
┌──────────────────────────────────────────────┐
│ 3. risk-gate (CI job) │
│ reads the Skeptic's verdict, prints DECISION │
│ fails the pipeline only on RISK: high │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 4. A person decides and merges │
└──────────────────────────────────────────────┘
│ merge to main
▼
┌──────────────────────────────────────────────┐
│ 5. Pipeline on main │
│ tests > image + SBOM > SAST, secrets, deps │
│ > deploy to Cloud Run (no stored key) │
└──────────────────────────────────────────────┘
│ after the deploy
▼
┌──────────────────────────────────────────────┐
│ 6. smoke-prod │
│ checks the live service after each deploy │
│ and every hour (schedule runs only this) │
└──────────────────────────────────────────────┘
A hash-chained log (evidence/log.jsonl) records notable events. A script writes it, and it isn't part of the live flow.
Who does what
| Who or what | What it does | What it can't do |
|---|---|---|
| Skeptic (my Duo custom flow) | Reads the diff and posts a review ending in a RISK: line |
Merge or edit anything |
| Reporter (my Duo custom flow) | Posts a status note quoting the verdict and listing what a person must decide | Merge or edit anything |
risk-gate (CI job) |
Reads the Skeptic's verdict and prints a DECISION; fails only on RISK: high |
Judge the code itself |
| Planner Agent (GitLab's own) | Planned issue #4 and, after I approved, created child tasks #5 to #8 | It isn't my code |
| Fix CI/CD Pipeline flow (GitLab's own) | Diagnosed a failed job and suggested a retry | It changed no code |
| Me | Open the merge request, mention the agents, approve, merge |
Why Assisted
In my tests a person mentioned the agent to start it, and a person merged. The agents propose and report, and I approve each change. I haven't shown that a schedule or a bot can start a flow without a human, so I'm not claiming a higher autonomy level.
How I run a review: I open a merge request, comment @ai-skeptic-gitlab-ai-hackathon please review, retry risk-gate if it ran before the review arrived, comment @ai-reporter-gitlab-ai-hackathon please report, then decide and merge.
The nine stages
| Stage | Where |
|---|---|
| Plan | GitLab's built-in Planner Agent planned issue #4 and created child tasks #5 to #8 |
| Create | app/, flows/, merge requests (for example MR !6) |
| Verify | unit-tests, infra-validate, the Skeptic |
| Package | package-image, generate-sbom |
| Secure | sast, secret_detection, dependency_scanning |
| Release | deploy-cloudrun, rollback-cloudrun |
| Configure | infra/*.tf, Workload Identity auth in .gitlab-ci.yml |
| Monitor | smoke-prod and the hourly schedule |
| Govern | risk-gate, flows/reporter.yaml, policy/autonomy.yml, evidence/log.jsonl |
Verify it in 5 minutes
No login is needed for the live service. Each row is one claim and one link.
| Claim | What you'll see | Link |
|---|---|---|
| The service is live | {"status":"ok"} |
/health |
| The Skeptic found a real bug | HEAD requests counted as visits; fixed in 6c0821b |
MR !6 |
| The injection test | A hostile comment, the Skeptic's refusal, RISK: medium |
MR !14 |
| The Skeptic on a seeded change | The removed link cap, named as unbounded memory growth, RISK: high |
MR !24 |
| The benchmark | Eight seeded changes; my tests miss two of the five unsafe ones | benchmark/ |
| Keyless deploy works | deploy-cloudrun passed |
pipeline 2950729078 |
| Post-deploy check works | SMOKE PASS |
job 17199574841 |
| Infra check works | infra-validate passed |
MR !13 |
| Planning happened | Child tasks #5 to #8 | issue #4 |
| Auto-diagnosis | A timeout called a network failure, retry suggested | session 9530923 |
How we built it
The app is a small Node.js URL shortener with no dependencies. It's the stage for the automation, not the point. It has unit tests, a non-root Docker image, and a .gitlab-ci.yml for the pipeline above. risk-gate is a short Python script with its own tests. OpenTofu creates the Cloud Run service (capped at one instance), an Artifact Registry repo, two service accounts and the Workload Identity setup.
I used Claude Code to write much of the code, the scripts and the CI config, and other AI tools (Codex and DeepSeek) while brainstorming and planning. The GitLab Duo agents do the review and the reporting inside the pipeline. I did the GitLab and Google Cloud setup and every approval myself, and I checked each claim on GitLab before writing it into STATUS.md.
Design decisions
| Decision | Why |
|---|---|
| The agents can't merge or edit | Both flows only post a comment. I wanted the approval to stay with a person, so they never got a merge tool |
| The gate trusts one author | risk-gate only accepts a verdict from the Skeptic's own service account, so a copied "RISK: low" from anyone else is ignored |
The gate fails only on RISK: high |
A low or medium verdict prints a decision but doesn't block the pipeline. I wanted the gate to guide a person, not replace one |
| Keyless deploys | The Workload Identity provider only accepts tokens from this project's main branch. The deploy account has developer rights on this one Cloud Run service, write access to one Artifact Registry repo, and permission to act as the runtime account |
| One instance, request-based CPU | State lives in memory, so the service is capped at one instance. 256 MiB of memory needs request-based CPU on Cloud Run |
| The schedule runs only the smoke test | Scheduled pipelines switch the other jobs off, so the hourly check stays cheap |
Built with
| Layer | Tools |
|---|---|
| Agents | GitLab Duo Agent Platform: two custom flows, plus the built-in Planner and Fix CI/CD agents |
| Pipeline | GitLab CI/CD, SAST, secret detection, dependency scanning, an SBOM |
| Cloud | Google Cloud Run, Artifact Registry, Workload Identity Federation |
| Infrastructure | OpenTofu |
| App and scripts | Node.js, Python, shell |
| Coding help | Claude Code |
Challenges we ran into
Learning GitLab itself
GitLab itself was the hardest part at first. I hadn't worked with merge request pipelines, protected branches, custom flows or Duo's triggers before. The docs for the general flow framework and for custom flows disagreed, and my first YAML was rejected for fields the general docs call optional. I mixed up a branch pipeline with a merge request pipeline: on MR !13 the branch pipeline failed on a runner timeout while the merge request pipeline passed, and it took me a while to see they were two different things. I also didn't expect the gate to run before the Skeptic had commented.
What went wrong
| What happened | What I did |
|---|---|
risk-gate ran before the Skeptic had commented on MR !14 and printed DECISION: no Skeptic review yet. It doesn't run again when a review arrives |
Retried the job by hand |
I started rollback-cloudrun and cancelled it after 48 seconds. It had already moved traffic back one revision |
Fixed it with gcloud run services update-traffic. I never saw the job finish |
A Reporter session ended as failed (dropped) |
A retry worked. Nobody knows why it dropped |
| My first infra config was rejected: "Total memory < 512 Mi is not supported with cpu always allocated (unthrottled)" | Switched the service to request-based CPU |
On Oct 22 I merged a deliberately unsafe route (eval on a query parameter) into main, trying to trigger a RISK: high verdict. It deployed as revision 00010 and ran about 30 minutes |
Reverted it. The logs show three requests to it, all with the harmless value expr=1, from one device. I should have used a throwaway branch |
I also haven't shown that a schedule or a bot can start a flow. In my tests a human mention started both agents.
Limits
| Limit | Detail |
|---|---|
| The gate trusts the Skeptic | risk-gate follows the Skeptic's verdict. A prompt injection that changed that verdict would change the gate's decision, so the person who merges is the backstop |
| Deploys move all traffic at once | deploy-cloudrun sends 100% of traffic to the new revision and then runs the health check. If the check fails, the new revision is already serving and rollback is manual |
| The link store can be filled | POST /shorten is public with no rate limit, so a client can fill the 10,000-link store, and it stays full until a restart |
| In-memory state | Links and visit counts are lost on a restart, so the service is capped at one instance |
| Public stats | GET /stats/:code has no login, and bot visits are counted (HEAD is not) |
| The SBOM job is not pinned | generate-sbom uses aquasec/trivy:latest, so the result can change between runs |
| The Skeptic can read widely | Its only write is one comment, but its read tool can reach whatever its service account can read |
| Infrastructure state is local | OpenTofu state lives on one machine, not in a shared backend |
| The evidence log | It is backfilled and not tamper-proof: someone who can rewrite the whole file can recompute the chain, and cutting lines off the end goes unnoticed |
| The injection test | One case against two agents. It's not proof they resist injection |
| Scope | I've only run this on one app |
Accomplishments that we're proud of
- A real bug caught before merge. On MR !6 the Skeptic found that HEAD requests were being counted as visits. I fixed it, and on production a HEAD request now leaves the count at zero while a normal request raises it to one.
- A reproducible gap in my own tests. Two of five seeded unsafe changes pass all 10 unit tests, and a script in
benchmark/reproduces that. On one of them, the removed link cap, the Skeptic named the problem and rated itRISK: high. - An injection test that held up once. I added a hostile comment aimed at AI reviewers to MR !14. Neither agent acted on it, and the Skeptic returned
RISK: medium. After a retry,risk-gateprintedDECISION: human approval required (RISK: medium). - Keyless deploys. The pipeline deploys to Cloud Run from GitLab with no stored Google credential.
- Every step leaves a link. Almost every claim in this write-up points at the merge request, pipeline or job that proves it.
- Honest limits. The project says what it can't do, and what went wrong, in the same place it says what worked.
What we learned
About the agents
- A
RISK: lowverdict doesn't mean there are no bugs. The Skeptic rated MR !6 low while it was catching a real one. - Agent flows can fail with no clear reason, so a retry plan is part of the design.
- Check the schema before writing a flow. GitLab's custom-flow rules rejected fields that the general framework docs list.
About GitLab and the Duo Agent Platform
- Once I understood the merge flow, I came away impressed by how much sits in one place. A merge request, its pipeline, the agents' comments, the jobs and the deploy all link to each other, so a claim can point at the exact thing that proves it. I didn't have to wire that together myself.
- Duo flows run as their own service accounts, and every run is a visible session linked to a CI job. That made it possible to build a gate that only trusts one author, and to keep an audit trail without extra tooling.
- Triggering an agent by mentioning it on a merge request fits the way review already works. It also kept a person in the loop, which is why this is an Assisted project.
- GitLab's built-in agents are useful on their own. The Planner Agent asked for my approval before it created tasks, and the Fix CI/CD Pipeline flow diagnosed a failed job and recommended a retry instead of changing code.
- Keyless deploys with GitLab's identity tokens and Workload Identity Federation meant I never stored a Google credential. That was worth the setup time.
What's next for Glass Box
These are not done yet.
- Run the Skeptic on the other six seeded changes, and report the full result.
- Turn the two seeded changes my tests missed into unit tests.
- Deploy new revisions with no traffic until the health check passes.
- Wire the evidence log into CI so it isn't appended by hand.
- Confirm the hourly schedule runs with nobody touching it.
- Make
risk-gaterun again when a late review arrives. - Try the loop on a second project.
Links
Built With
- agent
- agents
- ci/cd
- claude
- devsecops
- docker
- duo
- infrastructure
- javascript
- node.js
- python
- sast
- sbom
- terraform
Log in or sign up for Devpost to join the conversation.