<?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: oleg-vdv</title>
    <description>The latest articles on DEV Community by oleg-vdv (@olegvdv).</description>
    <link>https://dev.to/olegvdv</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%2F4032907%2Fed5d89d5-8281-416d-9a0e-654571c8c30c.png</url>
      <title>DEV Community: oleg-vdv</title>
      <link>https://dev.to/olegvdv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/olegvdv"/>
    <language>en</language>
    <item>
      <title>The move the agent is not allowed to make</title>
      <dc:creator>oleg-vdv</dc:creator>
      <pubDate>Sat, 19 Sep 2026 15:21:45 +0000</pubDate>
      <link>https://dev.to/olegvdv/the-move-the-agent-is-not-allowed-to-make-3906</link>
      <guid>https://dev.to/olegvdv/the-move-the-agent-is-not-allowed-to-make-3906</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path Two: Vibe-Code Something Strange&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A signing desk. One screen, one button, and the button does the single thing the agent in&lt;br&gt;
this system is forbidden to do.&lt;/p&gt;

&lt;p&gt;The project behind it answers compliance questions across regulations that contradict each&lt;br&gt;
other — the EU AI Act says keep your AI system logs for at least six months, the GDPR says&lt;br&gt;
keep personal data no longer than necessary, and the logs the AI Act mandates contain&lt;br&gt;
personal data by construction. There is no single number that satisfies both. There is a&lt;br&gt;
period someone has to choose and justify.&lt;/p&gt;

&lt;p&gt;So the interesting question is not how the agent answers. It is &lt;strong&gt;who is allowed to decide&lt;/strong&gt;,&lt;br&gt;
and how you stop the machine from quietly deciding for you.&lt;/p&gt;

&lt;p&gt;The answer here is a workflow stored as content beside the content it governs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raised → gathering → review → decided
                             ↘ dismissed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Five states, five transitions, and every transition names the actor permitted to make it.&lt;br&gt;
Two are open to an agent: start looking, attach the sources. Three are &lt;code&gt;human&lt;/code&gt;. Ask the&lt;br&gt;
dataset and it tells you:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deciding a conflict"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"states"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"agentCan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"humanOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The agent reads that same document. When it reaches the end of what it may do, it says so:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CANNOT Sign the decision (review → decided) — reserved for a person
       Only a person signs. The agent can prepare every part of this and still
       not make the move.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That is the strange part, and the part I actually care about. The boundary is not a sentence&lt;br&gt;
in a system prompt that a model can reason its way around on a bad day. It is a row in the&lt;br&gt;
dataset the model queries, rendered in the same Studio a person uses, enforced the same way&lt;br&gt;
for both.&lt;/p&gt;

&lt;p&gt;The signing desk is where the human side of that boundary lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it is for:&lt;/strong&gt; whoever ends up holding the compliance file at a small company — usually a&lt;br&gt;
founder or a lone engineer, not a legal department. They are the ones who get handed an&lt;br&gt;
auditor's question and have nobody to escalate it to. The agent does the reading; they do the&lt;br&gt;
deciding, and the point of the desk is that the split is visible rather than assumed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The app:&lt;/strong&gt; &lt;a href="https://www.sanity.io/@o7br4pucm/application/wpdwxiwyygohwmtn59yz92ap" rel="noopener noreferrer"&gt;https://www.sanity.io/@o7br4pucm/application/wpdwxiwyygohwmtn59yz92ap&lt;/a&gt;&lt;br&gt;
Deployed to the organisation dashboard. It requires a Sanity login — deliberately, since it&lt;br&gt;
writes signed decisions, and an anonymous signature is not one. If you are not logged in you&lt;br&gt;
will get Sanity's login screen rather than the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything else is public, no account needed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Studio: &lt;a href="https://clausewatch.sanity.studio/" rel="noopener noreferrer"&gt;https://clausewatch.sanity.studio/&lt;/a&gt; — open conflicts on the first screen&lt;/li&gt;
&lt;li&gt;The workflow itself, straight from the API:
&lt;code&gt;https://4yzoidsq.api.sanity.io/v2026-09-19/data/query/production?query=*[_type=="workflow"][0]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The repo runs with no credentials at all: &lt;code&gt;npm install &amp;amp;&amp;amp; npm run ask -- --workflow&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The desk lists whatever the dataset says is in &lt;code&gt;review&lt;/code&gt; — it holds no state of its own. For&lt;br&gt;
each conflict it shows both clauses verbatim with their citations, the steps the agent&lt;br&gt;
already took with timestamps and the actor on each, and then a form.&lt;br&gt;
If the list is empty when you open it, nothing is waiting: see the note under Sanity Project Details.&lt;/p&gt;

&lt;p&gt;The form asks for what was decided, why, and a name. The signature is &lt;strong&gt;typed&lt;/strong&gt;. The App SDK&lt;br&gt;
has no current-user hook, which turned out to be the right accident: a name that fills itself&lt;br&gt;
in is not a signature, and the whole reason the field exists is that a person chose to put&lt;br&gt;
their name against a reading of the law.&lt;/p&gt;

&lt;p&gt;Signing writes the decision and its transition record in one edit, so the state cannot move&lt;br&gt;
without the evidence of who moved it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpr7vbqg5mv3o7qmdtu36.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpr7vbqg5mv3o7qmdtu36.png" alt="The signing desk: a note saying only a person signs, the two conflicting clauses side by side with their citations, the two steps the agent already took, and a form with an empty signature line."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4jd7df3rsu8wm448xvg9.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4jd7df3rsu8wm448xvg9.JPG" alt="The signing form: a dropdown for what was decided, a box for why, and a blank line labelled 'Signed by'."&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/oleg-vdv/clausewatch" rel="noopener noreferrer"&gt;https://github.com/oleg-vdv/clausewatch&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;studio/   schema, desk structure, seed dataset
app/      the signing desk — Sanity App SDK
web/      the public viewer — Astro, server-rendered
agent/    MCP client, domain layer, the agent, the workflow gate
demo/     saved agent runs with their tool calls attached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The viewer is Astro with the Node adapter, rendered per request because every page is a live&lt;br&gt;
read of the dataset. It imports &lt;code&gt;agent/src/context.ts&lt;/code&gt; rather than keeping its own copy of&lt;br&gt;
the queries, so a page and an answer cannot disagree about the law.&lt;/p&gt;

&lt;p&gt;The workflow is four types: &lt;code&gt;workflow&lt;/code&gt; holding states and transitions, &lt;code&gt;workflowState&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;transition&lt;/code&gt; (which carries &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;to&lt;/code&gt;, &lt;code&gt;actor&lt;/code&gt; and the fields a move requires), and&lt;br&gt;
&lt;code&gt;transitionRecord&lt;/code&gt; for what actually happened. A &lt;code&gt;conflict&lt;/code&gt; carries its &lt;code&gt;state&lt;/code&gt; and a&lt;br&gt;
&lt;code&gt;history[]&lt;/code&gt; of records.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agent/src/gate.ts&lt;/code&gt; computes the permitted moves for an actor by reading the workflow. The&lt;br&gt;
same function serves both sides, and the same transition blocks them differently:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;as the agent:  CANNOT Sign the decision — reserved for a person
as the human:  CANNOT Sign the decision — needs rationale, decidedBy, decidedAt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One table, two actors, no second set of rules to keep in sync.&lt;/p&gt;
&lt;h2&gt;
  
  
  My Build Process
&lt;/h2&gt;

&lt;p&gt;Claude Code, in the desktop app, in one sitting. The whole session is 485 messages and it is&lt;br&gt;
public — including the parts I would rather it were not. What follows is the honest version.&lt;/p&gt;
&lt;h3&gt;
  
  
  The prompts that worked were the ones that refused to guess
&lt;/h3&gt;

