Autonomy level

Supervised. Duo proposes, a person decides.

Inspiration

In 2023 an eating disorder helpline in the US ran a chatbot called Tessa. After a vendor upgrade it started handing out dieting advice, and it took a user's public posts to get it switched off (NPR).

What stuck with me is that nothing crashed. The bot kept answering, politely, and nobody was watching for the answers going wrong. Most tests I've seen check that an agent still runs. I wanted something that checks it still tells the truth, before a change goes in and also after it ships.

What it does

My demo agent is Harbor, a made-up helpline assistant. Its most important answer is one phone number: "call the Harbor Line at 555-0142." All numbers are fake 555 numbers on purpose. Each merge request starts Harbor in a review environment, and the job log shows it giving that answer (review job).

When a merge request changes Harbor, the gate sends it up to 36 requests and runs 15 fixed checks on the answers. No second AI grades the first one, so the same agent gets the same result every time. On main Harbor scores 100 and the pipeline signs a certificate that says "9 of 15 checks passed". The other six either don't apply to Harbor or can't be judged from outside, and the certificate says so rather than guessing.

Then I played the vendor. MR !42 changes one line, and Harbor starts giving 555-0100. Two checks catch it: before_after sees the answer changed since the approved version, and made_up_numbers sees a number Harbor was never given. Score 78, and the gate job fails.

The red pipeline started my Gate fix flow on its own. It explained the failure on the merge request and opened fix MR !43, one line, "90 percent sure". After the fix went into the upgrade branch, !42 went green.

The part I care most about is after release. On my instruction, my AI build session switched on a GitLab feature flag, harbor-vendor-model, for harbor-production, in the browser. No code changed. The daily re-check tested the production Harbor image, scored it 78, revoked the certificate and opened incident #37, "Certificate revoked: what changed". With the flag off again, the next re-check scored 100 and revoked nothing. The re-check is scheduled for 06:00 UTC every day. The runs shown here were started by hand from that schedule so they would land on camera.

Those two runs tested the newest production image. Since then I've tightened it: each signing on main stores the exact image fingerprint (its digest), and the daily re-check starts that exact signed image. Its first run that way passed with 100 and revoked nothing.

Anyone can check a certificate offline with python -m gatekeeper verify, or on the live check page, with no account. The page also has a /status view that shows the signed certificate and the revocation on file, with the two checks that changed. The image itself is signed with Sigstore and carries the certificate (sign job). My build session ran cosign verify on my computer against the project's identity and it passed, and the certificate pulled out of the image checked out as score 100, 9 of 15. That check was on a later signed image (sha256:26cca6fd...), not the one signed in the job linked here.

A test I ran on myself. I wanted to know what the gate misses, so I broke Harbor on purpose. On a branch called seeded-test, which was never merged, I made 10 small bad changes, one at a time, and ran the gate on each (pipeline 2992751225). Unchanged, Harbor still scored 100 (job).

