The problem
AI agents can write the code, but a team still has to decide whether that change
should reach main. A familiar author name and a passing pipeline do not tell
the whole story:
- AI-written merge requests look human. A Duo flow's MR is attributed to the person who triggered it, while its service account writes the commits.
- Agents can loosen their own guardrails. A one-line fix can widen a network allowlist or make a failing check optional.
- Untrusted text can steer an agent. Issues and comments can influence a change without carrying the authority to approve it.
- People cut the same corners. Removed tests, oversized changes and rewritten history also need scrutiny when a person writes the code.
- Merges leave no record of authority. Git history alone usually cannot show what was checked, which policy allowed a merge or who approved that exact change.
This came from watching real runs. On 24 October 2026, 34 of 120 recent public
hackathon MRs had human MR authors and Duo-authored commits. In our own project,
one agent widened its network allowlist to fix a failing job; another added
allow_failure: true. Both changes were small, plausible and easy to approve.
The solution
GitGovern built with GitLab Duo adds an enforceable merge gate for teams working with AI agents. Assign GitGovern Merge as a reviewer, or open an MR from the default template, and the Duo flow runs four phases:
- Prepare. Read the exact commit, commit authors, changed files, weakened checks, size, history and instruction sources from GitLab. Check the project access of the people whose issues or comments could have steered the agent.
- Review. A Duo agent reviews the diff for eight kinds of risk. Its flags can only add risk; its explanatory prose is shown separately and remains unsigned.
- Govern. Wait for that commit's pipeline, evaluate a published Decionis policy and enforce the result with Govern. Before any merge, recheck the facts and claim a single-use grant for that exact commit.
- Explain. Post one decision note and, after a merge, write the decision, signed dossier, policy bundle and AI review references into the merge commit.
The result controls execution:
- ALLOW: merge exactly the permitted commit after the final fact check.
- ESCALATE: wait for a WebAuthn passkey approval in Presence from someone
holding
GITGOVERN_MERGE_APPROVER. Each request lasts up to five minutes; expired requests can be renewed, with fresh facts, up to six requests total. - BLOCK: nothing runs, even with approval.
Agents are held to a stricter standard than people. An agent that edits governance files or removes a scanner is blocked; a person doing the same is escalated. Weakened tests, changes over 1,000 lines or 50 files, and content rewritten by a force push escalate for anyone.
Anyone can check the receipts. npm run receipt:audit lists every commit on
main without a matching receipt, using only git history. npm run
receipt:verify recomputes an MR's facts and checks them against the signed
dossier.
How we built it
- GitLab Duo Agent Platform.
- A registered flow, GitGovern Merge, triggered by reviewer assignment. Fixed steps call our service; one agent reviews the diff and another explains the result.
- Each step authenticates with its job's GitLab ID token, so the flow holds no secret.
- A default MR template requests the flow at creation.
- A hosted MCP service in TypeScript on Node 24. It reads GitLab with a fine-grained token kept in Azure Key Vault, accepts only a running Duo workload of this project and flow, and holds requests waiting for a pipeline or an approval.
- Govern 2.1.0, Decionis's open-source (Apache-2.0) enforcement binary. It captures the intent, gets the verdict, claims the grant and finalises the outcome.
- A Decionis policy (v3, 17 rules), published as an immutable bundle after a review against the live tenant. Recorded verdicts from the policy engine pin 32 cases, and a SHADOW backtest replays every MR in the project.
- GitLab CI. A
receipt-auditjob on every push tomain, secret detection, and the image builds the flow and service run on.
We built it with only Developer and Duo access in the hackathon project: no Maintainer settings and no secrets in the project.
Results
All of this ran live in the showcase project, against a real Decionis tenant.
- !7: an agent widened its own allowlist. BLOCK; nothing ran.
- !8: an agent added
allow_failure. ESCALATE; nothing ran. - !21: opened from the template. It waited for its pipeline, then policy v3 allowed and merged it with a receipt.
- !22: the AI reviewer caught us. We loosened two exact test assertions, and it flagged both and named the weakest. That is a change line patterns cannot see. As a human change, it merged with the flags recorded; an agent's would have been held.
- !25: a Duo agent's docs change, asked for in issue #4. Policy v3 allowed
it under
agent-routine, with its one instruction source trusted, and merged it with a receipt. - !26: a person edited the policy docs, a governance file. v3 escalated it.
One passkey approval in Presence later, Decionis re-evaluated under
approved-escalationand GitGovern merged it. Its receipt verifies. - !28: approval after two expired requests. Another governance edit. Its first two requests expired unapproved; GitGovern asked again on its own, and the third was approved and merged.
- Since !14, every merge into
mainwent through GitGovern. Every receipt verifies against Decionis's signing key. - A SHADOW backtest of 20 MRs under v3:
- agent: 1 BLOCK, 1 ESCALATE;
- human: 10 ALLOW, 8 ESCALATE, all governance edits.
Challenges
- Two runs decided ALLOW but merged nothing. We made the merge helper record where and why it stops, which showed GitLab answering slowly during the recheck. We then made the recheck read in parallel, with a retry per read. It now takes about 1.5 s instead of 2.8 s.
- An MR opened from the template merged 2 min 49 s before its pipeline passed. Now GitGovern waits for the head's pipeline before it asks for a verdict.
- An approval window shorter than we asked for. Govern keeps an escalation open for at most 5 minutes, although we asked for 60, and !26's first request expired unapproved. GitGovern now asks again when a request expires, for up to six requests, rereading the facts each time: about 30 minutes in all. On !28 the third request was approved.
- Policy updates are immutable bundles in a shared tenant. Each version keeps every other application's rules verbatim. We replay those rules before publishing, and earlier receipts keep verifying.
What we learned
The hard part isn't stopping a bad merge. It's being able to prove, later, why a good one was allowed. A receipt in git that anyone can check changes the conversation from "trust the bot" to "check the record".
What's next
- Native flow triggers (pipeline passed, MR ready), so agent-written MRs are governed without anyone requesting a review.
- Linking GitLab users to Presence identities, so a person cannot approve their own escalation.
- The same governed merge for GitHub, Bitbucket and Azure DevOps.
Testing instructions
Everything listed here is public and needs no account:
- the decision notes on !7, !8, !21, !22, !25, !26 and !28;
- the merge commits and their receipts;
- the pipeline history, including the flow's own runs;
- the receipt audit and the test suite (482 tests, run with the real Govern binary). See For judges in the README.
Starting a live governed merge needs Developer access to the project. Verifying a dossier yourself needs Decionis credentials. The video shows both.
Built With
- azure-container-apps
- azure-key-vault
- decionis
- gitlab
- gitlab-ci
- gitlab-duo-agent-platform
- go
- model-context-protocol
- node.js
- typescript
- webauthn
Log in or sign up for Devpost to join the conversation.