<?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: Roogify</title>
    <description>The latest articles on DEV Community by Roogify (@roogify).</description>
    <link>https://dev.to/roogify</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%2F4092457%2Ffd676269-edb3-4e73-912a-201ce0ed05cd.png</url>
      <title>DEV Community: Roogify</title>
      <link>https://dev.to/roogify</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/roogify"/>
    <language>en</language>
    <item>
      <title>The filter missed. The schema held. Neither is where the guarantee lives.</title>
      <dc:creator>Roogify</dc:creator>
      <pubDate>Mon, 24 Aug 2026 13:51:18 +0000</pubDate>
      <link>https://dev.to/roogify/the-filter-missed-the-schema-held-neither-is-where-the-guarantee-lives-keg</link>
      <guid>https://dev.to/roogify/the-filter-missed-the-schema-held-neither-is-where-the-guarantee-lives-keg</guid>
      <description>&lt;p&gt;&lt;em&gt;Written for the All Things Agentic Hackathon (Google + Devpost). It describes measurements taken while building Okimera, a multi-agent system for maritime sanctions compliance, and is published as part of that entry.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;We built a system whose whole premise is that one of its agents reads documents written by the party under investigation. A counterparty sends a bill of lading; an agent extracts structured facts from it; other agents decide whether the deal can proceed. If a hidden instruction inside that PDF can influence the decision about its own sender, the product has no reason to exist.&lt;/p&gt;

&lt;p&gt;So we put a prompt-injection filter in front of that agent, and then we tried to measure whether it worked. What we found was more useful than a filter that worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement
&lt;/h2&gt;

&lt;p&gt;Google's Model Armor, template configured for prompt injection and jailbreak detection at &lt;code&gt;LOW_AND_ABOVE&lt;/code&gt; — the most sensitive threshold available. Our test document contained an instruction addressed to whatever model would read it, telling it the deal had already been verified.&lt;/p&gt;

&lt;p&gt;Result: &lt;code&gt;NO_MATCH_FOUND&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before concluding anything, we checked the obvious explanations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was the template misconfigured?&lt;/strong&gt; No. A control jailbreak of the classic shape — an instruction to disregard prior instructions and reveal configuration — was caught by the same template, same threshold, same call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was it a language problem?&lt;/strong&gt; No. The injection was written in two languages and missed in both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Had we tuned the document to defeat the filter?&lt;/strong&gt; No. It was written before the filter existed, as part of a scenario about contradictory shipping paperwork. We never iterated it against the classifier. A separate document, deliberately written in the shape filters expect, is caught reliably.&lt;/p&gt;

&lt;p&gt;So: one configuration, two documents, opposite outcomes. That is a property of the classifier and its threshold, not of our setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is not a bug report
&lt;/h2&gt;

&lt;p&gt;Generic injection filters are trained on the attack that actually happens in the wild, which is an attack on an assistant: extract the system prompt, escalate a role, ignore previous instructions, pretend to be a different model. Those attacks have a recognisable register. They read like someone talking to a machine.&lt;/p&gt;

&lt;p&gt;A domain-worded instruction inside a plausible business document does not have that register. It reads like a clause. In a corpus of commercial paperwork, sentences that assert a document's own status are not anomalous — they are what commercial paperwork is made of.&lt;/p&gt;

&lt;p&gt;This is not a defect in the filter. It is the boundary of what a general-purpose classifier can be expected to know, and it becomes visible the moment you leave the assistant setting and enter a domain where the attacker writes in the domain's own voice.&lt;/p&gt;

&lt;p&gt;The practical consequence is that in a domain system, the probabilistic filter should not be carrying the load. It should be the cheap first pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually stopped it
&lt;/h2&gt;

&lt;p&gt;Our document agent does not return text. It returns an object with a strict schema: enums, numbers, dates. There is no free-text field anywhere in that contract. A quotation from the source document is represented as an offset and a length into the original file, not as a portable string.&lt;/p&gt;

