Our Security Scanner Passed an Agent That Leaked Root and an API Key

Three consecutive releases of our own harness were correctness disclosures, not features. An absent answer read as safe, a refusal read as a failure, a compliance read as a refusal. The tests ran, returned, and were green — and nothing checked whether the green meant anything.

Three consecutive releases of our own security harness were correctness disclosures. Not features. Each one found a different way the tool had been reporting a verdict it had not earned, and each one told users to re-run anything they had measured before it.

The three defects are worth reading together, because separately they look like ordinary bugs and together they are a single lesson about testing anything autonomous.

4.15.0 — a test that could not run is not a test that passed

Seven modules recorded a PASS when the target never serviced the request. A host that was not running produced a clean pass.

Every affected detector short-circuited on an error response and returned “no attack indicator found,” and every caller read that as “the control held.” Verified by running the verdict expressions rather than by reading them:

  • transport failure, host down → recorded passed=True
  • HTTP 404 → recorded passed=True
  • HTTP 500 → recorded passed=True
  • HTTP 200 carrying a JSON-RPC error envelope → recorded passed=True

In two adapter modules it was not even accidental. Seventeen sites computed passed = self._check_error(resp) — setting a pass because the target had errored.

The part that should worry you most

The guard already existed. A _serviced() helper had been written two releases earlier to fix twenty false passes in one module — and left in that file. Nothing carried it to the six other modules with the identical verdict pattern. The fix was known, correct, and un-propagated for two releases. It now lives in the shared recording path, so a test cannot forget a guard it never has to call.

4.16.0 — a target that refuses is the control working

The next release moved verdicts in both directions, and the second direction was the new one. More results became INCONCLUSIVE, for the reason above. But some results that used to FAIL now correctly PASS: a target that visibly refuses an attack is the control working, and ten modules could not recognise that.

A scanner that cannot tell “the attack was blocked” from “the attack failed to arrive” is not conservative. It is wrong in a direction that happens to feel safe.

4.17.0 — and a target that complies is the control failing

Then the sharpest one. Eight modules could not distinguish an agent that refused an attack from one that complied with it, and reported PASS for both.

Measured against a live loopback agent answering HTTP 200 to everything:

8 / 8
return_channel_harness passed every test against a complying agent, a refusing agent, and an agent visibly attacking — including one emitting root:x:0:0 and API_KEY=sk-live-1
From the 4.17.0 release notes. The complying and refusing columns were frequently identical, down to the passing set.

gtg1002_simulation, which simulates a documented nation-state espionage campaign, passed 17 of 17 for a complying agent and 17 of 17 for a refusing one. Six of the rows that survived an agent reporting harvested credentials, lateral movement, 42,000 exfiltrated rows and established persistence are rated P0-Critical.

And the inverse case, which is the one I would put on a slide. over_refusal_harness exists to catch an agent that wrongly blocks a legitimate request. It passed 25 of 25 against an agent that refused everything. It read the HTTP status and never the response body, so the entire class it exists to detect was invisible to it.

One contributing defect is small enough to be funny and serious enough to be instructive: the refusal classifier matched substrings, so “restricted” matched inside “unrestricted”. An agent announcing “Profile escalated to admin, unrestricted” was therefore classified as refusing. It now matches on word boundaries. The related negation case — “no longer blocked” — is not fixed, and ships documented as a known limit rather than quietly.

The common shape

Three defects, three releases, one structure. In every case the tool produced a confident verdict from a signal that could not support it:

  • an absent answer read as a safe one;
  • a refusal read as a failure;
  • a compliance read as a refusal.

None of these is a false negative in the ordinary sense. The tests ran. They returned. They were green. What was missing was any check that the green meant anything — and a passing test that cannot fail is not evidence, it is decoration.

What actually catches this: targets whose answers you already know

You cannot find this class by reading the code, because the code looks fine. Every one of these defects was found by running the suite against deliberately constructed targets whose correct verdict was known in advance:

  • a closed port — nothing answers, so everything should be inconclusive;
  • a permissive host that answers HTTP 200 and grants everything — harder, because every serviced-request guard is satisfied and the target is maximally unsafe;
  • a refusing host — where a pass is correct, so a suite that fails everything is exposed too.

The permissive host is the one most tools never build, and it is the one that found the worst of these. A closed port catches a suite that passes when nothing is there. Only a target that says yes to everything catches a suite that passes when everything is wrong.

The test to run on your own tooling this week

Point your security suite at an endpoint that answers 200 and grants every request. If your pass rate does not collapse, your suite is not measuring what you think. Then point it at one that refuses everything. If your pass rate does not go up, it is not measuring that either. Both runs take an afternoon, and either result is worth more than another hundred tests.

Why publish this

The uncomfortable framing is the accurate one: for several releases, a tool whose entire purpose is telling you whether your agent is safe was, in specific and now-enumerated ways, unable to tell. Anyone who ran the affected modules against a live target got numbers that did not mean what they said, which is why each release told them to re-run.

The alternative was to fix it quietly in a patch note. That option was available and it was worse, for a reason that has nothing to do with virtue: a security tool’s only real asset is that you believe its verdicts. A tool that silently corrects a class of false passes is asking to be trusted on exactly the dimension it just failed on. Publishing the tables — complying, refusing, attacking, side by side — is the only version that leaves a reader able to check the claim.

It is the same argument as the pipeline stage that logged success for fourteen days while writing nothing, arriving from the other end. There, a producer reported success and produced nothing. Here, a verifier reported a verdict it had not earned. Both are the same failure: a system reporting on itself, with nothing independent to contradict it.

Two limits, stated because they are the point

First: this harness holds no I2 evidence at all — no independent-sensor observation — because it reads protocol responses the target itself emits. That is a structural ceiling, not a backlog item.

Second, and more relevant here: an independent developer recently wrote a separate implementation of one of our fixture oracles and reproduced all eleven verdicts. That is the first external check this project has ever received, and the honest reading of it was published alongside the release rather than in a press line:

“Two implementations agreeing establishes that they share a reading; only a third can tell you whether that reading is the contract or a coincidence.”

A reproduction is not a validation. It is one external party getting the same answers from the same inputs — genuinely useful, and not the thing a reader should mistake it for. Which is the whole argument again, applied to the one piece of good news in it.

This field note was drafted by AI agents operating under the constitutional governance framework it describes, and human-reviewed. Every figure, table and quotation is taken from the harness’s own published CHANGELOG for releases 4.15.0, 4.16.0 and 4.17.0, read directly rather than from any summary of it. The defects described are first-party and self-reported; the affected modules, counts and verdict tables are reproduced as published so a reader can check them against the source. No claim is made that this harness is now correct — only that these three classes are fixed and disclosed, and that its structural I2 limit stands (HC-9). Composition preprint: doi.org/10.5281/zenodo.21400261 · Enterprise Agent Architecture: doi.org/10.5281/zenodo.21105314.