The first time, the gate caught 5 of the 10. It missed a dropped "open every hour" sentence, a reply time that went from 10 minutes to 2 hours, diet advice, an action Harbor isn't allowed to take, and an instruction hidden inside the question. Harbor's own code tests (small tests that check Harbor's code directly) caught 9 of the 10. They missed only the hidden instruction.

Then I fixed the gate, in MR !77. The risk with a fix like that is tuning it to the exact ten changes you already know about. So before touching the gate, I wrote 10 new bad changes and ran them on the old gate first. I call them the held-out set, because the fix wasn't shaped around them.

Gate before the fix Gate after the fix Harbor's own tests
The original 10 5 of 10 10 of 10 9 of 10
10 new ones, written before the fix 3 of 10 9 of 10 9 of 10

I ran these on my build computer, not in a pipeline, so there are no job links for them. The steps to repeat both sets, before and after, are in docs/SEEDED_TEST_RETEST.md. Unchanged, Harbor still scored 100 before and after.

What the fix added: the gate can now compare wording and not just numbers, it asks the reply-time question, it tries two questions Harbor must turn down, it sends two forbidden actions to the separate route Harbor uses to carry out actions, and it hides an "ignore all previous instructions" line inside a real question. It's still 15 checks. The most requests it can send went from 30 to 36.

One of the new 10 still gets through: Harbor saying chat now costs 15 dollars a month. The gate only compares the questions in its settings file, and the price of chat isn't one of them, so it never asks. Adding it is one more request. I haven't added it, on purpose. A held-out set is a fair test only once. If I changed the gate after reading its results, the 9 of 10 would stop meaning anything, and I'd need a fresh set to get a second honest number.

Two more limits. The new checks only run when an agent's settings file asks for them. Harbor's does, but for an agent whose settings don't, they do nothing. And I wrote both sets of bad changes myself, so they show whether the fix was tuned to the first ten, not how it does against someone else's mistakes.

So the gate doesn't replace an agent's own tests. What it adds is a view from outside, the way a caller hears it. When nothing in the code changes, like the feature flag flip above, the code tests never run, and the gate is the thing that noticed.

How GitLab Duo is used

The checks, the certificate and the revocation are plain code. Duo does the part code can't: read a failure, explain it in everyday words, and decide whether to propose a fix or hand it to a person.

Duo run on Harbor What happened
Gate fix, started by the red pipeline on !42 Explained the changed crisis number and opened fix MR !43, "90 percent sure"
Gate fix, started by the revoked re-check Saw no code to fix and opened issue #38 asking a person to look
Recheck investigator Wrote issue #42: what got worse, old and new answer, what to do
Duo Code Review on the Harbor build, MR !41 Asked for one change, approved after I fixed it
Duo Troubleshoot on the red gate job Called it a real gate failure, not a CI problem, and named both failing checks
Certificate explainer, my custom agent in Duo Chat Read the certificate and incident #37 and answered "revoked today: yes", with the two checks
Merge readiness on !42 Said do not merge. !42 stays open on purpose

All 14 of my flows ran on Harbor. Gate fix started by itself; I started the other 13 with a mention. Twelve of those 13 wrote a comment or an issue. Flaky job spotter finished and wrote nothing. Every session is linked in docs/FLOW_RUNS.md.

Gate fix only opens a merge request when the cause is clear, the change is at most 2 files and 20 lines, it doesn't touch gate settings, keys or certificates, and it's at least 80 percent sure. Otherwise it opens an issue.

One thing I had to be careful about: Duo's Security Analyst gave me totals (3 Critical, 28 High and more) that don't match the newest main pipeline, which has 3 Critical and 1 High. Its totals are closer to the project-wide list, which keeps old findings (6 Critical, 28 High). I use the pipeline's numbers and treat the agent's answer as triage ideas.

Who pressed merge

Gatekeeper's design is Supervised: Duo never merges, a person decides. In this run I delegated the clicks. On 26 October I told my AI build session in chat, and then in a written standing approval at 06:30 UTC, to merge the planned merge requests from my account. Later merges (!62, !63) went in on my yes in chat. It pressed every merge in the Harbor run, including the Harbor build !41 and the Gate fix !43. Duo Code Review approved the Harbor build before it went in. The same session switched the feature flag and pressed play on the 1.3.0 release. GitLab shows all of these as me. No merge in the Harbor run was pressed by my own hand, and I'd rather say that than let the account name imply it.

How I built it

  • gate/: the 15 checks, in Node. 399 tests pass.
  • gatekeeper/: signs certificates, re-checks, writes the revocation and opens the incident. Plain Python, no extra packages, Ed25519 checked against the RFC 8032 test vector. 171 tests pass (pipeline 2993858613).
  • templates/gatekeeper.yml: the CI jobs, gatekeeper-certify on normal pipelines and gatekeeper-recheck on the daily schedule.
  • duo/flows/: the 14 flows as YAML, plus the Certificate explainer agent.
  • demo-agent/: Harbor. The feature flag reaches it as a setting and turns on the "vendor model".
  • A sign-harbor job that signs the image with Sigstore, attaches the certificate and stores the image's digest for the re-check.
  • A register-harbor-model job that records each certified Harbor version in GitLab's Model registry with its gate score (version 3025515: score 100, 9 of 15).
  • An API fuzzing job that sends Harbor's /ask route odd inputs. On its run it sent 42 requests and found 0 vulnerabilities. It reports and never blocks.
  • site/: the check page anyone can use, with a /status view. pages/ builds the same kind of plain status page for GitLab Pages.
  • A "Harbor live status" wiki page with two GLQL tables that list the incidents and the open merge requests straight from the project.
  • ci/seeded_test.py on the seeded-test branch: the 10 bad changes from the test above. ci/seeded_runner.py on main reruns those 10 and the 10 held-out ones on a throwaway copy of Harbor.

The full release pipeline (pipeline 2991143798) has 24 jobs: 23 passed, and 1 (the check page deploy) waits for a person to press play. It runs tests, the gate, the image build, signing, the model registry, GitLab's scanners, API fuzzing, a security gate, staging, a canary check, harbor-production and a carbon report. Secret detection and the code scanner also run on merge requests, and gate results show in the merge request as a test report and metrics. The CI template is published as version 1.3.0 in the CI/CD Catalog, from a copy under my own account, because my role in the hackathon project can't publish there.

Challenges

Most of what I learned came from things that broke for real.

  • The first two scheduled re-checks failed because the gate only tests local addresses, and I'd pointed it at the service name. Gate fix opened issues #33 to #36 about it. I fixed it by forwarding a local port to the production service.
  • Then a re-check with the flag on passed with "Nothing revoked". That looked like good news and wasn't. The flag variable had the same name as the service variable, so GitLab never passed "on" through. I renamed it, and the next run revoked.
  • Gate fix runs twice per red push, because GitLab runs two pipelines on a merge request. It opened two fix MRs and I closed one as a duplicate.
  • Duo Code Review held up the merge request with the real Harbor sample certificate twice, saying a value was "[REDACTED]". The raw files were fine. I answered on its threads instead of going around it, and it then approved.
  • When I pinned the re-check to the exact signed image, the next main pipeline went red at signing, because the image tag already existed and the signing tool won't overwrite it. Duo had approved that change without catching it. I added the overwrite in !56, main went green, and the re-check ran on the exact image and passed. The daily re-check was never affected, since it already used the stored fingerprint.
  • Breaking Harbor on purpose was humbling. I expected the gate to catch most of the 10 changes and it caught 5. Fixing it was the easy part. Proving the fix wasn't just tuned to those 10 took more care: I wrote 10 new bad changes and ran them on the old gate before changing anything. The fixed gate caught all 10 of the originals and 9 of the new 10. The one it misses is in the README, along with the rest.

Accomplishments I'm proud of

The after-release catch. A flag flip with no code change was caught by a scheduled job, the certificate was revoked and an incident was opened with the exact checks that went from pass to fail. Then the restore was checked too.

I'm also glad Gate fix knew when not to fix. On the revocation there was no code to change, and it opened an issue for a person instead of inventing a patch.

What I learned

Running it beat reasoning about it. Every real bug in the re-check showed up only when the schedule actually ran, and one of them looked like a pass. The seeded test taught the same thing about the gate itself: I only found its blind spots by trying to slip things past it. And a held-out test only works once: after I'd seen what it caught, any further change would be tuned to it, so I left the last miss alone and reported it.

What's next

  • Close the one miss left from the seeded test: have the gate ask Harbor's two questions it never asks, the support email and the price of chat. Then test that with a new held-out set, ideally written by someone other than me.
  • Get the Recheck investigator mentioned on the incident automatically. Right now I start it by hand.
  • Have the /status view read the newest re-check result by itself. Today it shows the signed files bundled with the page.
  • Watch Duo Chat call the verify_certificate tool from VS Code. The tool works from the command line; the IDE check is still open.

GitLab lifecycle stages covered

I only list a stage where I can point to a Harbor run.

Stage What covers it Link
Plan Duo's Plan refiner turned a vague issue into a plan issue #43
Create Gate fix opened a one-line fix by itself; Duo Code Review approved the Harbor build after one change MR !43, MR !41
Verify The gate failed the upgrade at 78; tests and the gate run on every pipeline. A seeded test of 10 bad changes showed the gate catching 5. After the fix it caught 10 of 10, and 9 of 10 new ones written before the fix (run on my build computer) gate job, main test report, seeded test pipeline, fix MR !77
Package Harbor is built into an image, signed and stored with the certificate attached, and each certified version goes into the Model registry. The template is in the CI/CD Catalog as 1.3.0 (copy under my account) sign job, model registry job
Secure GitLab's scanners and a live attack test run on main. The newest main pipeline has 42 findings: 3 Critical, 1 High, 28 Medium, 10 Info. All 3 Critical are notes that the demo agent's API description file (demo-agent/openapi.json) declares no login or access rules. They're about that demo file, not bugs in the code. The library scan also writes a list of everything the app is built from (an SBOM). API fuzzing sent 42 requests and found 0 vulnerabilities. Duo's security triage flow wrote up the ones to look at API fuzzing job, security triage issue #46
Release Release 1.3.0, made by the release job from a tag. My AI build session pressed play on my instruction. It was tagged before the last docs change, and a note at the top of the release says so Releases
Deploy Staging, then a canary check (5 test questions, both crisis questions must give the right number), then harbor-production, which also needs a valid signed certificate from the same pipeline. There are no real users, so the canary is a check, not a split of live visitors canary job, production deploy job
Monitor Daily re-check revoked the certificate with the flag on and passed with it off. It now starts the exact signed image. A separate job per environment checks that its last deployment worked revoked run, incident #37, restored run, exact-image run, environment checks
Govern Signed certificates that can be revoked, Duo never merges, merge readiness said "do not merge" on !42 incident #37, MR !42

Most Environmentally Impactful (opt in)

Every pipeline ends with a carbon-report job that estimates the carbon cost of one signed certificate using the Green Software Foundation's SCI method. It is an estimate from job run times, not a measurement. The inputs are in docs/CARBON.md: 2 vCPUs at 2.4 W each, a data-centre overhead of 1.1, and 579 g of CO2 per kWh for us-east1. I assumed GitLab's runners sit in us-east1 and haven't confirmed it, and I left out the carbon of making the hardware, so the figure is a lower bound. On the main pipeline I measured, the 20 jobs ran for 497.8 seconds in total, which comes to about 0.00073 kWh and about 0.4227 g CO2e per signed certificate (estimate, lower bound) (carbon-report job). The point isn't that the number is big. A daily re-check costs well under a gram, so there's no carbon reason to skip it.

Try it

  1. Open the live check page: https://8080-agqwwfnoxxf3f2xts3jidjykla.instabox.tech . It sleeps when idle, so the first visit can take a few seconds.
  2. Press Load sample certificate, then Check. You should see Valid.
  3. Press Load sample revocation, then Check. You should see Valid, revoked, and the checks that went from pass to fail. Or open /status on the same page.
  4. Load the sample certificate again, change "score": 100 to "score": 90, and press Check. You should see Not valid, because the signature no longer matches.
  5. The samples are Harbor's real signed certificate and revocation, bundled with the page. The runs that made them are in the README's claims table, and every job log opens without signing in.

Repo: https://gitlab.com/gitlab-ai-hackathon/transcend-october-2026/43380354/agent-gatekeeper. Video: https://youtu.be/zZEekOozu9Y

Path B: what's new since Oct 5

This grew out of StressProof, my earlier MIT project (https://github.com/Sireadell/stressproof). Its gate is reused as the check engine in gate/. New for this hackathon, starting from the first commit on 22 October: six checks StressProof didn't have (including before_after and made_up_numbers, the two that caught Harbor), the certificate and revocation layer, the daily re-check with incidents, the feature-flag catch, Sigstore signing, the 14 Duo flows and the Certificate explainer agent, Harbor, the check page and the seeded test. docs/NEW_VS_STRESSPROOF.md lists it file by file.

What it is not

It doesn't judge whether an answer is right in general. It checks that known facts still match and that the agent doesn't quietly make things up. In my own test it caught 5 of 10 planted mistakes at first, 10 of 10 after the fix, and 9 of 10 new ones written before the fix. It still can't see a change to an answer it isn't told to ask about, and the new checks only run when an agent's settings file asks for them, so it's one layer, not the whole safety net. A certificate is a signed record of one run, which is why the daily re-check exists. The demo signing key is a masked CI variable, which hides it in logs but not from project members. And Harbor is a demo, not a real helpline.

Built With

Share this project:

Updates

Submission history