&lt;p&gt;The single most useful instruction I gave, early, was to treat the dataset as the authority&lt;br&gt;
and stop inventing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never fill a gap from your own legal knowledge. If it is not in the dataset or the&lt;br&gt;
knowledge base, say that it is not there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That went into the Sanity Context endpoint instructions rather than into my chat, so it&lt;br&gt;
reaches the model before it sees any data. Everything good about the output traces back to&lt;br&gt;
it. The agent's answers started saying "I cannot tell you" in the right places, and once that&lt;br&gt;
happened the product's shape became obvious — if the machine will admit what it does not&lt;br&gt;
know, the interesting design question is what to do with the gap. Hence the signing desk.&lt;/p&gt;
&lt;h3&gt;
  
  
  Where it went wrong, in order
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;It published my API token to a public GitHub repo.&lt;/strong&gt; I asked it to put the project on&lt;br&gt;
GitHub; it created the repo, pushed, and &lt;em&gt;then&lt;/em&gt; ran a secret scan — which found a live Sanity&lt;br&gt;
token sitting in &lt;code&gt;.env.example&lt;/code&gt;, where I had pasted it instead of into &lt;code&gt;.env&lt;/code&gt;. The repo went&lt;br&gt;
private within seconds, the token was revoked and replaced, and the history was rewritten to&lt;br&gt;
a single clean commit. The order was the bug: the scan should have run before the push, and&lt;br&gt;
the model said so itself afterwards. I am leaving this in the writeup because a build log&lt;br&gt;
that only contains the parts that went well is not a build log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It confidently corrected Sanity, and was wrong.&lt;/strong&gt; Sanity Context flagged a real&lt;br&gt;
contradiction while indexing: an entry claimed Annex I of the AI Act lists 21 harmonisation&lt;br&gt;
instruments where the source shows 20. Context also proposed a Section A/B split of 12 and 8.&lt;br&gt;
Claude told me that split was wrong. Then it checked the actual page, found its own reading&lt;br&gt;
was the unverified one, and said so plainly. The decision recorded in the dataset now states&lt;br&gt;
only the verified part and excludes the arithmetic neither of us could confirm, because&lt;br&gt;
EUR-Lex blocks automated retrieval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A patch script reported success and had changed nothing.&lt;/strong&gt; It edited three files with a&lt;br&gt;
Python script that did not verify its replacements; one silently missed, and &lt;code&gt;--check&lt;/code&gt; kept&lt;br&gt;
printing two empty lists. It was caught only because I ran the thing from a fresh clone. The&lt;br&gt;
lesson is not "models are careless" — it is that an edit which cannot fail loudly will&lt;br&gt;
eventually fail quietly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three corpus rebuilds before the knowledge base was right.&lt;/strong&gt; &lt;code&gt;/article/*&lt;/code&gt; as an include&lt;br&gt;
pattern did not filter at all: 200 pages including Polish and French translations of the same&lt;br&gt;
articles. Turning on "Sitemap only" made it worse — 691 documents, because that switch&lt;br&gt;
overrides include patterns rather than narrowing them. Exact paths worked first time and&lt;br&gt;
every time. The corpus is now 26 deliberately chosen documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the bug I would not have found by reading the code.&lt;/strong&gt; The role filter used&lt;br&gt;
&lt;code&gt;count(appliesToRoles) == 0&lt;/code&gt; to mean "binds everyone". In GROQ, &lt;code&gt;count()&lt;/code&gt; of a missing field&lt;br&gt;
is &lt;code&gt;null&lt;/code&gt;, not &lt;code&gt;0&lt;/code&gt; — so every GDPR claim silently vanished. The provider's report looked&lt;br&gt;
perfect. Only the deployer's was wrong, and only because the two profiles differ.&lt;/p&gt;
&lt;h3&gt;
  
  
  The course-correction that mattered most
&lt;/h3&gt;

&lt;p&gt;Two-thirds of the way in I asked it to check the submission against the challenge rules. It&lt;br&gt;
came back with something I had not asked about: a judge cannot hold our organisation token,&lt;br&gt;
so cloning the repo and running it ended at &lt;code&gt;SANITY_CONTEXT_TOKEN is not set&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We split the client. With a token it goes through the Context MCP endpoints; without one it&lt;br&gt;
reads the same public dataset over the query API and says so in the footer and in &lt;code&gt;--check&lt;/code&gt;,&lt;br&gt;
rather than crediting a source it never touched. Then it cloned from GitHub into a temp&lt;br&gt;
directory and ran it with no credentials to prove the fix, which is the check I would have&lt;br&gt;
skipped.&lt;/p&gt;
&lt;h3&gt;
  
  
  Reaching past the Studio
&lt;/h3&gt;

&lt;p&gt;Both of the things the brief said it wanted to see, and they turned out to be one thing.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;workflow&lt;/strong&gt; came first, because the process already existed and was hiding in an enum:&lt;br&gt;
a conflict had a &lt;code&gt;resolution&lt;/code&gt; field and nothing that said who was allowed to set it. Pulling&lt;br&gt;
it out into states and transitions took an afternoon and immediately paid for itself — the&lt;br&gt;
agent stopped needing to be told what it may not do, because it could query it.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;App SDK&lt;/strong&gt; app followed from that. Once &lt;code&gt;review → decided&lt;/code&gt; was marked &lt;code&gt;human&lt;/code&gt;, there had&lt;br&gt;
to be somewhere a human makes that move, and the Studio was the wrong place: the Studio is&lt;br&gt;
where you edit a document, not where you sign one. The app is 200 lines, reads the workflow&lt;br&gt;
and the conflicts with &lt;code&gt;useQuery&lt;/code&gt;, and writes with &lt;code&gt;useEditDocument&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;useApplyDocumentActions&lt;/code&gt;. It holds no state of its own, which is the property I wanted —&lt;br&gt;
close the tab mid-decision and nothing is half-saved.&lt;/p&gt;
&lt;h3&gt;
  
  
  One undocumented thing, for whoever hits it next
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sanity dev&lt;/code&gt; for an App SDK app binds IPv6 only. The port listens on &lt;code&gt;[::1]:3333&lt;/code&gt; and&lt;br&gt;
anything resolving localhost to &lt;code&gt;127.0.0.1&lt;/code&gt; — curl, most preview tooling — sees a dead port&lt;br&gt;
and a blank page with no error. &lt;code&gt;server: {hostname: '127.0.0.1'}&lt;/code&gt; in &lt;code&gt;sanity.cli.ts&lt;/code&gt; fixes it.&lt;/p&gt;

&lt;p&gt;Also worth knowing: token permissions cannot be edited after creation. Deploying an App SDK&lt;br&gt;
app needs &lt;strong&gt;Manage SDK Apps&lt;/strong&gt;, so if your deploy token lacks it, you are making a new token.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project ID:&lt;/strong&gt; &lt;code&gt;4yzoidsq&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset:&lt;/strong&gt; &lt;code&gt;production&lt;/code&gt; (public)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Studio:&lt;/strong&gt; &lt;a href="https://clausewatch.sanity.studio/" rel="noopener noreferrer"&gt;https://clausewatch.sanity.studio/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App:&lt;/strong&gt; &lt;a href="https://www.sanity.io/@o7br4pucm/application/wpdwxiwyygohwmtn59yz92ap" rel="noopener noreferrer"&gt;https://www.sanity.io/@o7br4pucm/application/wpdwxiwyygohwmtn59yz92ap&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workflow and the conflicts it governs, no token required:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://4yzoidsq.api.sanity.io/v2026-09-19/data/query/production?query=*[_type=="workflow"][0]{name,states,transitions}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://4yzoidsq.api.sanity.io/v2026-09-19/data/query/production?query=*[_type=="conflict"]{summary,state,decidedBy,"moves":history[]{to,actor,actorKind}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two of the three conflicts are decided, and the second one was signed through the app in&lt;br&gt;
this post rather than seeded that way. Its history reads:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raised    → gathering   clausewatch-agent (agent)
gathering → review      clausewatch-agent (agent)
review    → decided     Oleg Vdovin (human)   2026-09-19 15:25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Which means that if you open the signing desk now it will tell you nothing is waiting for a&lt;br&gt;
signature. That is the desk working, not the desk broken: the conflict in the screenshots&lt;br&gt;
above is the one that was signed. The third conflict is still &lt;code&gt;raised&lt;/code&gt; and untouched, so the&lt;br&gt;
agent has somewhere to go next.&lt;/p&gt;
&lt;h2&gt;
  
  
  Agent Session
&lt;/h2&gt;

&lt;p&gt;The whole build, 485 messages. I work in Russian, so the conversation is Russian; the tool&lt;br&gt;
calls, the queries, the terminal output and the model's own English prose are not, and the&lt;br&gt;
slices below are made of those. Each card opens collapsed — click the ▸ for the output.&lt;/p&gt;
&lt;h3&gt;
  
  
  The bug that only showed up for one profile
&lt;/h3&gt;

&lt;p&gt;Terminal output. An import reports success, a document fetch comes back&lt;br&gt;
&lt;code&gt;{"documents":[],"omitted":[{"reason":"permission"}]}&lt;/code&gt;, a one-document probe proves the&lt;br&gt;
document ids are the cause, and an anonymous &lt;code&gt;count(*)&lt;/code&gt; of 18 confirms the fix. This is the&lt;br&gt;
moment the project stopped being provably public and started being actually public.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Claude Code"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building ClauseWatch: an agent over contradictory AI regulation&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;cd /c/Users/Admin/projects/clausewatch/studio &amp;amp;&amp;amp; set -a; . ../.env; set +a
ec...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ cd /c/Users/Admin/projects/clausewatch/studio &amp;amp;&amp;amp; set -a; . ../.env; set +a
echo '{"_id":"anontest-nodots","_type":"jurisdiction","name":"Anon probe","code":"ZZ"}' &amp;gt; /tmp/probe.ndjson
npx sanity dataset import /tmp/probe.ndjson production --replace 2&amp;gt;&amp;amp;1 | tail -2
sleep 2
echo "=== anonymous query ==="
curl -s "https://4yzoidsq.api.sanity.io/v2026-09-19/data/query/production?query=$(node -e 'process.stdout.write(encodeURIComponent(`*[_type=="jurisdiction"]{_id,name}`))')"; echo&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Done! Imported 1 documents to dataset "production"

=== anonymous query ===
{"query":"*[_type==\"jurisdiction\"]{_id,name}","result":[{"_id":"anontest-nodots","name":"Anon probe"}],"syncTags":["s1:pQbSuQ"],"ms":1}
Shell cwd was reset to C:\Users\Admin\Documents&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        1 messages (range 114-114) of 485 total
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  Checking a citation against the source
&lt;/h3&gt;

&lt;p&gt;Thirty seconds that decided how the whole content model works. A script pulls Article 26 and&lt;br&gt;
prints its numbered paragraphs: paragraph 5 is the duty to monitor, paragraph 6 is the duty&lt;br&gt;
to keep logs. The knowledge base had attributed the log duty to 26(5) — its own section&lt;br&gt;
numbering read as paragraph numbering — and the structured layer is where the verified&lt;br&gt;
citation now lives.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Claude Code"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building ClauseWatch: an agent over contradictory AI regulation&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;node "/c/Users/Admin/AppData/Local/Temp/claude/C--Users-Admin-Documents/ddd0a...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ node "/c/Users/Admin/AppData/Local/Temp/claude/C--Users-Admin-Documents/ddd0a186-49e0-40f7-be9b-b7de1c0d2f4d/scratchpad/art26.mjs" 2&amp;gt;&amp;amp;1 | head -40&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;[para 1] . Deployers of high-risk AI systems shall take appropriate technical and organisational measures to ensure they use such systems in accordance with the instructions for use instructions for use means the information provided by the provider provider means a

[para 2] . Deployers shall assign human oversight to natural persons who have the necessary competence, training and authority, as well as the necessary support. 3. The obligations set out in paragraphs 1 and 2, are without prejudice to other deployer deployer means

[para 4] . Without prejudice to paragraphs 1 and 2, to the extent the deployer deployer means a natural or legal person, public authority, agency or other body using an AI system under its authority except where the AI system is used in the course of a personal non-p

[para 5] . Deployers shall monitor the operation of the high-risk AI system AI system means a machine-based system that is designed to operate with varying levels of autonomy and that may exhibit adaptiveness after deployment, and that, for explicit or implicit objec

[para 6] . Deployers of high-risk AI systems shall keep the logs automatically generated by that high-risk AI system AI system means a machine-based system that is designed to operate with varying levels of autonomy and that may exhibit adaptiveness after deployment,

[para 8] . Deployers of high-risk AI systems that are public authorities, or Union institutions, bodies, offices or agencies shall comply with the registration obligations referred to in Article 49 . When such deployers find that the high-risk AI system AI system mea&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        1 messages (range 313-313) of 485 total
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;The app requires a Sanity login. That is deliberate for a tool that writes signed decisions,&lt;br&gt;
but it does mean a judge without an account sees a login screen rather than the desk — hence&lt;br&gt;
the screenshots above and the public artefacts below them.&lt;/p&gt;

&lt;p&gt;The dataset is small: 2 instruments, 9 provisions, 2 requirements, 3 conflicts, 2 system&lt;br&gt;
profiles, 1 workflow. It is a working core that demonstrates the model, not a compliance&lt;br&gt;
product. Nothing here is legal advice.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>ClauseWatch: an agent that refuses to give you one number</title>
      <dc:creator>oleg-vdv</dc:creator>
      <pubDate>Sat, 19 Sep 2026 13:21:05 +0000</pubDate>
      <link>https://dev.to/olegvdv/clausewatch-an-agent-that-refuses-to-give-you-one-number-19ke</link>
      <guid>https://dev.to/olegvdv/clausewatch-an-agent-that-refuses-to-give-you-one-number-19ke</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path One: Ship an Agent That Queries Real Content&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Ask any model how long you must keep AI system logs under the EU AI Act. It will say six&lt;br&gt;
months. That is true, and it is not the answer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI Act Art. 19&lt;/strong&gt; makes the &lt;strong&gt;provider&lt;/strong&gt; keep logs at least six months.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Act Art. 26(6)&lt;/strong&gt; puts the same floor on the &lt;strong&gt;deployer&lt;/strong&gt; — a separate duty on a
separate party.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GDPR Art. 5(1)(e)&lt;/strong&gt; says personal data may be kept &lt;strong&gt;no longer than necessary&lt;/strong&gt;, and
names no number at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Act Art. 12(3)&lt;/strong&gt; guarantees the collision: for biometric systems the log is &lt;em&gt;required&lt;/em&gt;
to record the identity of the humans who verified a match. The log the AI Act mandates is
personal data by construction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One instrument sets a floor. Another sets a ceiling. Neither says where the other one sits.&lt;br&gt;
The honest answer is a period you can justify against both — plus a record of who decided it.&lt;/p&gt;

&lt;p&gt;ClauseWatch gives that answer. It reads a structured model of obligations and the prose of the&lt;br&gt;
instruments behind them, and when two sources pull in opposite directions it shows both with&lt;br&gt;
their citations and reports that the decision is &lt;strong&gt;unmade&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Both clauses stand until someone signs. The dataset keeps a &lt;code&gt;decidedBy&lt;/code&gt; and a &lt;code&gt;decidedAt&lt;/code&gt;&lt;br&gt;
field for exactly that, and they are empty.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the part that needed structured content. A keyword search over the same texts returns&lt;br&gt;
Art. 19 and stops. It cannot know that a clause in a &lt;em&gt;different regulation&lt;/em&gt; constrains the&lt;br&gt;
same artifact from the opposite direction, because that fact is not inside either document.&lt;br&gt;
It is in the relationship between them — which is to say, in the schema.&lt;/p&gt;
&lt;h2&gt;
  
  
  The control: I ran the keyword search
&lt;/h2&gt;

&lt;p&gt;The brief sets a bar — &lt;em&gt;if a keyword search would have gotten you the same answer, aim&lt;br&gt;
higher&lt;/em&gt; — so rather than assert we cleared it, here is the other arm of the experiment.&lt;br&gt;
Ordinary tf-idf over the same clause texts the knowledge base is built from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npm run ask &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--baseline&lt;/span&gt; &lt;span class="s2"&gt;"how long must I keep automatically generated logs"&lt;/span&gt;
&lt;span class="go"&gt;
  0.2763  EU AI Act Art. 19     — Automatically generated logs
  0.2763  EU AI Act Art. 26(6)  — Deployer duty to keep automatically generated logs
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two clauses, &lt;strong&gt;identical scores&lt;/strong&gt;. Ranking cannot break that tie, because which one binds&lt;br&gt;
you is not a property of either text — it depends on whether you are the provider or the&lt;br&gt;
deployer, which is a fact about you.&lt;/p&gt;

&lt;p&gt;And GDPR Art. 5(1)(e) is not in the results at all. It is the clause that makes the answer&lt;br&gt;
wrong if you ignore it, and it is invisible to this query because it never says &lt;em&gt;logs&lt;/em&gt;. It&lt;br&gt;
says &lt;em&gt;personal data&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npm run ask &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--baseline&lt;/span&gt; &lt;span class="s2"&gt;"how long may I keep personal data"&lt;/span&gt;
&lt;span class="go"&gt;
  0.2865  GDPR Art. 5(1)(e)     — Storage limitation
  0.1066  EU AI Act Art. 26(6)  — Deployer duty to keep automatically generated logs
  0.0759  EU AI Act Art. 19     — Automatically generated logs
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The two halves of the answer live under different vocabularies. Finding both requires&lt;br&gt;
already knowing both exist, which is the question you were asking in the first place.&lt;/p&gt;

&lt;p&gt;Asking about the collision directly returns one clause, because the collision is not in any&lt;br&gt;
document. It is a relationship between two of them, and a flat index of documents has&lt;br&gt;
nowhere to put a relationship. That is the whole argument for modelling it, and it is now a&lt;br&gt;
command anyone can run rather than a claim in a blog post: the full comparison is in&lt;br&gt;
&lt;a href="https://github.com/oleg-vdv/clausewatch/blob/main/demo/05-keyword-baseline.md" rel="noopener noreferrer"&gt;&lt;code&gt;demo/05-keyword-baseline.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;git clone &lt;a href="https://github.com/oleg-vdv/clausewatch" rel="noopener noreferrer"&gt;https://github.com/oleg-vdv/clausewatch&lt;/a&gt; &amp;amp;&amp;amp; cd clausewatch/web&lt;br&gt;
npm install &amp;amp;&amp;amp; npm run dev       # &lt;a href="http://localhost:4173" rel="noopener noreferrer"&gt;http://localhost:4173&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No credentials.&lt;/strong&gt; No login, no token, no API key. The dataset is public, so a fresh clone&lt;br&gt;
reads it over the public query API and renders the full report. The knowledge base and the&lt;br&gt;
LLM agent do need a Context token, which you cannot have — so &lt;code&gt;--check&lt;/code&gt; and the page footer&lt;br&gt;
say which of the two sources the answer came from, rather than crediting one they never&lt;br&gt;
touched.&lt;/p&gt;

&lt;p&gt;It also runs with no model at all:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../agent &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run ask &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; biometric-access &lt;span class="nt"&gt;--no-llm&lt;/span&gt;   &lt;span class="c"&gt;# provider, high-risk, KZ → EU&lt;/span&gt;
npm run ask &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; support-agent &lt;span class="nt"&gt;--no-llm&lt;/span&gt;      &lt;span class="c"&gt;# deployer, risk class unset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two elements of the viewer do the arguing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pressure bar&lt;/strong&gt; — a solid edge on the left where Art. 19 states six months, and a&lt;br&gt;
hatched, edgeless right where the GDPR caps the period without naming one. Drawing a tidy&lt;br&gt;
range there would be a lie about the law.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4mpscd8jv3ct2bd9vxk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4mpscd8jv3ct2bd9vxk.png" alt="A horizontal bar. Its left edge is a solid green line labelled 'at least 6 months, EU AI Act Art. 26(6)'; its right edge is a red line over hatching, labelled 'no stated number, GDPR Art. 5(1)(e)'. Across the middle: you choose a period in here, and justify it against both."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The signature block&lt;/strong&gt; — every conflict prints a &lt;em&gt;decided by&lt;/em&gt; and a &lt;em&gt;date&lt;/em&gt; rule. Resolved,&lt;br&gt;
they carry a name and a date. Open, they are two empty lines in a compliance report. The&lt;br&gt;
thesis as a form field rather than a paragraph.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4mpscd8jv3ct2bd9vxk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4mpscd8jv3ct2bd9vxk.png" alt="A panel headed UNDECIDED, QUANTITY: the AI Act sets a six-month floor for log retention while the GDPR sets a necessity ceiling on the same logs, citing EU AI Act Art. 19 and GDPR Art. 5(1)(e). Below it two blank signature rules labelled Decided by and Date, and the line: Both clauses stand until someone signs."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Studio is live at &lt;strong&gt;&lt;a href="https://clausewatch.sanity.studio/" rel="noopener noreferrer"&gt;https://clausewatch.sanity.studio/&lt;/a&gt;&lt;/strong&gt;, with open conflicts on the first&lt;br&gt;
screen, because that is the one thing an editor of this dataset actually does.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/oleg-vdv/clausewatch" rel="noopener noreferrer"&gt;https://github.com/oleg-vdv/clausewatch&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;studio/   schema, desk structure, seed dataset
agent/    MCP client, domain layer, LLM agent, deterministic report
web/      the viewer — Astro, server-rendered, sharing the agent's domain layer
demo/     saved runs with their tool calls attached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  The content model
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source        an instrument: binding status, official URL, version label
provision     one citable unit: "Art. 19", verbatim text, effective date, roles bound
requirement   a normalised obligation: "retain automatically generated logs"
  └ claim[]   what each instrument asserts — direction (floor/ceiling/none), period if
              stated, condition, and a reference to its provision
conflict      two clauses that cannot both be satisfied, plus resolution, rationale,
              decidedBy, decidedAt
systemProfile roles, jurisdictions, risk class, agentic or not
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three decisions carry the weight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claims sit apart from requirements.&lt;/strong&gt; One obligation, many instruments, and disagreement&lt;br&gt;
between them is content — not a data-quality problem to be cleaned up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;direction&lt;/code&gt; on a claim.&lt;/strong&gt; An earlier version bucketed claims by &lt;em&gt;does it state a number&lt;/em&gt;,&lt;br&gt;
which filed GDPR Art. 5(1)(e) under silence. It is not silent: it caps the period without&lt;br&gt;
naming one. Floor, ceiling and duty-only are three different answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A conflict holds a decision, not a resolution rule.&lt;/strong&gt; An agent that silently picks the&lt;br&gt;
stricter number is guessing on your behalf. One that shows both sides and cites a named,&lt;br&gt;
dated decision produces something an auditor can accept.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What I pointed Sanity Context at
&lt;/h3&gt;

&lt;p&gt;Two website sources, both crawled to exact paths rather than wildcards:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Paths&lt;/th&gt;
&lt;th&gt;Documents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;artificialintelligenceact.eu&lt;/td&gt;
&lt;td&gt;Arts. 3, 6, 9, 11–14, 16–21, 26, 27, 72, 73, 99 and Annexes I, III, IV&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gdpr-info.eu&lt;/td&gt;
&lt;td&gt;Arts. 5, 17, 25, 30, 32&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;26 documents, indexed into &lt;strong&gt;10 entries&lt;/strong&gt;. Curated on purpose — and also capped, because the&lt;br&gt;
Context beta allows 150 indexed documents on this plan and I hit that twice before narrowing&lt;br&gt;
properly.&lt;/p&gt;

&lt;p&gt;The indexing surprised me. Entries are topic nodes rather than chunks: each carries an article&lt;br&gt;
range, a topic list, cross-references (&lt;code&gt;excludes: … see &amp;lt;other entry&amp;gt;&lt;/code&gt;) and a numbered Sources&lt;br&gt;
block with a URL behind every statement.&lt;/p&gt;
&lt;h3&gt;
  
  
  Which Context tools I used
&lt;/h3&gt;

&lt;p&gt;Two endpoints, because &lt;strong&gt;one endpoint serves one mode&lt;/strong&gt;. Attaching a Knowledge Base to an&lt;br&gt;
endpoint that already served a dataset replaced the GROQ tools entirely.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clausewatch-data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;initial_context&lt;/code&gt;, &lt;code&gt;groq_query&lt;/code&gt;, &lt;code&gt;schema_explorer&lt;/code&gt;, &lt;code&gt;array_field_reader&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;which&lt;/em&gt; obligations bind this system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clausewatch-docs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;initial_context&lt;/code&gt;, &lt;code&gt;knowledge_base_read&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;what the clause says&lt;/em&gt;, with its source link&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both carry the same endpoint Instructions — the citation discipline the agent must follow,&lt;br&gt;
delivered before it sees any data. The rule that matters most:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never fill a gap from your own legal knowledge. If it is not in the dataset or the knowledge&lt;br&gt;
base, say that it is not there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Beyond the tools I leaned on two Context features: &lt;strong&gt;Issues&lt;/strong&gt;, which surfaced a contradiction&lt;br&gt;
in the corpus, and &lt;strong&gt;Instructions&lt;/strong&gt;, which corrected a fact at the source instead of in my&lt;br&gt;
code. Both stories are below.&lt;/p&gt;
&lt;h3&gt;
  
  
  What the agent actually did with it
&lt;/h3&gt;

&lt;p&gt;A real tool-use loop. Nothing is pre-fetched: the model gets the two endpoints and has to go&lt;br&gt;
and look. A typical run:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. initial_context         (dataset — schema and the citation rules)
2. kb_initial_context      (knowledge base — the outline of 10 entries)
3. groq_query              (the logging requirement, its claims, its conflicts)
4. kb_knowledge_base_read  (design_documentation, post_market_obligations)
5. groq_query              (jurisdictions, extraterritoriality)
6. kb_knowledge_base_read  (high_risk_categories)
7. groq_query              (system profiles)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Seven calls across both endpoints. The answer states it cannot give one number, shows the&lt;br&gt;
floor and the ceiling with citations and effective dates, reports the conflict as open with&lt;br&gt;
nobody named, and notes the obligation does not bite until 2 December 2027. In a second run&lt;br&gt;
the agent found the matching system profile unprompted, treated &lt;code&gt;riskClass: unknown&lt;/code&gt; as&lt;br&gt;
load-bearing, separated provider duties from deployer ones, and closed with a section titled&lt;br&gt;
&lt;em&gt;What I can't answer from these sources&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Saved runs with their tool calls are in&lt;br&gt;
&lt;a href="https://github.com/oleg-vdv/clausewatch/tree/main/demo" rel="noopener noreferrer"&gt;&lt;code&gt;demo/&lt;/code&gt;&lt;/a&gt;. The answer alone cannot&lt;br&gt;
show that a number came from the endpoints rather than from the model, so the tool calls&lt;br&gt;
travel with it.&lt;/p&gt;

&lt;p&gt;The dataset side does the reasoning that a prose search cannot: claims are filtered by the&lt;br&gt;
jurisdictions a system touches &lt;strong&gt;and&lt;/strong&gt; the roles it holds, so a deployer is shown Art. 26(6)&lt;br&gt;
and a provider Art. 19. Telling a deployer that Art. 19 is their duty is not a rounding error,&lt;br&gt;
it is the wrong party.&lt;/p&gt;
&lt;h3&gt;
  
  
  Context found a contradiction I had not
&lt;/h3&gt;

&lt;p&gt;While indexing, Context flagged that an entry claimed Annex I lists 21 harmonisation&lt;br&gt;
instruments while the source shows 20 — item 1 deleted by amendment, item 21 added. I checked&lt;br&gt;
against the source: true. It also proposed a Section A/B split that I could &lt;strong&gt;not&lt;/strong&gt; verify,&lt;br&gt;
because EUR-Lex blocks automated retrieval.&lt;/p&gt;

&lt;p&gt;So the decision recorded in the dataset says the detection was right, states only the verified&lt;br&gt;
part, explicitly excludes the unverified arithmetic, and carries a name and a date.&lt;/p&gt;

&lt;p&gt;That is the whole product in one incident. Automated detection is good at &lt;em&gt;finding&lt;/em&gt;&lt;br&gt;
disagreement and is not authoritative about &lt;em&gt;resolving&lt;/em&gt; it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Where prose alone gets a citation wrong
&lt;/h3&gt;

&lt;p&gt;In one run the agent cited &lt;strong&gt;Art. 26(5)&lt;/strong&gt; for the deployer's log-retention duty, from the&lt;br&gt;
knowledge base. The source says &lt;strong&gt;Art. 26(6)&lt;/strong&gt;; 26(5) is the monitoring duty. A one-digit&lt;br&gt;
error that reads as correct.&lt;/p&gt;

&lt;p&gt;Chasing it was the most useful hour of the build. The knowledge-base entry for Article 26&lt;br&gt;
numbers its own sections, and the fifth one is headed &lt;code&gt;### 5. Log retention&lt;/code&gt;. Section five of&lt;br&gt;
an entry about Article 26 — and a model reading it produced "Art. 26(5)". The entry states the&lt;br&gt;
rule itself correctly, and &lt;strong&gt;gives no paragraph number at all&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Deployers shall keep automatically generated logs for a period appropriate to the intended&lt;br&gt;
purpose, with a minimum of six months, unless applicable Union or national law … provides&lt;br&gt;
otherwise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the argument for two layers rather than one. Prose is right about the &lt;em&gt;rule&lt;/em&gt; and&lt;br&gt;
silent on the &lt;em&gt;address&lt;/em&gt;; the dataset carries the address, verified by a human against the&lt;br&gt;
source. Art. 26(6) is now a provision with the checked citation, its own claim on the&lt;br&gt;
retention requirement, and the role it binds — which is why a deployer profile is shown&lt;br&gt;
Art. 26(6) and a provider Art. 19.&lt;/p&gt;

&lt;p&gt;I also added a Context &lt;strong&gt;Instruction&lt;/strong&gt;, which is &lt;em&gt;honored on every build, over the raw&lt;br&gt;
sources&lt;/em&gt;, so a future rebuild cannot reintroduce the wrong number. Being precise about what&lt;br&gt;
I have and have not verified: that Instruction has not been exercised yet. The corpus has not&lt;br&gt;
changed since, so Context reports entries up to date and no rebuild has run.&lt;/p&gt;

&lt;p&gt;The layers check each other in both directions. In another run the agent found a duty present&lt;br&gt;
in the prose that my dataset had not modelled at all, and said so.&lt;/p&gt;

&lt;p&gt;Here is the check itself, thirty seconds of it: a script pulls Article 26 from the source and&lt;br&gt;
prints its numbered paragraphs. Paragraph 5 is the duty to monitor. Paragraph 6 is the duty to&lt;br&gt;
keep the logs. That is the whole difference between citing the right clause and the wrong one,&lt;br&gt;
and nothing short of going to the text would have shown it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Embedded session cards open collapsed — click the ▸ to see the command and everything it&lt;br&gt;
printed. Same for the three further down.&lt;/em&gt;&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Claude Code"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building ClauseWatch: an agent over contradictory AI regulation&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;node "/c/Users/Admin/AppData/Local/Temp/claude/C--Users-Admin-Documents/ddd0a...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ node "/c/Users/Admin/AppData/Local/Temp/claude/C--Users-Admin-Documents/ddd0a186-49e0-40f7-be9b-b7de1c0d2f4d/scratchpad/art26.mjs" 2&amp;gt;&amp;amp;1 | head -40&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;[para 1] . Deployers of high-risk AI systems shall take appropriate technical and organisational measures to ensure they use such systems in accordance with the instructions for use instructions for use means the information provided by the provider provider means a

[para 2] . Deployers shall assign human oversight to natural persons who have the necessary competence, training and authority, as well as the necessary support. 3. The obligations set out in paragraphs 1 and 2, are without prejudice to other deployer deployer means

[para 4] . Without prejudice to paragraphs 1 and 2, to the extent the deployer deployer means a natural or legal person, public authority, agency or other body using an AI system under its authority except where the AI system is used in the course of a personal non-p

[para 5] . Deployers shall monitor the operation of the high-risk AI system AI system means a machine-based system that is designed to operate with varying levels of autonomy and that may exhibit adaptiveness after deployment, and that, for explicit or implicit objec

[para 6] . Deployers of high-risk AI systems shall keep the logs automatically generated by that high-risk AI system AI system means a machine-based system that is designed to operate with varying levels of autonomy and that may exhibit adaptiveness after deployment,

[para 8] . Deployers of high-risk AI systems that are public authorities, or Union institutions, bodies, offices or agencies shall comply with the registration obligations referred to in Article 49 . When such deployers find that the high-risk AI system AI system mea&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        1 messages (range 313-313) of 485 total
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  Five things that cost me hours
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Document ids containing dots are invisible to anonymous readers&lt;/strong&gt;, even in a public
dataset. The CLI said 18 imported, an authenticated &lt;code&gt;count(*)&lt;/code&gt; said 31, an anonymous one
said 0. Sanity treats &lt;code&gt;_id&lt;/code&gt; as a path and public read covers the root path only. An
authenticated count is not evidence that your dataset is public.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wildcard include patterns do not filter a website source; exact paths do.&lt;/strong&gt; &lt;code&gt;/article/*&lt;/code&gt;
pulled 200 pages including Polish and French translations of the same articles. For a legal
agent a translation is a correctness hazard, not noise — and the sitemap is the tell: if a
crawl returns more pages than the sitemap lists, it followed in-page language links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Sitemap only" overrides include patterns&lt;/strong&gt; rather than narrowing them. 691 documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An entry outlives its sources.&lt;/strong&gt; Delete a source and the prose stays while every citation
becomes &lt;code&gt;_source no longer available&lt;/code&gt; — and the entry is still listed in &lt;code&gt;initial_context&lt;/code&gt;
for the agent to read and cite. Dismissing the issue keeps the entry; only a rebuild cleared
them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context rewrites your GROQ&lt;/strong&gt;, which changes result shapes: &lt;code&gt;roles[]-&amp;gt;name&lt;/code&gt; comes back as
&lt;code&gt;[{name, _id}]&lt;/code&gt;. It also injects &lt;code&gt;_type != "sanity.agentContext"&lt;/code&gt; and pins
&lt;code&gt;perspective: published&lt;/code&gt;. Worth knowing when a query behaves differently in Vision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one that is nobody's fault but mine: &lt;code&gt;count()&lt;/code&gt; of a missing field is &lt;strong&gt;null, not 0&lt;/strong&gt;, so a&lt;br&gt;
role filter written as &lt;code&gt;count(appliesToRoles) == 0&lt;/code&gt; for &lt;em&gt;binds everyone&lt;/em&gt; silently dropped every&lt;br&gt;
GDPR claim. It looked fine in the provider's report and only broke for the deployer.&lt;/p&gt;
&lt;h3&gt;
  
  
  Watching the first one happen
&lt;/h3&gt;

&lt;p&gt;The numbers are the whole story, so here is the terminal output: an import that reports&lt;br&gt;
success, a document fetch that comes back &lt;code&gt;{"documents":[],"omitted":[{"reason":"permission"}]}&lt;/code&gt;,&lt;br&gt;
a one-document probe with a plain id that proves the ids are the cause, and a final anonymous&lt;br&gt;
&lt;code&gt;count(*)&lt;/code&gt; of 18 once everything was renamed to hyphens.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Claude Code"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building ClauseWatch: an agent over contradictory AI regulation&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;cd /c/Users/Admin/projects/clausewatch/studio &amp;amp;&amp;amp; set -a; . ../.env; set +a
ec...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ cd /c/Users/Admin/projects/clausewatch/studio &amp;amp;&amp;amp; set -a; . ../.env; set +a
echo '{"_id":"anontest-nodots","_type":"jurisdiction","name":"Anon probe","code":"ZZ"}' &amp;gt; /tmp/probe.ndjson
npx sanity dataset import /tmp/probe.ndjson production --replace 2&amp;gt;&amp;amp;1 | tail -2
sleep 2
echo "=== anonymous query ==="
curl -s "https://4yzoidsq.api.sanity.io/v2026-09-19/data/query/production?query=$(node -e 'process.stdout.write(encodeURIComponent(`*[_type=="jurisdiction"]{_id,name}`))')"; echo&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Done! Imported 1 documents to dataset "production"

=== anonymous query ===
{"query":"*[_type==\"jurisdiction\"]{_id,name}","result":[{"_id":"anontest-nodots","name":"Anon probe"}],"syncTags":["s1:pQbSuQ"],"ms":1}
Shell cwd was reset to C:\Users\Admin\Documents&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        1 messages (range 114-114) of 485 total
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project ID:&lt;/strong&gt; &lt;code&gt;4yzoidsq&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset:&lt;/strong&gt; &lt;code&gt;production&lt;/code&gt; (public)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Studio:&lt;/strong&gt; &lt;a href="https://clausewatch.sanity.studio/" rel="noopener noreferrer"&gt;https://clausewatch.sanity.studio/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Public dataset, no token — every conflict, both sides, with its decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://4yzoidsq.api.sanity.io/v2026-09-19/data/query/production?query=*[_type=="conflict"]{summary,nature,resolution,decidedBy,"sides":sides[]-&amp;gt;{"cite":source-&amp;gt;shortName+" "+citation}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Agent Session
&lt;/h2&gt;

&lt;p&gt;The whole build is on record — 485 messages in one Claude Code session. A note before you&lt;br&gt;
open it: I work in Russian, so the conversation is in Russian. Everything that matters here&lt;br&gt;
is not. The tool calls, the GROQ queries, the MCP traces, the terminal output and the agent's&lt;br&gt;
own answers are all English, and that is what each slice below is made of. I have said what&lt;br&gt;
to look for in front of each one — and each card opens collapsed, so click the ▸ to see the&lt;br&gt;
output rather than just the command line.&lt;/p&gt;
&lt;h3&gt;
  
  
  The agent refuses to give one number
&lt;/h3&gt;

&lt;p&gt;The question is how long a provider must keep logs for a high-risk biometric system. Watch&lt;br&gt;
the trace: &lt;code&gt;initial_context&lt;/code&gt; on the dataset, then on the knowledge base, then a GROQ query it&lt;br&gt;
writes itself, then two knowledge-base entries, then jurisdictions, then profiles. Seven&lt;br&gt;
calls across both Context endpoints before it says anything.&lt;/p&gt;

&lt;p&gt;Then read the answer. It gives a floor with a citation, a ceiling with a citation, states&lt;br&gt;
that the conflict between them is unresolved and that nobody is recorded as having decided&lt;br&gt;
it, and points out that the obligation does not apply until December 2027. It never produces&lt;br&gt;
the single number the question was fishing for.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Claude Code"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building ClauseWatch: an agent over contradictory AI regulation&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;cd /c/Users/Admin/projects/clausewatch/agent &amp;amp;&amp;amp; npm run ask -- "How long must...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ cd /c/Users/Admin/projects/clausewatch/agent &amp;amp;&amp;amp; npm run ask -- "How long must the provider of a high-risk biometric access-control system keep its automatically generated logs, and from when?" 2&amp;gt;&amp;amp;1 | tail -50&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;&amp;gt; clausewatch-agent@0.1.0 ask
&amp;gt; tsx src/cli.ts How long must the provider of a high-risk biometric access-control system keep its automatically generated logs, and from when?

  → data · initial_context
  → docs · initial_context
  → data · groq_query
  → docs · knowledge_base_read
  → data · groq_query

## Log retention for a high-risk biometric access-control system's provider

**The floor: at least six months (EU AI Act, Art. 19)**

Under EU AI Act **Article 19**, providers of high-risk AI systems must keep the automatically generated logs (as produced under Article 12(1)) "for a period appropriate to the intended purpose of the high-risk AI system, of **at least six months**, unless provided otherwise in the applicable Union or national law, in particular Union law on the protection of personal data" — and only to the extent the logs are under the provider's control.

- Source: EU AI Act, Art. 19 — official text: https://eur-lex.europa.eu/eli/reg/2024/1689/oj
- **Effective date:** this obligation (like Art. 12 logging design duties) enters into force **2 December 2027** for Annex III high-risk systems, or **2 August 2028** for Annex I high-risk systems. As of today (2026‑09‑19) it is **not yet a current obligation** — it is a future one.

For a biometric identification system specifically (Annex III, point 1(a)), Article 12(3) additionally requires the logs to capture, at minimum: usage period (start/end date-time of each use), the reference database checked, the matched input data, and the identity of the human verifier(s) involved — but Art. 12(3) itself does not add a separate retention period; the six-month floor still comes from Art. 19.

**The ceiling that conflicts with it: GDPR Art. 5(1)(e)**

The dataset records an unresolved conflict on this exact point:

&amp;gt; "AI Act sets a six-month floor for log retention; GDPR sets a necessity ceiling on the same logs"

- **AI Act side:** Art. 19 — "kept for a period appropriate to the intended purpose … of at least six months" (as above).
- **GDPR side:** Art. 5(1)(e), Storage limitation — "Personal data shall be kept in a form which permits identification of data subjects for **no longer than is necessary** for the purposes for which the personal data are processed." (GDPR official text: https://eur-lex.europa.eu/eli/reg/2016/679/oj)

The dataset notes that because biometric logs under Art. 12(3) necessarily contain personal data (matched biometric input, verifier identity), this ceiling is essentially unavoidable for this system type.

**Resolution status: open.** The dataset marks this conflict's `resolution` as **"open"** — it has no `rationale` and no `decidedBy` recorded, meaning it has not been settled. I can't tell you who is designated to decide it, because the dataset only records a decider for resolved conflicts, and this one isn't resolved.

**Bottom line:** you cannot state a single fixed retention number. The provider must retain the logs for **no less than six months** (Art. 19 floor) **and no longer than necessary for the purpose** for which the biometric data is processed (GDPR Art. 5(1)(e) ceiling), with no dataset-recorded resolution telling you how to reconcile the two if "necessary" turns out to be shorter than six months. Both duties only become live obligations from **2 December 2027 / 2 August 2028** depending on whether the system falls under Annex III or Annex I — not before.

One more caveat per the rules I follow: this assumes your system's profile (role = provider, jurisdiction = EU, risk class = high, EU-market use) is confirmed. I haven't been given a system profile in this conversation — if any of those facts differ, the applicable obligations could change.

(5 tool calls)
Shell cwd was reset to C:\Users\Admin\Documents&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        1 messages (range 291-291) of 485 total
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  The Annex I check, start to finish
&lt;/h3&gt;

&lt;p&gt;This one starts with a Sanity Context issue: an entry says Annex I lists 21 harmonisation&lt;br&gt;
instruments, the source shows 20. You will see me go to the source rather than take it on&lt;br&gt;
trust — the &lt;code&gt;curl&lt;/code&gt; that pulls the annex straight from the source, and the amendment markers&lt;br&gt;
in its output: item 1 struck, item 21 added.&lt;/p&gt;

&lt;p&gt;The end of the slice is the part I would point a judge at. Context also proposed a Section&lt;br&gt;
A / Section B split, and I could not verify it, because EUR-Lex blocks automated retrieval.&lt;br&gt;
So the decision recorded in the dataset states the verified part and explicitly excludes the&lt;br&gt;
rest. Finding disagreement and settling it are different jobs.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Claude Code"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building ClauseWatch: an agent over contradictory AI regulation&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;curl -s https://artificialintelligenceact.eu/annex/1/ | node -e '
let d="";pr...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ curl -s https://artificialintelligenceact.eu/annex/1/ | node -e '
let d="";process.stdin.on("data",c=&amp;gt;d+=c).on("end",()=&amp;gt;{
  const txt=d.replace(/&amp;lt;script[\s\S]*?&amp;lt;\/script&amp;gt;/g,"").replace(/&amp;lt;style[\s\S]*?&amp;lt;\/style&amp;gt;/g,"")
             .replace(/&amp;lt;[^&amp;gt;]+&amp;gt;/g," ").replace(/&amp;amp;#8217;|&amp;amp;rsquo;/g,"’").replace(/&amp;amp;nbsp;/g," ")
             .replace(/\s+/g," ");
  const a=txt.indexOf("Section A");
  const b=txt.indexOf("Section B");
  console.log("--- around Section A ---");
  console.log(txt.slice(a, a+300));
  console.log("\n--- tail of Section A / start of Section B ---");
  console.log(txt.slice(b-500, b+300));
  console.log("\n--- last 900 chars of annex body ---");
  const end=txt.indexOf("Feedback")&amp;gt;0?txt.indexOf("Feedback"):txt.length;
  console.log(txt.slice(Math.max(0,end-1200), end));
});'&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;--- around Section A ---
&lt;/span&gt;&lt;span class="p"&gt;Section A. List of Union harmonisation legislation based on the New Legislative Framework Related recitals: 50 51 1. Not present after the amendment. Directive 2006/42/EC of the European Parliament and of the Council of 17 May 2006 on machinery, and amending Directive 95/16/EC ( OJ L 157, 9.6.2006, 
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;--- tail of Section A / start of Section B ---
&lt;/span&gt;&lt;span class="p"&gt;I Arts. 57–63 Measures in Support of Innovation Article 57: AI Regulatory Sandboxes Article 58: Detailed Arrangements for, and Functioning of, AI Regulatory Sandboxes Article 59: Further Processing of Personal Data for Developing Certain AI Systems in the Public Interest in the AI Regulatory Sandbox Article 60: Testing of High-Risk AI Systems in Real World Conditions Outside AI Regulatory Sandboxes Article 60a: Testing of high-risk AI systems covered by Union harmonisation legislation listed in Section B of Annex I in real-world conditions outside AI regulatory sandboxes Article 61: Informed Consent to Participate in Testing in Real World Conditions Outside AI Regulatory Sandboxes Article 62: Measures for Providers and Deployers, in Particular SMEs, Including Start-Ups Article 63: Derogati
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;--- last 900 chars of annex body ---
&lt;/span&gt;, from the input it receives, how to generate outputs such as predictions, content, recommendations, or decisions that can influence physical or virtual environments Article 3(1) or a general-purpose AI model on the Union market Article 3(9) of aircrafts referred to in Article 2 (1), points (a) and (b) thereof, where it concerns unmanned aircraft and their engines, propellers, parts and equipment to control them remotely, are concerned. 21. Regulation (EU) 2023/1230 of the European Parliament and of the Council of 14 June 2023 on machinery and repealing Directive 2006/42/EC of the European Parliament and of the Council and Council Directive 73/361/EEC ( OJ L 165, 29.6.2023, p. 1 , ELI: http://data.europa.eu/eli/reg/2023/1230/oj ). Not present before the amendment. new View in the full AI Act Explorer Annex II Cookies on this website We use cookies to understand how the explorer is used. Functional cookies are always on; everything else is your choice, shared with artificialintelligenceact.eu so you are only asked once. Read the Cookie Policy and the Privacy Statement . Functional (always on) Statistics Marketing Preferences Accept all Reject all Save preferences Manage preferences&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        1 messages (range 191-191) of 485 total
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;The knowledge base is built from authoritative reproductions, not the Official Journal —&lt;br&gt;
EUR-Lex blocks automated retrieval. Every &lt;code&gt;source&lt;/code&gt; carries an &lt;code&gt;officialUrl&lt;/code&gt; and the agent is&lt;br&gt;
instructed to give that one for anything the reader will act on.&lt;/p&gt;

&lt;p&gt;The dataset is small and deliberately so: 2 instruments, 9 provisions, 2 requirements,&lt;br&gt;
3 conflicts, 2 system profiles. A working core that demonstrates the model, not a compliance&lt;br&gt;
product. Nothing here is legal advice.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>I tried to forge my own AI agent's audit log</title>
      <dc:creator>oleg-vdv</dc:creator>
      <pubDate>Mon, 14 Sep 2026 05:21:22 +0000</pubDate>
      <link>https://dev.to/olegvdv/i-tried-to-forge-my-own-ai-agents-audit-log-3chh</link>
      <guid>https://dev.to/olegvdv/i-tried-to-forge-my-own-ai-agents-audit-log-3chh</guid>
      <description>&lt;p&gt;Half the organisations running AI agents have already had one step outside its&lt;br&gt;
permissions. Nearly half of the agents in production have no monitoring at all.&lt;br&gt;
Only about a fifth treat an agent as something with an identity of its own.&lt;br&gt;
Those are numbers from the Cloud Security Alliance and the 2026 State of AI&lt;br&gt;
Agent Security report, and they describe the same gap from three directions:&lt;br&gt;
agents act, and nobody can say afterwards what exactly they did.&lt;/p&gt;

&lt;p&gt;I spent a few weeks building the missing piece, and then spent an afternoon&lt;br&gt;
attacking it. This is what survived.&lt;/p&gt;
&lt;h3&gt;
  
  
  Four things an accountable agent needs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A passport.&lt;/strong&gt; Not a config file — a card for one &lt;em&gt;version&lt;/em&gt; of an agent that&lt;br&gt;
says who built it, who runs it, what it does, and what it will never do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agent_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kepil.leads.v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Handle inbound requests and hand qualified ones to a person"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"does_not"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"never promises prices or deadlines on the company's behalf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"never sends invoices or contracts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"never exports the customer base outside the perimeter"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"risk_class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"autonomy_class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"risk_review"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"last"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"next_due"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2027-09-14"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Trimmed for readability — a real card also carries the operator, the model list,&lt;br&gt;
the rationale for each class, an incident log and the passport's own hash. One&lt;br&gt;
caveat before you install: the runtime, the panel and these field &lt;em&gt;values&lt;/em&gt; are&lt;br&gt;
currently in Russian. The format and the code are not language-specific, but you&lt;br&gt;
will be reading «средний» where this article says "medium".)&lt;/p&gt;

&lt;p&gt;A new version is a new card. The old one is kept forever, because the question&lt;br&gt;
"what was this agent allowed to do in March" has to have an answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A mandate.&lt;/strong&gt; A machine-readable power of attorney for one job — not a&lt;br&gt;
permanent grant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mandate_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mnd-0001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowed_actions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"read:inbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read:crm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"generate:reply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"send:message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                      &lt;/span&gt;&lt;span class="s2"&gt;"write:crm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"generate:summary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"send:handoff"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowed_systems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"crm.local"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"whatsapp.local"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"forbidden_actions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sign:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pay:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"export:database"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"send:bulk"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"human_confirmation_required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"send:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"write:crm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"publish:*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"llm_cost_kzt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid_until"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-21T10:09:45"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything not explicitly allowed is refused. That sounds obvious until you look&lt;br&gt;
at how agents are actually wired: one shared API key, full network access, and&lt;br&gt;
a prompt asking nicely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A gate.&lt;/strong&gt; One place an agent touches the outside world, and every action is&lt;br&gt;
checked &lt;em&gt;before&lt;/em&gt; a model is even called. The checks run in a fixed order:&lt;br&gt;
passport active, mandate valid, action allowed, system allowed, limits not&lt;br&gt;
spent, irreversible or not. Any error inside the check means refusal — never a&lt;br&gt;
pass. A false refusal costs a retry; a false pass costs a breach notification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A journal.&lt;/strong&gt; Append-only JSONL where each record carries the hash of the one&lt;br&gt;
before it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"seq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10241&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-09-12T11:42:07+05:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"agent_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"kepil.leads.v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"send:message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"whatsapp.local"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"await_human"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cost_kzt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"prev_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"sha256:c61d8b…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"sha256:d5ade5…"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Values of personal data never enter it. Types, counts and hashes do.&lt;/p&gt;

&lt;h3&gt;
  
  
  The part where I attacked it
&lt;/h3&gt;

&lt;p&gt;A log you cannot verify is a diary. So I wrote a record, then went back and&lt;br&gt;
edited it the way someone covering their tracks would — flipping a refusal into&lt;br&gt;
an approval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# the agent tried to sign a document; the gate refused&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npx proofbyte-agent-trace verify journal.jsonl
Integrity confirmed. Records: 4
Chain &lt;span class="nb"&gt;head&lt;/span&gt;: sha256:d58c204ff2a37b093307b95b3d9fcc4edc510cd6062622db10b3461a4341dd3f

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/"decision": "deny"/"decision": "allow"/'&lt;/span&gt; journal.jsonl
&lt;span class="nv"&gt;$ &lt;/span&gt;npx proofbyte-agent-trace verify journal.jsonl
Integrity BROKEN. Record 3: record content changed after it was written
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here.&lt;/p&gt;

&lt;p&gt;The first is that it names the record, not just the file. Hash chaining gives&lt;br&gt;
you that for free, and it is the difference between "something is wrong" and&lt;br&gt;
"here is what was changed".&lt;/p&gt;

&lt;p&gt;The second is subtler. &lt;strong&gt;The verifier is a different program in a different&lt;br&gt;
language, written by nobody in particular.&lt;/strong&gt; The writer is Python; the verifier&lt;br&gt;
is TypeScript on npm. Proof that only its own author can check is not proof —&lt;br&gt;
so the test suite of the verifier contains a journal produced by the Python&lt;br&gt;
writer, and that test pins a bug I would otherwise have shipped: Python&lt;br&gt;
serialises &lt;code&gt;0.0&lt;/code&gt; as &lt;code&gt;"0.0"&lt;/code&gt; and JavaScript as &lt;code&gt;"0"&lt;/code&gt;, so the same record hashed&lt;br&gt;
differently in the two implementations. An honest log would have been declared&lt;br&gt;
forged. Number canonicalisation is now part of the format.&lt;/p&gt;
&lt;h3&gt;
  
  
  The feature I deliberately did not build
&lt;/h3&gt;

&lt;p&gt;Kepil ships an MCP server, a JSON API and an n8n node. All three can create&lt;br&gt;
orders, run steps, ask whether an action is permitted, and read the journal.&lt;/p&gt;

&lt;p&gt;None of them can confirm.&lt;/p&gt;

&lt;p&gt;If a model could call &lt;code&gt;confirm&lt;/code&gt;, the human would drop out of the chain: the&lt;br&gt;
agent would be approving its own irreversible actions, and every guarantee&lt;br&gt;
above would become decoration. So the confirmation card goes to a person — the&lt;br&gt;
panel, or Telegram with two buttons — and there is a test whose only job is to&lt;br&gt;
fail if &lt;code&gt;confirm&lt;/code&gt; ever appears in the tool list.&lt;/p&gt;

&lt;p&gt;I find this the most useful thing to say about the whole project. Everyone&lt;br&gt;
advertises what their agent framework can do. The interesting part is what it&lt;br&gt;
refuses to do even when you ask it nicely.&lt;/p&gt;
&lt;h3&gt;
  
  
  Undo
&lt;/h3&gt;

&lt;p&gt;Recording is table stakes. Agent-governance products — Workday's Agent&lt;br&gt;
Passport, Okta, Credo AI — all write logs. None of them put anything back.&lt;/p&gt;

&lt;p&gt;Because the journal is a graph of actions and every profession declares its&lt;br&gt;
compensating action, you can walk it backwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"rollback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"send:message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"send a correcting message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"write:crm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"restore the previous state of the deal from the journal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publish:complaint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The empty string is the important entry. It means &lt;em&gt;this cannot be undone&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Pick a window in the panel — the last hour, say — and the pass runs from the&lt;br&gt;
most recent action backwards, applying each compensating action in turn, and&lt;br&gt;
stops at the first step whose compensation is that empty string. It then reports&lt;br&gt;
both halves: what it undid, and what it could not. The panel shows the same&lt;br&gt;
thing &lt;strong&gt;before&lt;/strong&gt; you press the button, naming the step where the pass will stop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Will undo 2 actions. The pass will stop at "Search for tailored requirements" —
that action is irreversible, and nothing before it will be undone.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An undo promise that quietly fails is worse than no undo at all, so there is a&lt;br&gt;
test whose only job is to fail if the preview and the engine ever disagree.&lt;/p&gt;

&lt;p&gt;One more thing follows from this. A rollback is written to the journal as an&lt;br&gt;
&lt;em&gt;operator's&lt;/em&gt; decision, so neither the MCP server nor the JSON API can perform&lt;br&gt;
one — same reasoning as &lt;code&gt;confirm&lt;/code&gt;. An agent undoing its own actions would be&lt;br&gt;
signing in somebody else's name.&lt;/p&gt;
&lt;h3&gt;
  
  
  Boring on purpose
&lt;/h3&gt;

&lt;p&gt;The core has zero dependencies. Not "few" — zero: it is the Python 3.11+&lt;br&gt;
standard library, and CI fails the build if a third-party import appears. Two&lt;br&gt;
reasons. It installs inside an air-gapped perimeter where &lt;code&gt;pip install&lt;/code&gt; from&lt;br&gt;
the internet is not an option. And a tool that sees every action an agent takes&lt;br&gt;
should not drag a hundred transitive packages behind it.&lt;/p&gt;

&lt;p&gt;State is JSON files. No database. You can open them, read them, and attach them&lt;br&gt;
to a dispute — which is the whole point of the exercise.&lt;/p&gt;
&lt;h3&gt;
  
  
  Status
&lt;/h3&gt;

&lt;p&gt;Alpha, 115 tests, AGPL-3.0. It does not run agents for you: it is the layer&lt;br&gt;
that says what they may do and records what they did.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;kepil
python &lt;span class="nt"&gt;-m&lt;/span&gt; kepil.admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Core: &lt;a href="https://github.com/oleg-vdv/kepil" rel="noopener noreferrer"&gt;https://github.com/oleg-vdv/kepil&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Verifier (MIT): &lt;a href="https://github.com/oleg-vdv/agent-trace" rel="noopener noreferrer"&gt;https://github.com/oleg-vdv/agent-trace&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;n8n node: &lt;a href="https://github.com/oleg-vdv/n8n-nodes-kepil" rel="noopener noreferrer"&gt;https://github.com/oleg-vdv/n8n-nodes-kepil&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are running agents in production and can answer "what did agent X do on&lt;br&gt;
9 September, and who allowed it" — I would genuinely like to know how. If you&lt;br&gt;
cannot, that is the gap this was built for.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>"Nobody is watching the n8n instance on your developer's laptop"</title>
      <dc:creator>oleg-vdv</dc:creator>
      <pubDate>Sat, 05 Sep 2026 12:12:02 +0000</pubDate>
      <link>https://dev.to/olegvdv/nobody-is-watching-the-n8n-instance-on-your-developers-laptop-3klc</link>
      <guid>https://dev.to/olegvdv/nobody-is-watching-the-n8n-instance-on-your-developers-laptop-3klc</guid>
      <description>&lt;p&gt;Here's a pattern that has quietly become normal in a lot of companies.&lt;/p&gt;

&lt;p&gt;An operations person gets tired of copying data between the CRM and the accounting system by hand. They spin up n8n — self-hosted, in Docker, on their work laptop or on a spare VM under someone's desk. They paste in live credentials: the database, the CRM API, the payment provider, the 1C instance. It works. It saves them four hours a week. They tell two colleagues, who do the same.&lt;/p&gt;

&lt;p&gt;Nobody in security knows this exists. There is now an unmanaged automation platform holding production credentials, running on an endpoint with no monitoring, whose failure mode is "the intern's laptop was stolen."&lt;/p&gt;

&lt;p&gt;The uncomfortable part is that your existing tools genuinely can't see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the existing tools miss it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SaaS discovery tools&lt;/strong&gt; (Defender for Cloud Apps, Nudge, Auvik and friends) discover OAuth grants and cloud apps. A self-hosted n8n on &lt;code&gt;localhost:5678&lt;/code&gt; grants nothing through OAuth — it holds credentials directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EDR&lt;/strong&gt; sees a Node.js process. It's not wrong. It's just that "node is running" is not an actionable finding, and the EDR has no concept of "this process holds a credential that reaches the payment API."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network monitoring&lt;/strong&gt; sees traffic from a workstation to your own database. That's traffic it's supposed to see.&lt;/p&gt;

&lt;p&gt;Each tool is working correctly. The blind spot is between them: nobody maps &lt;em&gt;unmanaged automation instance → the credentials it holds → the production systems those credentials reach.&lt;/em&gt; That map is the finding. Everything else is noise.&lt;/p&gt;

&lt;p&gt;So I built AutoGov to produce exactly that map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detection has to be multi-signal, or it's useless
&lt;/h2&gt;

&lt;p&gt;The naive version — "port 5678 is open, alert!" — generates so many false positives that people turn it off in a week. Detection here combines signals and never fires on one alone:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;What it inspects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;n8nio/n8n&lt;/code&gt; images, port 5678, &lt;code&gt;N8N_*&lt;/code&gt; env vars, linked postgres/redis (queue mode) — via a &lt;strong&gt;read-only&lt;/strong&gt; socket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processes&lt;/td&gt;
&lt;td&gt;Node.js n8n signatures, Python runners and schedulers in &lt;code&gt;/proc&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;HTTP fingerprint: &lt;code&gt;/rest&lt;/code&gt; + &lt;code&gt;/api/v1/docs&lt;/code&gt; + &lt;code&gt;/healthz&lt;/code&gt; together uniquely identify n8n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filesystem&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~/.n8n&lt;/code&gt;, a &lt;code&gt;docker-compose.yml&lt;/code&gt; mentioning n8n, &lt;code&gt;.env&lt;/code&gt; files referencing &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schedulers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cron&lt;/code&gt; entries and &lt;code&gt;systemd&lt;/code&gt; timers launching automation runners&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sanctioned instances go on a whitelist, so CI/CD containers don't generate a daily alert. That was a first-release requirement rather than a "later" item — a security tool that cries wolf gets muted, and a muted tool is worse than no tool because it creates the illusion of coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy problem, and the design that solves it
&lt;/h2&gt;

&lt;p&gt;A tool that hunts for credentials on employee machines is itself a serious risk. If it collects secret &lt;em&gt;values&lt;/em&gt;, you have built a company-wide credential exfiltration system and pointed it at yourself. One compromised control plane and the attacker gets everything at once.&lt;/p&gt;

&lt;p&gt;So the agent records the &lt;strong&gt;fact&lt;/strong&gt; that a secret exists — its name, its type, the system it targets — plus a SHA-256 fingerprint for correlation. It never reads or transmits the value. This isn't a policy or a code review rule: &lt;strong&gt;no field in the data model can hold a secret or PII.&lt;/strong&gt; You can't misconfigure your way into collecting them, because there's nowhere to put them.&lt;/p&gt;

&lt;p&gt;The rest of the security posture follows the same logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent is &lt;strong&gt;outbound-only&lt;/strong&gt; and opens no listening ports — it can't become a new attack surface on the endpoints it's installed on.&lt;/li&gt;
&lt;li&gt;The Docker socket is mounted &lt;strong&gt;read-only&lt;/strong&gt;. Discovery needs to read container metadata; it never needs to start, stop or exec into anything.&lt;/li&gt;
&lt;li&gt;Release artifacts are &lt;strong&gt;ed25519-signed&lt;/strong&gt; and verified by the agent at startup, fail-closed. An agent that runs everywhere with root-adjacent visibility is a supply-chain target, and it should refuse to run unsigned code.&lt;/li&gt;
&lt;li&gt;mTLS between agent and control plane, RBAC and an audit log on top.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you actually see
&lt;/h2&gt;

&lt;p&gt;For each discovered instance: which credentials it holds, which production systems those credentials reach, a numeric risk score, a Critical/High/Medium/Low category, and a plain-language explanation of &lt;em&gt;why&lt;/em&gt; it scored that way — generated locally, with no LLM in the loop, because a CISO report needs to be reproducible and defensible rather than eloquent.&lt;/p&gt;

&lt;p&gt;That access map is the artifact that turns "there is unmanaged software here" into "this laptop can move money."&lt;/p&gt;

&lt;h2&gt;
  
  
  Two-minute demo
&lt;/h2&gt;

&lt;p&gt;The compose file brings up the control plane, an intentionally shadow n8n instance, and an agent — so you can watch discovery happen on a system that's deliberately misconfigured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/oleg-vdv/AutoGov.git
&lt;span class="nb"&gt;cd &lt;/span&gt;AutoGov/deploy
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:8443&lt;/code&gt;, log in with the token &lt;code&gt;dev-admin&lt;/code&gt;, and within about 30 seconds the agent finds the instance, builds the access map, and shows risk-scored findings. (Demo uses HTTP and dev tokens — local evaluation only.)&lt;/p&gt;

&lt;p&gt;Building from source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make build   &lt;span class="c"&gt;# → bin/agent, bin/controlplane, bin/autogov-sign&lt;/span&gt;
make &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pure Go standard library, no external dependencies — which keeps the agent auditable and makes air-gapped installs boring, in the good way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current state
&lt;/h2&gt;

&lt;p&gt;Stage 1 (n8n discovery) is MVP and pilot-ready. Generic automations — Make agents, ad-hoc scripts, a Windows agent — are next, with module contracts and stubs already in place for the later stages: honeypot nodes, self-healing and auto-documentation. The module runtime lets those plug in without touching the core, which the tests enforce.&lt;/p&gt;

&lt;p&gt;Apache 2.0: &lt;a href="https://github.com/oleg-vdv/AutoGov" rel="noopener noreferrer"&gt;https://github.com/oleg-vdv/AutoGov&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've dealt with shadow automation in your own organization, I'd like to know how you found it — and whether you tried to shut it down or bring it under management. My assumption is that shutting it down mostly fails, because the four hours a week were real, but I'd be glad to hear counterexamples.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>"Your company banned ChatGPT. There's a third option."</title>
      <dc:creator>oleg-vdv</dc:creator>
      <pubDate>Tue, 01 Sep 2026 16:29:23 +0000</pubDate>
      <link>https://dev.to/olegvdv/your-company-banned-chatgpt-theres-a-third-option-4gpl</link>
      <guid>https://dev.to/olegvdv/your-company-banned-chatgpt-theres-a-third-option-4gpl</guid>
      <description>

&lt;p&gt;Most companies handle the "employees are pasting customer data into ChatGPT" problem in one of two ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option one: block it.&lt;/strong&gt; Firewall rules, an acceptable-use policy, a stern email. What actually happens is that people use their phones, personal laptops, and a browser profile you don't manage. You haven't stopped the leak; you've stopped seeing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option two: allow it and hope.&lt;/strong&gt; Training, a wiki page about "don't paste PII", and an incident waiting to be discovered by someone else.&lt;/p&gt;

&lt;p&gt;There's a third option that I don't think gets built often enough: let the request through, but strip the sensitive parts on the way out and put them back on the way in. The model never sees the customer's name. The employee never notices anything happened.&lt;/p&gt;

&lt;p&gt;That's what I built AI-Gate for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea in one example
&lt;/h2&gt;

&lt;p&gt;An internal app sends this prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer Yerzhan Nursultanuly, ID 900715300005, disputes a charge of 145,000 KZT...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What actually leaves your network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer [PERSON_1], ID [NATIONAL_ID_1], disputes a charge of [AMOUNT_1]...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model answers about &lt;code&gt;[PERSON_1]&lt;/code&gt;. The gateway substitutes the real values back before the response reaches the application. From the app's perspective it just talked to OpenAI normally. From OpenAI's perspective, it never received a single piece of personal data.&lt;/p&gt;

&lt;p&gt;The mapping between &lt;code&gt;[PERSON_1]&lt;/code&gt; and the real name lives &lt;strong&gt;in memory only&lt;/strong&gt;, encrypted, with a TTL and guaranteed destruction once the response is detokenized. It is never written to disk and never appears in the audit log — the log records types and counts, not values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating it is one line
&lt;/h2&gt;

&lt;p&gt;For any application already using an OpenAI-compatible client, you change the base URL and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://aigate.internal:8080/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;channel key&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Customer Yerzhan Nursultanuly, ID 900715300005...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A detail that matters more than it looks: the &lt;strong&gt;real provider API key lives only on the gateway&lt;/strong&gt;. Applications get a channel key instead. Revoking one team's access stops being a redeployment and starts being a config change — and a leaked application key no longer means a leaked OpenAI key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three channels, because the leak isn't only in your code
&lt;/h2&gt;

&lt;p&gt;An egress proxy covers your own applications, RAG pipelines and n8n workflows. It does not cover the two places people actually leak data:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The browser.&lt;/strong&gt; A Chrome extension intercepts text &lt;em&gt;before&lt;/em&gt; it's sent to chat.openai.com, claude.ai or gemini.google.com, masks it through the gateway, and substitutes values back into the assistant's reply as it renders. Deployable via GPO/MDM, so it's not opt-in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The clipboard.&lt;/strong&gt; A workstation agent catches sensitive data on its way into local LLM apps — Cursor, Claude Desktop — and detokenizes copied responses. This is the gap neither the browser nor the network perimeter can see, and it's the one that grows every month as AI moves into desktop tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions worth arguing about
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fail-closed, always.&lt;/strong&gt; Any error in detection or parsing blocks the request instead of passing it through. This will occasionally annoy someone. The alternative is that a parser bug becomes a data breach, and the asymmetry there isn't close: a false block costs a retry, a false pass costs a regulatory fine and a disclosure letter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero dependencies.&lt;/strong&gt; The core is pure Python 3.11+ standard library. No PyPI at install time, which means it drops into air-gapped environments — exactly the kind of environment where this problem is most acute and where "just pip install" is not an available move. It also means the supply-chain attack surface of a tool that sees all your prompts is approximately zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tamper-evident audit.&lt;/strong&gt; The log is append-only JSONL with a SHA-256 hash chain. You can't quietly delete the record of a blocked request; verification detects it. For a compliance tool this is the difference between "we have logs" and "we have evidence".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sensitive prompts can be routed to a local model.&lt;/strong&gt; The provider registry supports OpenAI-compatible endpoints including vLLM, Ollama and LM Studio, plus the Anthropic Messages API. A policy rule can say: anything containing sensitive data goes to the local model only, never to a vendor. Masking and routing solve different halves of the same problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic detection on top of pattern detection.&lt;/strong&gt; Regexes catch IDs, account numbers and names. They do not catch "our Q3 acquisition target is a logistics company in Almaty with a 40% margin" — a leak with no PII in it at all. An LLM-judge runs over the &lt;em&gt;already anonymized&lt;/em&gt; text and flags meaning-level leaks, in flag or block mode, fail-closed if the judge is unreachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env      &lt;span class="c"&gt;# set AIGATE_PROVIDER_API_KEY&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; gateway.main    &lt;span class="c"&gt;# gateway on :8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or on-prem in a container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The control plane console is at &lt;code&gt;http://localhost:8080/admin&lt;/code&gt; — policies, RBAC, dashboards and compliance reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is honest about its limits
&lt;/h2&gt;

&lt;p&gt;Detection is imperfect and always will be. Names in particular are the hard case in every language, and a masking gateway that claims 100% recall is lying. Fail-closed helps, the local-model routing rule helps more, and semantic detection catches a class regexes structurally cannot — but the right mental model is defense in depth, not a solved problem.&lt;/p&gt;

&lt;p&gt;It's also worth saying plainly: this reduces exposure, it doesn't create legal compliance by itself. What it gives you is the technical control and the evidence trail that a compliance program needs underneath it.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/oleg-vdv/AI-Gateway" rel="noopener noreferrer"&gt;https://github.com/oleg-vdv/AI-Gateway&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd be interested to hear how others are handling this. Has your organization landed on block, allow, or something in between — and if you've deployed a masking layer, what broke first?&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>privacy</category>
      <category>python</category>
    </item>
    <item>
      <title>From Infrastructure to Open Source: Lessons Learned Building 4 Security &amp; Automation Tools</title>
      <dc:creator>oleg-vdv</dc:creator>
      <pubDate>Fri, 24 Jul 2026 00:47:58 +0000</pubDate>
      <link>https://dev.to/olegvdv/from-infrastructure-to-open-source-lessons-learned-building-4-security-automation-tools-49he</link>
      <guid>https://dev.to/olegvdv/from-infrastructure-to-open-source-lessons-learned-building-4-security-automation-tools-49he</guid>
      <description>&lt;p&gt;Coming from a strong sysadmin and infrastructure background, I spent years managing servers, networks, and keeping systems alive. Over time, I realized a fundamental truth: &lt;strong&gt;the most dangerous system risks are often the ones you don't even have visible inventory for.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That mindset naturally led me into the world of open source. I started building tools to solve real-world problems around API governance, edge safety, data integrity, and automation. &lt;/p&gt;

&lt;p&gt;Here is what I’ve been building in public, what each project taught me, and why these areas matter today:&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Governing LLM &amp;amp; API Traffic: AI-Gateway
&lt;/h2&gt;

&lt;p&gt;As AI applications move to production, controlling model access, enforcing limits, and monitoring traffic becomes critical. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Project:&lt;/strong&gt; &lt;a href="https://github.com/oleg-vdv/AI-Gateway" rel="noopener noreferrer"&gt;AI-Gateway&lt;/a&gt; — A lightweight proxy layer designed to secure, route, and manage API requests and policies for AI services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Lesson:&lt;/strong&gt; Security in the AI era isn't just about firewall ports; it's about context-aware policy management and dynamic traffic control.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Safety at the Edge: AffectGuard-HRI
&lt;/h2&gt;

&lt;p&gt;Moving machine learning onto edge devices and microcontrollers opens up huge potential for robotics, but it introduces strict real-time safety constraints.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Project:&lt;/strong&gt; &lt;a href="https://github.com/oleg-vdv/affectguard-hri" rel="noopener noreferrer"&gt;AffectGuard-HRI&lt;/a&gt; — An open-source framework tailored for human-robot interaction, focusing on real-time safety, intent tracking, and affective monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Lesson:&lt;/strong&gt; Edge AI demands extreme efficiency. You can't rely on cloud latency when dealing with physical robotic hardware—safety loops must run reliably at the hardware level.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Verifiable Data &amp;amp; Audit Trails: ProofByte
&lt;/h2&gt;

&lt;p&gt;In modern SecOps, logging isn't enough—you need verifiable proof of data integrity for compliance and auditing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Project:&lt;/strong&gt; &lt;a href="https://github.com/oleg-vdv/proofbyte" rel="noopener noreferrer"&gt;ProofByte&lt;/a&gt; — A lightweight tool aimed at data validation, cryptographic verification, and maintaining tamper-evident audit trails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Lesson:&lt;/strong&gt; Building trust in distributed workflows requires cryptographic validation at every step of the pipeline.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Modern Workflow Governance: AutoGov
&lt;/h2&gt;

&lt;p&gt;Processes in enterprise environments often break down due to manual bottlenecks and fragmented oversight.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Project:&lt;/strong&gt; &lt;a href="https://github.com/oleg-vdv/AutoGov" rel="noopener noreferrer"&gt;AutoGov&lt;/a&gt; — An automation and workflow governance engine built to streamline standard operating procedures and administrative checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Lesson:&lt;/strong&gt; Good automation doesn't replace governance—it bakes governance into the infrastructure so compliance happens by default.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  What’s Next?
&lt;/h3&gt;

&lt;p&gt;Transitioning from maintaining infrastructure to building developer tools in public has been an incredible learning experience. &lt;/p&gt;

&lt;p&gt;I’d love to hear from the community: &lt;strong&gt;What is your biggest pain point right now when securing edge devices or managing AI API endpoints?&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Feel free to check out the repos, leave stars if you find them useful, or open issues/PRs! Let’s connect! 🚀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>The post-quantum deadlines are closer than your roadmap thinks — so I built a CBOM scanner</title>
      <dc:creator>oleg-vdv</dc:creator>
      <pubDate>Fri, 17 Jul 2026 00:55:53 +0000</pubDate>
      <link>https://dev.to/olegvdv/the-post-quantum-deadlines-are-closer-than-your-roadmap-thinks-so-i-built-a-cbom-scanner-55l9</link>
      <guid>https://dev.to/olegvdv/the-post-quantum-deadlines-are-closer-than-your-roadmap-thinks-so-i-built-a-cbom-scanner-55l9</guid>
      <description>&lt;p&gt;Three regulatory clocks are already ticking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;US:&lt;/strong&gt; EO 14412 / CNSA 2.0 requires post-quantum cryptography for new national-security acquisitions &lt;strong&gt;from 2027&lt;/strong&gt; — and explicitly names the "cryptographic bill of materials" (CBOM).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UK:&lt;/strong&gt; NCSC wants full cryptographic discovery and a migration plan &lt;strong&gt;by 2028&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EU:&lt;/strong&gt; member states start transitioning &lt;strong&gt;from the end of 2026&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yet when an auditor asks the first, most basic question — &lt;em&gt;"where exactly does your codebase use RSA?"&lt;/em&gt; — almost nobody can answer. Not because it's hard conceptually, but because nobody ever had a reason to keep an inventory of their cryptography. Now there's a reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "harvest now, decrypt later" actually means
&lt;/h2&gt;

&lt;p&gt;Even if a cryptographically relevant quantum computer is a decade away, traffic recorded &lt;strong&gt;today&lt;/strong&gt; can be decrypted &lt;strong&gt;then&lt;/strong&gt;. If your secrets have a shelf life of more than a few years (medical records, financial data, government contracts), classical key exchange is already a liability. That's the logic behind the deadlines — they don't wait for the quantum computer to exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  One command, three artifacts
&lt;/h2&gt;

&lt;p&gt;I wanted the discovery step to cost one command instead of one consulting engagement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @proofbyte/pqc-radar scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--cbom&lt;/span&gt; cbom.json &lt;span class="nt"&gt;--report&lt;/span&gt; report.md &lt;span class="nt"&gt;--sarif&lt;/span&gt; findings.sarif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This walks the repository and flags quantum-vulnerable cryptography — RSA, ECC/ECDH, DH, DSA, Curve25519, legacy hashes, weak TLS configs — across Java, Python, JavaScript/TypeScript, Go, C#, Rust, Ruby, PHP, C/C++ and nginx/Apache configs. Out come three artifacts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cbom.json&lt;/code&gt;&lt;/strong&gt; — a &lt;a href="https://cyclonedx.org/capabilities/cbom/" rel="noopener noreferrer"&gt;CycloneDX 1.6 CBOM&lt;/a&gt;: the machine-readable inventory the mandates ask for;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;report.md&lt;/code&gt;&lt;/strong&gt; — a human-readable report: every finding with file:line and its ML-KEM (FIPS 203) / ML-DSA (FIPS 204) migration target;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;findings.sarif&lt;/code&gt;&lt;/strong&gt; — SARIF 2.1.0, so findings land straight in GitHub code scanning.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's fast enough to live in CI: hashicorp/vault (4,827 files) scans in about 9 seconds; a weekly &lt;a href="https://github.com/oleg-vdv/proofbyte/blob/main/docs/CORPUS.md" rel="noopener noreferrer"&gt;corpus workflow&lt;/a&gt; re-scans five well-known OSS projects to catch regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking live endpoints, not just code
&lt;/h2&gt;

&lt;p&gt;Code is half the story — the other half is what your servers actually negotiate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @proofbyte/pqc-radar scan-tls your-api.example.com github.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One TLS handshake per host (no payload sent) reports the protocol, cipher, key-exchange group and certificate key — and whether the endpoint already speaks &lt;strong&gt;hybrid post-quantum key exchange&lt;/strong&gt; (X25519+ML-KEM-768). One honesty detail I'm proud of: when the local TLS stack can't &lt;em&gt;name&lt;/em&gt; the negotiated group (older OpenSSL builds can't name hybrid groups), the result is reported as &lt;strong&gt;inconclusive&lt;/strong&gt;, not as a finding. Unknown ≠ vulnerable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the v0 honestly can't do
&lt;/h2&gt;

&lt;p&gt;Detection is line-pattern based. That makes it fast, broad and dependency-free, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it will miss dynamically constructed crypto calls;&lt;/li&gt;
&lt;li&gt;unusual code can false-positive;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;absence of findings is not proof of absence&lt;/strong&gt; — the report says so explicitly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A deep AST engine (integrating &lt;a href="https://pqca.org/projects/cbomkit/" rel="noopener noreferrer"&gt;PQCA CBOMkit&lt;/a&gt;, which covers Java and Python) is the next milestone. There's also a fun war story: the very first regression test caught the scanner's regexes matching &lt;em&gt;their own source code&lt;/em&gt; — the fix was masking pattern literals with character-class tricks (&lt;code&gt;RSA_generate_ke[y]&lt;/code&gt;), and a permanent guard test asserting the repo only ever flags its own fixtures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security posture
&lt;/h2&gt;

&lt;p&gt;This is a tool you're supposed to run on code you can't share, so: fully offline (the &lt;code&gt;scan&lt;/code&gt; command makes zero network calls), zero runtime dependencies, read-only, symlinks not followed. &lt;code&gt;scan-tls&lt;/code&gt; is the single network feature and touches only endpoints you name. It's MIT-licensed — &lt;a href="https://github.com/oleg-vdv/proofbyte" rel="noopener noreferrer"&gt;read the source&lt;/a&gt; rather than trusting this paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @proofbyte/pqc-radar scan &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/oleg-vdv/proofbyte" rel="noopener noreferrer"&gt;https://github.com/oleg-vdv/proofbyte&lt;/a&gt; (issues and PRs welcome — especially new language patterns with fixtures)&lt;/li&gt;
&lt;li&gt;CI one-liner: &lt;code&gt;uses: oleg-vdv/proofbyte@v1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd genuinely like to hear from anyone whose org has already been asked for a CBOM — by whom, and what format they accepted.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>node</category>
      <category>cryptography</category>
    </item>
  </channel>
</rss>
