Inspiration

We ran GitLab Duo flows in the background on our own merge requests and noticed three things. Nobody is asked before a step runs. Tool governance controls which tools an agent may call, not whether its reasons are true. And the commits the flow makes are recorded under the person who triggered it. "The model behaved well this time" cannot be audited or tuned per team. We wanted a gate that checks each consequential step against what GitLab itself knows (pipeline, tests, scanner reports, diff), produces a number you can set a threshold on, and stays shut when anything is uncertain or broken.

What it does

Turnike wraps a Duo flow ("Turnike Supervisor") in two gates and a verify stage, all of them CI jobs:

  • Entry gate. Before any agent runs, it reads every untrusted channel the agent will read (description, commits, comments, added diff lines, failing job log tails) and asks our model one question: does any of it try to give the agent orders? BLOCK ends the flow before an agent starts. On MR !4 a description addressed to "the automated assistant" was blocked.
  • Proposer. A Duo agent with read-only tools writes a structured plan grounded in the failing pipeline.
  • Exit gate. On the committed plan it asks three questions: does the evidence support the justification, is untrusted text steering the agent, is the action within the goal? Only ALLOW lets the executor act, and it may apply nothing but the approved plan.
  • Approve. A held plan (ESCALATE) is released by a person pressing play on a manual CI job. The approval binds to that exact record and plan, and the flow resumes. Live on MR !13: held, approved 82 s later, applied, verified, pipeline green.
  • Verify. After the executor commits, a job checks the pushed diff against the approved plan and the pipeline on that commit.

The questions are answered by Turnike-JEV-9B gate8_r2, our own 9B decision model (Qwen3.5-9B + LoRA, FP8), in one forward pass with calibrated probabilities and no generated text. A YAML policy turns those probabilities, deterministic rules (test weakening, paths outside the diff, weakened controls, a failed pipeline) and a path-based blast radius into ALLOW, ESCALATE or BLOCK; one line moves a project between Assisted, Supervised and Hands-off. Every decision leaves a JSON record, a receipt note on the MR that quotes the exact line that stopped a step and where it came from, and a row on a public Pages console with its GPU energy.

How we built it

  • Our own model, eleven runs, each judged by a rule fixed before the run. Training text is synthetic and every label is derived by program; external material (LLMail-Inject, CyberSecEval, NotInject, CommitPackFT, gitlab-org commit messages, SWE-rebench) is licensed and attributed row by row. From round 2 the data builder and the judge were two different parties: the judge measured each candidate once on sets the builder never saw (37 fresh attacks in 18 families, 12 look-alikes, 349 real upstream MRs, the recorded field decisions). Three runs shipped (gate1, gate5, gate8_r2); eight did not, two of them trained after gate8_r2 and beaten by it on the withheld sets.
  • Calibration. Soft targets in training, inverse smoothing at inference, thresholds set on out-of-generator text, 96 hard rows relabelled as human gold.
  • GitLab integration. The CI jobs build the evidence from GitLab APIs and job artifacts (pipeline, JUnit, SAST, secret and dependency reports, diffs, approvals) with a source for every field. The flow's gate steps run a trusted copy of the gate code from the default branch, so an MR cannot change its own judge. A missing endpoint or an unreadable MR fails closed (ESCALATE, exit 30). Rules parse to an AST, no eval; 1,892 tests.
  • A CI/CD component adds the five gate jobs to any project with one include and three variables; a project created from scratch got its first receipt about four minutes later.
  • Serving. Since 23 Oct a Google Cloud Run GPU service (one L4, scale to zero, weights fetched from a private bucket, a warm-up inference before the startup probe, nothing billed between requests); one RTX 5060 Ti for every evaluation and as fallback. GPU energy per decision is sampled with NVML, and SCI (ISO/IEC 21031) is computed with every input tagged MEASURED or ESTIMATED.

