<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daviyon Daniels</title>
    <description>The latest articles on DEV Community by Daviyon Daniels (@daviyon_daniels).</description>
    <link>https://dev.to/daviyon_daniels</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4103325%2Fc9265f0a-25e7-4213-a159-d679296f7013.jpg</url>
      <title>DEV Community: Daviyon Daniels</title>
      <link>https://dev.to/daviyon_daniels</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daviyon_daniels"/>
    <language>en</language>
    <item>
      <title>I asked one model to review another, and it found a bias I would not have</title>
      <dc:creator>Daviyon Daniels</dc:creator>
      <pubDate>Mon, 31 Aug 2026 23:35:10 +0000</pubDate>
      <link>https://dev.to/daviyon_daniels/i-asked-one-model-to-review-another-and-it-found-a-bias-i-would-not-have-54md</link>
      <guid>https://dev.to/daviyon_daniels/i-asked-one-model-to-review-another-and-it-found-a-bias-i-would-not-have-54md</guid>
      <description>&lt;p&gt;I spent five days building an autonomous system that owns the vulnerability&lt;br&gt;
remediation lifecycle — the six weeks &lt;em&gt;after&lt;/em&gt; a scan, which is where one-person&lt;br&gt;
security programs actually die. Finding vulnerabilities is solved. Chasing the&lt;br&gt;
owner who never opened the ticket is not.&lt;/p&gt;

&lt;p&gt;The part worth writing about is not the pipeline. It is a number I did not&lt;br&gt;
expect and initially misread as a bug.&lt;/p&gt;
&lt;h3&gt;
  
  
  65% disagreement
&lt;/h3&gt;

&lt;p&gt;Every triage decision in this system is challenged before it becomes state.&lt;br&gt;
A reasoning agent on Gemini 3.5 Flash proposes a severity, an SLA, and a&lt;br&gt;
remediation path, with cited evidence. A reviewer then either ratifies it or&lt;br&gt;
rejects it with a stated reason.&lt;/p&gt;

&lt;p&gt;The reviewer runs on &lt;strong&gt;Gemma&lt;/strong&gt; — deliberately a different model family. The&lt;br&gt;
argument for that is easy to state and hard to verify: a model auditing its own&lt;br&gt;
reasoning shares its own blind spots. I believed it when I designed it. I did&lt;br&gt;
not have evidence for it.&lt;/p&gt;

&lt;p&gt;The reviewer rejects &lt;strong&gt;65% of the proposals it sees&lt;/strong&gt; — 91 rejections across&lt;br&gt;
139 verdicts, measured on 31 August 2026. About half of the findings that&lt;br&gt;
reach a decision are ratified.&lt;/p&gt;

&lt;p&gt;Those denominators matter and I will not blur them. A rejected proposal is&lt;br&gt;
re-proposed once with the feedback attached, so one finding can produce more&lt;br&gt;
than one verdict: 65% is against verdicts, and the roughly-half figure is&lt;br&gt;
against decisions. Reporting only the friendlier of the two would have been&lt;br&gt;
the easier thing to do.&lt;/p&gt;

&lt;p&gt;My first reaction was that something was broken. A 65% disagreement rate looks&lt;br&gt;
like a defect rate.&lt;/p&gt;

&lt;p&gt;It is not. A reviewer that ratifies everything is indistinguishable from having&lt;br&gt;
no reviewer at all, so the rate is a health metric — and the direction that&lt;br&gt;
should worry you is the one approaching zero.&lt;/p&gt;
&lt;h3&gt;
  
  
  The rejections are not random
&lt;/h3&gt;

&lt;p&gt;I categorised every rejection reason expecting a spread. Instead:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the reviewer objected to&lt;/th&gt;
&lt;th&gt;Share of rejections&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Severity escalated beyond what the CVSS evidence supports&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remediation text naming no specific version&lt;/td&gt;
&lt;td&gt;32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proposed SLA conflicting with the CISA KEV due date&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt-injection text inside the scanner's own comment field&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A rejection can cite more than one of these, so the shares total more than&lt;br&gt;
100%. The last row was not a category I went looking for.&lt;/p&gt;

&lt;p&gt;Half of all rejections say the same thing. Verbatim, from the record:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The severity is escalated to critical without evidence supporting such a&lt;br&gt;
jump from the CVSS base of 7.8, and the remediation is vague."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The severity is rated as critical despite the scanner's CVSS being 5.4 and&lt;br&gt;
the NVD description indicating a high (7.8) severity, creating a mismatch&lt;br&gt;
between evidence and proposal."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The triage model has a consistent bias toward inflating severity past its own&lt;br&gt;
cited evidence. The split confirms it: rejected proposals skew critical (24&lt;br&gt;
critical to 24 high — 55% critical), ratified ones lean the other way (22 to&lt;br&gt;
32 — 41% critical).&lt;/p&gt;

