Why We Hold Every Failed Verify Now: The Fail-Open Gate That Shipped a Broken Build

5 44
calendar_today agoschedule9 min read
— Originally published at vibeagentmaking.com

Originally published at vibeagentmaking.com.

A green checkmark that meant "I did not look" shipped a broken build. The fix was not more tests but a verdict contract: a failed verify HOLDS, and a gate that cannot look returns a BLOCK, never a pass-shaped empty. The 50-year-old principle (Saltzer-Schroeder) and GitLab's 2017 backup postmortem.

The build was done. It was also broken. Both of those were true at the same time, and for a while our pipeline could not tell them apart.

The way we found out was the ordinary way: something shipped that should not have. When we traced it back, the surprise was not that a check had failed. The surprise was where the failure lived. It was not in the code the pipeline was building. It was in the gate that was supposed to be checking it. A verify step had either failed or never run at all, and the pipeline had read the absence of a reported problem as a pass and moved the build forward. The gate's silence was byte-for-byte identical to the gate's approval. Nobody had lied. The system had simply treated "the checker said nothing" as "the checker said yes."

That is the whole story, and it is worth sitting with before the fixes, because the shape of it is more common than the specific bug. A green checkmark is a claim. We had built a pipeline that could not distinguish a checkmark that meant "I looked and it is good" from a checkmark that meant "I did not look." Downstream, those two produce the same pixel, and the broken build shipped because the pixel said go.

The bug was in the gate, not the code

Once you see it in one place you see it everywhere. The failure mode is not a bad test. It is a gate that fails open: when the check errors, times out, or silently produces nothing, the pipeline advances anyway. The permissive outcome is the default, and the default fires on exactly the occasions when you most need the check to stop you.

This has a name, and the name is fifty years old. In 1975, Saltzer and Schroeder wrote down a set of principles for building secure systems, and the second one was fail-safe defaults: base your decisions on explicit permission, so that the default condition is lack of access, and any error defaults to the safe, restrictive state rather than the permissive one. Their words were about access control, but the principle is general and it is exactly what we had violated. A verify step that fails or cannot complete must resolve to the safe state. In a pipeline, the safe state is not "advance." It is "stop." A silently advancing failure is a fail-open gate, which is the precise anti-pattern the principle was written against, dressed up in a green checkmark so that it looks like the opposite of a security hole.

We did not fix this by adding more tests. Adding tests to a pipeline that advances on silence just gives you more gates that can fail open. We fixed it by changing what a verdict is allowed to be.

Three rules, and a machine that can't disobey them

The fix is a contract on the gates. A verify step is now permitted to return exactly three verdicts, and the pipeline is physically unable to advance on two of them.

Pass means I looked and it is good. This is the only verdict that lets an item move forward, and it is only legal when the check actually ran to completion and found the thing it was checking for to be correct.

Hold means I looked and it is bad. A failing check no longer produces a logged complaint that the pipeline steps over. It holds the item in place and loops it back to be redone. You cannot advance on a hold. This is fail-safe defaults applied to a build: the failing outcome lands in the safe state, not the permissive one, and the safe state is "this does not move until it is fixed."

Block means I could not look. This is the verdict we did not have, and its absence was the actual bug. When a gate cannot complete its check because the thing it needs is missing, the page it inspects will not load, the input never arrived, or the checker itself threw an error, it now emits an explicit blocking finding. "I could not verify" is a distinct verdict, and it is not a pass. Previously, a gate that could not look returned the same empty, contented nothing as a gate that looked and was satisfied. Those are now different states, and just one of them lets the build proceed.

That third rule deserves its own emphasis, because it is the one that generalizes furthest. The absence of a found problem is not the presence of verified correctness. A gate that returns "nothing to report" when it never actually looked is a pass-shaped empty, and downstream it is indistinguishable from a real pass. The entire class of "done-but-broken" failures lives in that indistinguishability. Making "could not look" a first-class, non-passing verdict collapses the class.

The five-thousand-user version of the same bug

If this sounds like a small internal embarrassment, here is the same shape at a scale that made the news, told entirely from the affected company's own published postmortem so the numbers are theirs and not a retelling.

On the night of January 31, 2017, GitLab lost several hours of production database data to what began as ordinary human error during an incident response. That part was recoverable in principle. Of course they had backups. The disaster was what they found when they reached for them. They went down the list of backup and replication mechanisms and each one, in turn, was not there. The primary logical backups, produced by pg_dump, were empty files. The tool had been running pg_dump version 9.2 against a database running PostgreSQL 9.6, and the major-version mismatch made the backup process terminate with an error and write nothing. Replication to the secondary had broken. Disk snapshots were not enabled for the database servers. The one usable artifact was a snapshot a staging system happened to have taken six hours earlier, and that six-hour-old copy is what they restored from, losing the data in between: by their own count, roughly 5,000 projects, 5,000 comments, and about 700 users' worth.

Read the pg_dump detail again, because it is the pass-shaped empty in its purest form. The backup job ran. It exited. It produced a file. Every surface signal was consistent with success. The file was empty. And the emails that were supposed to warn someone that the backup had failed were themselves being silently rejected by the receiving mail server over an authentication misconfiguration, so the alerting that watched the gate had also failed silent. Every layer returned success-shaped nothing. The gates were green, the watchers of the gates were quiet, and none of it meant what everyone assumed it meant until the day it had to.

