Inspiration
Agents can now carry a change from issue to production, and a person is asked to approve each step. After the twentieth "Approve?" for the same kind of step, nobody reads it. The step that deletes data or ships to production then gets the same glance as a test re-run.
Asking people to read more carefully does not fix this. Asking them less often, for the right steps, might.
What it does
Two-Key gives every agent action a tier: reversible, costly or irreversible. Irreversible steps need two keys: an approval and the exact target typed by hand.
The ledger records every decision. When people have approved the same kind of step five times in a row with no rejection or incident, a script proposes a lighter tier for it (costly → reversible → auto, one step at a time). A Duo flow opens a merge request that changes policy/agent-policy.json and puts the evidence table in the description. A person approves that one merge request instead of the next prompts. A rejection or an incident makes the step heavier again. Irreversible steps are never made lighter.
In a seeded 14-day simulation of a small team (about 40 agent actions a day), prompts to humans went from 38 on day 1 to 3 on day 14. Two-key prompts stayed at 3 a day throughout. An incident on day 9 moved merge back from auto to reversible for one day. These are simulated numbers, not measurements from a real team.
How we built it
- Deterministic rules (
twokey/classify.py) set the tier. The model never decides it. - The counting (
twokey/promote.py) is plain Python. It only looks at decisions after each kind's last policy change, and proposes one step per kind. - CI/CD (
.gitlab-ci.yml): unit tests, SAST and Secret Detection templates, a generic package, thetwo-key-gatejob, a GitLab Pages deploy to theproductionenvironment, asmoke-checkjob, and apolicy-stewardjob that saves the proposal as an artifact. - GitLab Duo Agent Platform, two custom flows:
flows/two_key_release.yml: a scout agent writes a Risk Card on the merge request;HumanInputComponents collect the first and second key; a recorder commits the decision to the ledger.flows/policy_steward.yml: an agent reads thepolicy-stewardartifact and opens the policy merge request. It relays the script's output and does not change it.
- The gate (
twokey/gate.py) reads the merged policy.
Gate rule. The two-key-gate job asks for what the step's effective tier requires, and nothing else: auto passes with no ledger record; reversible and costly need one record with first_key: approve for this version and action; irreversible needs that record plus the exact target typed as the second key, and no policy file can lower it.
- GitLab Pages shows the current policy, the last ledger entries and the interrupts-per-day chart (inline SVG, no external scripts).
Real run (2026-10-21, this project). Release 1.1.0 went through the Two-Key Release flow end to end: the Risk Card was posted on MR !2, a person approved and typed the target, the recorder committed the record, ledger-check passed, and in pipeline 2969033925 two-key-gate printed PASS deploy:production: both keys turned. Pages then served 1.1.0 and smoke-check confirmed it. The Policy Steward has nothing to propose yet on real data (one record; the threshold is five), so the 14-day chart on the site is a simulation and is labeled as one. The Policy Steward flow has not opened a real merge request yet.
In the video, the reading and typing are from a second take of the same release; the record, the gate result and the deploy are from the first take, which is the one that went through.
Prior work
- Hedwig ("Hedwig: Dynamic Autonomy for Coding Agents Under Local Oversight", arXiv 2605.11495) adjusts how often a coding agent checks in from the developer's past approvals. It does not write the change as a merge request for review.
- GitLab Deployment approvals (Premium and Ultimate) block a deploy until enough people approve. They count approvers but do not change what is required over time.
Our part is small: the lighter policy is a reviewed commit with its evidence, and reverting it is a normal revert.
Challenges we ran into
- Letting the model choose the tier was tempting and wrong: a confident model can call a production deploy "low risk". We kept the model for explaining and relaying, not for deciding or counting.
- Deciding what "N approvals in a row" means after a policy change. We restart the count at each change, so an old streak cannot be spent twice.
- The live flow validator rejected
response_schemas, so the second-key relay now routes on plain text: the agent answers exactlyASKuntil the person replies, and anything else goes to the recorder. - The second key uses the same prompt as the first; the text box appears under Modify. We wrote that into the runbook.
- The first session failed with
git: command not found: the flow image must include git (python:3.12, not-slim). - In the dry run the scout listed only
mergeand missed that a merge tomaindeploys to production. It now reads.gitlab-ci.yml. The recorder had also guessed the time; it now takes it from the merge request.
Accomplishments that we're proud of
- The gate does not trust the agent. Even if a flow is skipped, production does not deploy without a matching two-key record, and no policy file can change that.
- The rules that matter are unit tests: irreversible never lighter, one step at a time, one rejection or incident tightens.
What we learned
- Approval fatigue is a design problem. Making the rare dangerous step look different, and letting the routine ones stop asking, works better than asking for more care.
- From the mention to the Risk Card on the merge request took about three minutes; from the mention to the live site, eighteen minutes, most of it the person reading and the pipeline. We did not measure Duo credits per run.
What's next
- Let teams add their own irreversible kinds (database migrations, DNS changes) as rules.
- Count per project and per agent, not only per action kind.
- A weekly digest of which steps retired and which came back.
Built With
- gitlab
- gitlab-ci
- gitlab-duo-agent-platform
- gitlab-package-registry
- gitlab-pages
- python
- sast
- secret-detection
- yaml
Log in or sign up for Devpost to join the conversation.