&lt;p&gt;This is the part I want to be careful about, because it is easy to overclaim.&lt;br&gt;
I cannot prove a same-family reviewer would have missed it — that experiment is&lt;br&gt;
one I did not run. What I can say is narrower and still worth something: the&lt;br&gt;
cross-family reviewer &lt;em&gt;did&lt;/em&gt; catch it, it caught it systematically rather than&lt;br&gt;
occasionally, and it wrote down its reasoning every time in a form I could count.&lt;br&gt;
I did not find this bias by reading outputs. The architecture found it and&lt;br&gt;
filed it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Restraint is also a result
&lt;/h3&gt;

&lt;p&gt;The system runs itself on a schedule. Cloud Scheduler publishes a tick, Pub/Sub&lt;br&gt;
fans it to two workers, each executing as its own service account.&lt;/p&gt;

&lt;p&gt;At 09:01 UTC on August 28th, unattended, both workers woke on cycle 30693 and&lt;br&gt;
did nothing. No ticket, no nudge, no escalation.&lt;/p&gt;

&lt;p&gt;Being precise about why, because the honest version is less tidy than the one&lt;br&gt;
I first wrote: the exception agent swept its acceptances and correctly found&lt;br&gt;
none to act on. The chase agent had an empty set of SLA clocks to walk, so its&lt;br&gt;
restraint that morning was trivial rather than considered. When there is&lt;br&gt;
something to weigh it does log the weighing — cycle 9003 logged &lt;code&gt;wait=9&lt;/code&gt;, nine&lt;br&gt;
findings evaluated and nine deliberately left alone because their deadlines&lt;br&gt;
had not arrived.&lt;/p&gt;

&lt;p&gt;I could have dressed the first version up and left it. The point survives the&lt;br&gt;
correction and is better for it: &lt;strong&gt;on the mornings there is something to&lt;br&gt;
weigh, the autonomous loop's answer is "not yet," and it says so rather than&lt;br&gt;
manufacturing activity.&lt;/strong&gt; A system that invents work to look busy is worse&lt;br&gt;
than one that waits — but a write-up that invents judgment where there was&lt;br&gt;
only an empty queue is worse than both.&lt;/p&gt;

&lt;p&gt;The duplicate-delivery guard I did have to go and provoke, because production&lt;br&gt;
had not obliged. Publishing the same tick a second time gets this — one line&lt;br&gt;
per worker, both from the same cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tick_already_ran   cycle=9004   rz-worker-chase
tick_already_ran   cycle=9004   rz-worker-exception
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pub/Sub delivers at least once, so a redelivered tick is a real possibility&lt;br&gt;
rather than a hypothetical. The second copy is recognised and does nothing. Every authoritative state-changing&lt;br&gt;
tool takes a key derived from the finding, the action, and the cycle, so a&lt;br&gt;
resumed agent cannot open a second ticket or send a second nudge.&lt;/p&gt;

&lt;p&gt;The obvious argument for that design is "don't duplicate work." The stronger&lt;br&gt;
one only appeared under test: &lt;strong&gt;models are not deterministic.&lt;/strong&gt; Running the same&lt;br&gt;
cycle twice produces &lt;em&gt;different decisions&lt;/em&gt; for the same finding — one finding&lt;br&gt;
was ratified on the first run and rejected twice on the second. Without the&lt;br&gt;
guard, the second run would not merely have duplicated work. It would have&lt;br&gt;
silently overwritten a decision a human may already have acted on, with a&lt;br&gt;
contradictory one.&lt;/p&gt;
&lt;h3&gt;
  
  
  Proving a boundary by crossing it
&lt;/h3&gt;

&lt;p&gt;Each agent has its own service account. What that buys is worth stating&lt;br&gt;
precisely, because the obvious version of the claim is not true: Firestore&lt;br&gt;
Native has no collection-scoped IAM permission, and Security Rules are bypassed&lt;br&gt;
entirely by a server SDK authenticating as a service account. Per-collection&lt;br&gt;
separation cannot be enforced by IAM here, and claiming it would be claiming a&lt;br&gt;
control that does not exist.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;is&lt;/em&gt; enforceable is a per-database boundary, and that is where the&lt;br&gt;
reporting agent's limit lives: read-only on the operational database, write&lt;br&gt;
access conditioned to a separate reports database. It is structurally&lt;br&gt;
incapable of writing a ticket.&lt;/p&gt;

&lt;p&gt;Asserting that in a README is free. So the check performs the forbidden action:&lt;br&gt;
a Cloud Run job whose identity &lt;em&gt;is&lt;/em&gt; the reporting agent attempts the write and&lt;br&gt;
reports what happened.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expect DENIED   got DENIED (PermissionDenied)   write a ticket        (as rz-reporting)
expect ALLOWED  got ALLOWED                     write a report        (as rz-reporting)
expect DENIED   got DENIED (PermissionError)    read the tracker token (as rz-exception)
expect ALLOWED  got ALLOWED                     read a finding        (as rz-exception)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Half of those expect ALLOWED on purpose. An identity that can do nothing proves&lt;br&gt;
only that it is broken; the control is that the boundary falls in a &lt;em&gt;specific&lt;/em&gt;&lt;br&gt;
place.&lt;/p&gt;