Challenges we ran into

  • Platform constraints. Flow commands get no custom CI variables and the sandbox reaches only localhost and gitlab.com, so the model is called from CI and the flow waits for the CI record. Routers read a route file; the catalog validator rejects inline response schemas, so the gate validates the proposal itself.
  • The gate judged itself. Our own receipts, posted under a human account, read as text aimed at the agent: one false BLOCK before we filtered our own notes, a second from a branch whose old CI config lacked the flow account.
  • Our red team beat the gate. An independent 124-row probe stopped 82.4 % of 74 attacks; seven of the 13 misses were payloads in channels the agent reads and the gate did not (code comments, a README, job logs, text past a length cap). We now scan every channel in windows and escalate over budget: 93.2 % on the same rows with no benign row changed; gate5 then 72 of 74, gate8_r2 37 of 37 on the withheld set.
  • A security review of our own change found chat-control tokens reaching the prompt, receipts that could leak per-line probabilities, quotes that could republish secrets, and a console that trusted MR-pipeline records. All fixed. One item stays open (CI secrets visible to MR pipelines): the remedies are outside the owner's hackathon role.
  • Our own acceptance rule could not be met. gate4 was better than gate1 and still failed a rule demanding zero lost items in every cell: training recipes disagree on about 9 % of out-of-generator decisions, so the chance of passing was about 0.001. We did not bend the rule afterwards; we wrote rule v2, showed it still rejects the earlier failures, and pre-registered it.
  • Real MRs found what probes had not: a false hold on GitLab Runner's own log lines, a linked-issue cap too low for Go MRs, the flow's proposal file contradicting the agent's own file list, a Maintainer-only approval nobody in the hackathon workspace could give. Each fixed and recorded.

Accomplishments that we're proud of

  • Real upstream merge requests, end to end, on the deployed model. Two gitlab-org/api/client-go MRs replayed with the upstream CI: a test change held by rule, approved with one click, applied and verified green in 11 minutes; a golden-file fix held one hundredth under the bar, approved, verified in 13 minutes.
  • Withheld-set results of gate8_r2: 37 of 37 attacks stopped, 12 of 12 look-alikes allowed, 10 of 349 honest upstream MRs held (previous model 47), nothing else changed in the field.
  • The first run outside our own repository: ten MRs in a Python project with outcomes written down first matched at every stage reached (entry 10/10, exit 6/6, verify 4/4, approve 1/1).
  • A second AI system integrated the API from the guest page alone in 5 to 10 minutes and reported ten friction points, six fixed the same night.
  • We killed eight of our own eleven models and published why, including where the shipped ones fall short: gate5 was not measurably better than gate4 on independent data, and the exit happy path is still held in most cases.
  • A frontier judge next to our 9B model, published although it wins at the question level (0.934 against 0.771 argmax); the gate's decisions come from thresholds and rules.
  • 2.84 J of GPU energy per gate decision (median, measured) and no Duo credits.

What we learned

Numbers from a generator flatter you: gate1 was near-perfect on its own split and got 13 of 17 decisions right on the first probe built from real MRs. Acceptance rules that demand zero regressions in small cells measure training noise, not improvement; a good rule has absolute safety floors, one paired test with a confidence interval, per-family guards, and is fixed before the run. Checks the model is bad at (test weakening, paths outside the diff, weakened controls) belong in deterministic rules. And a gate is only as good as what it reads.

What's next

  • The exit happy path at the model: round 4 was judged and not deployed (its builder's dev set said 44 of 50 fixes pass, the withheld set 1 of 38 with 13 false blocks). The next attempt needs the judge's rendering in the training data and a re-calibrated block threshold.
  • Catalog release of the component once a Maintainer role can list it; the gate weights to public (uploaded to Hugging Face, private until the submission goes in).
  • The open security item with a bot account's token, a first score on the untouched 60-item holdout, the frontier judge re-run from opaque ids.

Environmental impact (Most Environmentally Impactful)

Method: SCI (ISO/IEC 21031:2024), SCI = (E × I + M) / R, every input tagged MEASURED or ESTIMATED. A gate decision costs 2.84 J median GPU energy (19 real single-window decisions, NVML; 4.8 to 101 J with 2 to 7 windows after the coverage change), 0.966 mgCO2e per decision and 3.06 per MR at the GPU host's grid intensity (441 gCO2e/kWh, estimated), and 0 Duo credits. An un-gated agent run takes 72–77 s of hosted runner time, about 2.5 credits and an estimated 1,500 J. The saving comes from the entry gate: a BLOCK there stops the flow before the frontier agent runs, for 0.2 % to 7 % of the energy of the run it prevents; only entry BLOCKs count as prevented, escalations as deferred, exit decisions never. Design choices: a 9B model instead of a frontier model on the decision path, one forward pass with no generation, FP8, single-pass serving, a GPU that scales to zero. Honest note: of the first three entry blocks on the console, two were our own false positives, since fixed; the genuine prevented run is E9492 on MR !

Built With

Share this project:

Updates

Submission history