&lt;p&gt;The injection was not neutralised. It was never delivered. There was no field it could travel in.&lt;/p&gt;

&lt;p&gt;That is a structural guarantee rather than a probabilistic one, and demonstrating it on a filter &lt;em&gt;miss&lt;/em&gt; is far more convincing than demonstrating it on a filter hit. Two independent barriers only mean something if you can show the second one working while the first one is failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then we found out the schema is also a prompt
&lt;/h2&gt;

&lt;p&gt;We had been saying, rather pleased with ourselves, that a prompt asks and a schema compels — implying that the schema is structure while the prompt is text.&lt;/p&gt;

&lt;p&gt;It is not. The framework hands the entire output schema to the model: field names, types, and every description string. When a translation pass rewrote our schema descriptions, it was rewriting text that reaches the model, and we very nearly shipped that as a documentation change.&lt;/p&gt;

&lt;p&gt;The boundary still holds, but for a different reason than we thought. Not because a schema is not text — but because &lt;em&gt;we&lt;/em&gt; write the schema, and the party under investigation writes the document. It is a question of authorship, not of data type.&lt;/p&gt;

&lt;p&gt;We now keep the complete set of text that ever reaches a model — eleven blocks — hashed and version-controlled. A test fails if any of it changes without a deliberate re-run of the demo scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  The third measurement, which settled the argument
&lt;/h2&gt;

&lt;p&gt;Our planner is a language model. It decides which checks a given deal needs. One of those checks is the sanctions list screening — the one that produces the verdict.&lt;/p&gt;

&lt;p&gt;On live runs, we measured how often the planner simply left that step out of its plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;31 of 34 completed runs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not an occasional lapse. The default behaviour on that particular input. And when the step was absent, nothing raised a gap, so the deal was recorded as clear — having never been screened at all. An affirmative statement about a deal the system never checked.&lt;/p&gt;

&lt;p&gt;The fix was not a firmer instruction. We had by then four independent measurements showing that a prompt guarantees nothing: the model omits a required field while describing it in its own rationale, it ignores a catalogue constraint, it skips a node the instruction demands.&lt;/p&gt;

&lt;p&gt;So the rule went into the execution path instead: a verdict may only rest on steps that actually executed. A clear verdict is a positive claim and requires positive evidence. Absence of findings and absence of a check became different outcomes, and the unsafe state stopped being representable.&lt;/p&gt;

&lt;p&gt;After that change, across the same 34 runs: &lt;strong&gt;verdicts that escaped the rule — zero.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The planner still skips the step nine times out of ten. It no longer matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where guarantees actually live
&lt;/h2&gt;

&lt;p&gt;Four levels, each further from the guarantee you want than it appears:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A prompt asks.&lt;/strong&gt; It is a request to a system with no obligation to comply, and we have four measurements of non-compliance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A schema constrains the shape of an answer&lt;/strong&gt; — and is itself text the model reads, so it constrains form, not meaning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A registry makes a capability available.&lt;/strong&gt; It does not make it chosen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A check in the execution path makes an outcome impossible.&lt;/strong&gt; Only this one is a guarantee.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the security conversation around agents happens at the first level. Most of the actual assurance is available only at the fourth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are not publishing
&lt;/h2&gt;

&lt;p&gt;We are not publishing the injection string. The shape is the point, and the shape is described above; a copy-pasteable payload that evades a shipping classifier is not a contribution. Everything else — the filter configuration, the control case, the counts, the protocols — is in the repository.&lt;/p&gt;

&lt;p&gt;All data in this project is synthetic: invented IMO numbers, invented companies, an invented list. No real vessel or organisation appears anywhere in it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Okimera is open source: &lt;a href="https://github.com/roogify/Okimera" rel="noopener noreferrer"&gt;https://github.com/roogify/Okimera&lt;/a&gt; — the measurement protocols are in &lt;code&gt;docs/proof/&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>security</category>
    </item>
  </channel>
</rss>