&lt;p&gt;The first version of that check was wrong in an instructive way. It used&lt;br&gt;
&lt;code&gt;--impersonate-service-account&lt;/code&gt; from my laptop, and both identities returned&lt;br&gt;
&lt;code&gt;PERMISSION_DENIED&lt;/code&gt;. It looked like proof. It was proof that &lt;em&gt;I&lt;/em&gt; cannot&lt;br&gt;
impersonate anyone — which says nothing whatsoever about the secret. Running&lt;br&gt;
&lt;strong&gt;as&lt;/strong&gt; the identity rather than borrowing it is the whole difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one that had been passing for 37 hours
&lt;/h3&gt;

&lt;p&gt;Late in the build I had the control suite audited. One check — the one&lt;br&gt;
verifying that the reviewer catches a prompt injection with the input guardrail&lt;br&gt;
switched off — had not called a model since the previous day.&lt;/p&gt;

&lt;p&gt;It ran a cycle against a fixed probe number. The cycle skips any finding whose&lt;br&gt;
idempotency record already exists. So after its first successful run it&lt;br&gt;
returned immediately, and the check re-read the decision that first run had&lt;br&gt;
written, found the expected text, and reported PASS. In 0.3 seconds. For 37.8&lt;br&gt;
hours.&lt;/p&gt;

&lt;p&gt;The evidence was in my own timings table the entire time: three checks&lt;br&gt;
measured 17.2 seconds, while one triage-and-review round costs 15 to 22 seconds&lt;br&gt;
on its own. The number was too good and I read it as good news.&lt;/p&gt;

&lt;p&gt;It now clears its prior record and accepts a verdict only if it was written&lt;br&gt;
during that invocation. It takes 31 seconds, because it does the work.&lt;/p&gt;

&lt;p&gt;The lesson is not "write better tests." It is that &lt;strong&gt;cheap-and-green is the most&lt;br&gt;
comfortable way for a verification suite to fail&lt;/strong&gt;, and the suite you trust most&lt;br&gt;
is the one that most deserves an adversarial read.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does not do
&lt;/h3&gt;

&lt;p&gt;Two gaps that every reviewer found within an hour, so I would rather name them:&lt;/p&gt;

&lt;p&gt;There is &lt;strong&gt;no cross-asset finding normalization&lt;/strong&gt;: real scanner output repeats one CVE across&lt;br&gt;
hundreds of hosts, and the synthetic corpus here has 400 findings with 400&lt;br&gt;
distinct CVEs, so the problem never arises. That is the next thing.&lt;/p&gt;

&lt;p&gt;And the closure loop is newer than the rest of the system, which shows. A&lt;br&gt;
rescan closes what it confirms fixed and refuses to close what it could not&lt;br&gt;
examine — but the scan file's coverage manifest is taken on trust, and a&lt;br&gt;
regression reopening a finding resets that ticket's episode counters rather&lt;br&gt;
than preserving the whole trail. Both are known and neither is exercised by&lt;br&gt;
the committed corpus, which is exactly the kind of thing worth saying out loud&lt;br&gt;
about a feature that is four days old.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stack
&lt;/h3&gt;

&lt;p&gt;ADK 2.8 on Vertex AI. Gemini 3.5 Flash for reasoning, Gemma for review,&lt;br&gt;
Firestore for state, Agent Engine Memory Bank across sessions, Pub/Sub with a&lt;br&gt;
dead-letter queue proven by poisoning it, Model Armor on untrusted ingress,&lt;br&gt;
Cloud Run, Cloud Trace, Secret Manager, Terraform. Seven service accounts, of&lt;br&gt;
which the two scheduled workers genuinely execute as their own identity; the&lt;br&gt;
reasoning graph currently runs under a single client, and the remaining&lt;br&gt;
boundaries are proven by running the checks &lt;em&gt;as&lt;/em&gt; those identities rather than&lt;br&gt;
assumed at runtime. 623 tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Written for the All Things Agentic Hackathon
&lt;/h3&gt;

&lt;p&gt;I built Remediation Zero for the &lt;strong&gt;All Things Agentic Hackathon&lt;/strong&gt;, in the&lt;br&gt;
Fortified Enterprise Fleet track, over five days in August 2026. Everything in&lt;br&gt;
the repository was written during the submission period. The corpus is entirely&lt;br&gt;
synthetic — reserved ranges only, &lt;code&gt;.invalid&lt;/code&gt; hostnames and addresses drawn&lt;br&gt;
from &lt;code&gt;192.0.2.0/24&lt;/code&gt;, &lt;code&gt;198.51.100.0/24&lt;/code&gt; and &lt;code&gt;203.0.113.0/24&lt;/code&gt; — with real CVE&lt;br&gt;
identifiers.&lt;/p&gt;

&lt;p&gt;Code, architecture diagram, and a runbook with every step timed:&lt;br&gt;
&lt;strong&gt;github.com/Ayliea/remediation-zero&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  AllThingsAgenticHackathon
&lt;/h1&gt;

</description>
      <category>allthingsgentichackathon</category>
      <category>googlecloud</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