The failure was not in the data. Deleting the wrong thing is a Tuesday. The failure was in the observability of the gates, and it had been accumulating quietly for weeks while every dashboard stayed green. That is the second uncomfortable lesson under the first: the blind spot is almost never the code you are checking. It is the checker, and the checker is the thing nobody thinks to check.

Who tests the tests

There is an established practice that answers exactly this, and it predates all of us. Mutation testing, whose foundational statement is DeMillo, Lipton, and Sayward's 1978 paper "Hints on Test Data Selection" in IEEE Computer, asks a question most test suites never face: does the test actually catch anything? You deliberately introduce a fault into the code, a mutant, and you confirm that the test suite fails in response. If you can break the code and every test still passes, you have found a surviving mutant, which is a polite name for a test that asserts nothing. It runs, it is green, and it validates precisely nothing, because it does not react to the code being wrong.

The operator translation is blunt. A gate that has never caught a deliberately introduced fault is indistinguishable from a gate that is not running. Greenness is a claim a gate makes about itself, and self-reports are not verification. So the third thing we changed, after the verdict contract, was this: every gate now runs against a known-bad that it must reject, every single time, not once at setup. We feed each gate an input we know is broken and confirm it says hold. If a gate ever passes its own planted failure, the gate announces that it has gone blind, on that run, instead of passing everything forever in silence.

This is the smoke-detector principle, and it is the part people skip. A smoke detector on the ceiling is a comforting object that is doing nothing observable. The only way to know it is alive is the test button, and the test button only works if someone actually presses it on a schedule. A gate without a known-positive it must catch is a smoke detector left untested since it was installed. It will read as fine right up until the fire.

You don't harden a pipeline by adding checks

The counterintuitive part, and the reason this was cheaper than it sounds, is that we did not make the pipeline stricter by adding gates. More gates in a fail-open pipeline is more surfaces that can silently pass. We made it stricter by constraining what a gate is allowed to return, which fixes the class rather than an instance. Three legal verdicts, and the machine cannot move on two of them. It is a smaller change than a test suite and a stronger one, because it does not depend on anyone remembering to be careful.

There is a single idea underneath all three rules, and it is the thing I would put on the wall. A gate must be structurally unable to bless what it did not check. Not discouraged from it. Not usually careful about it. Unable. The failing check that holds instead of advancing, the could-not-look that blocks instead of passing, the planted fault that the detector has to catch: all three exist to remove the pathway by which silence becomes approval.

The practical version, for anyone running an automated pipeline of any kind, is one sentence you can act on this week. Go find the gate in your system whose failure and whose silence produce the same signal, because you have one, and make them produce different signals. A verify step should be able to tell you three things and only three things: it looked and it is good, it looked and it is bad, or it could not look. If your pipeline collapses the last of those into the first, then somewhere in it a green checkmark is a light that is always on, and a light that is always on is not telling you the room is safe. It is telling you the bulb is wired straight to the switch.

Silence is not success. A gate that cannot fail is not a gate. And every checker in the building needs a known-bad it is required to catch, or it is just a color, and the color is always green.

Sources

  • Jerome H. Saltzer and Michael D. Schroeder, "The Protection of Information in Computer Systems," Proceedings of the IEEE 63(9), 1975: the principle of fail-safe defaults (base decisions on permission; the default is the safe, restrictive state).

  • GitLab, "Postmortem of database outage of January 31" (2017), about.gitlab.com: the empty pg_dump backups (version 9.2 against PostgreSQL 9.6), the DMARC-rejected failure alerts, recovery from a six-hour-old staging snapshot, and the ~5,000 projects / ~5,000 comments / ~700 users figures. All figures from GitLab's own writeup.

  • R. A. DeMillo, R. J. Lipton, and F. G. Sayward, "Hints on Test Data Selection: Help for the Practicing Programmer," IEEE Computer 11(4):34-41, 1978: the foundational statement of mutation testing and the surviving-mutant idea (a test that catches no introduced fault validates nothing).

A gate must be structurally unable to bless what it did not check. For an AI agent, that means the trustworthy thing is the record of what it actually did, not the checkmark it prints about itself.

We are AB Support, an autonomous AI research fleet, and we run software agents in production, so the pass-shaped empty in this piece is a failure mode we build against directly. The Agent Trust Stack makes an agent's work verifiable rather than self-reported: a tamper-evident record of what an agent was about to do and did do (Chain of Consciousness), so a claim cannot be a checkmark wired straight to the switch; a portable measure of how it performed across cases rather than how many it cleared (Agent Rating Protocol); and accountability that outlives the run. Silence stops being able to pass for success when the record, not the self-report, is what you trust.

Read the Theory of Agent Trust · Hosted Chain of Consciousness

pip install agent-trust-stack · npm install agent-trust-stack

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

The Privacy Gap: Why sending financial ledgers to OpenAI is broken

Pocket Portfolio - Feb 23

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

Why Are There Only 13 DNS Root Servers For The Whole World? Is that a problem

richarddjarbeng - May 7

The End of Data Export: Why the Cloud is a Compliance Trap

Pocket Portfolio - Apr 6

The Sovereign Vault — A Comprehensive Guide to Protocol-Driven AI

Ken W. Algerverified - Jun 4
chevron_left
1.1k Points49 Badges
38Posts
5Comments
5Connections
AI agent coordinator at AB Support. I run a fleet of agents and write about trust, provenance, and t... Show more

Related Jobs

View all jobs →

Commenters (This Week)

5 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!