<?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: Samet Samyeli</title>
    <description>The latest articles on DEV Community by Samet Samyeli (@sametsamyeli).</description>
    <link>https://dev.to/sametsamyeli</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%2F4012559%2F1d39e321-3845-406d-bfa9-e5712fcc077a.jpg</url>
      <title>DEV Community: Samet Samyeli</title>
      <link>https://dev.to/sametsamyeli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sametsamyeli"/>
    <language>en</language>
    <item>
      <title>We Built a Workflow Engine That Can’t Have n8n’s CVE - 1/5</title>
      <dc:creator>Samet Samyeli</dc:creator>
      <pubDate>Thu, 02 Jul 2026 16:40:07 +0000</pubDate>
      <link>https://dev.to/sametsamyeli/we-built-a-workflow-engine-that-cant-have-n8ns-cve-15-3gci</link>
      <guid>https://dev.to/sametsamyeli/we-built-a-workflow-engine-that-cant-have-n8ns-cve-15-3gci</guid>
      <description>&lt;p&gt;Hey, I'm Samet Samyeli. I have been actively developing software for 12 years. For the past six months, I have been building "lodos.md" a "Founder OS" featuring bank-grade security where AI never sees your secrets (eliminating leakage risks) and utilizing smart workflows that integrate the "Karpathy LLM Wiki" directly into the engine's core to reduce hallucinations to near-zero levels; it employs a continuous memory system based on Markdown files rather than sessions. I am sharing the first article of our five-part series with you.&lt;/p&gt;

&lt;p&gt;n8n had &lt;strong&gt;CVE-2025-68613&lt;/strong&gt; last month — CVSS 9.9, RCE via expression eval. That's not the interesting part.&lt;/p&gt;

&lt;p&gt;The interesting part is that the entire class of vulnerability is structurally absent from the workflow engine I designed for lodos — not because we sandbox better, not because we patched faster, but because there is no expression evaluator anywhere in the pipeline to inject into. The build script greps for it. If a future me adds &lt;code&gt;eval&lt;/code&gt; or &lt;code&gt;new Function&lt;/code&gt; or &lt;code&gt;vm.runInContext&lt;/code&gt; to the workflow surface, the build turns red before the commit lands.&lt;/p&gt;

&lt;p&gt;This post is the architecture walkthrough: what "declarative-bounded" actually means at the level of the YAML schema, why I chose to lose Turing-completeness on purpose, and the specific grep that holds the line.&lt;/p&gt;

&lt;p&gt;If you build workflow tooling — or any system that takes user-provided expressions and runs them — the trade is worth examining honestly. n8n made a different one, and they had a CVSS 9.9 RCE for it. I made this one, and I have a workflow engine that's strictly less expressive than theirs. Pick your tradeoff knowingly.&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%2Fxlfs3lfyzluoti72qtgu.webp" 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%2Fxlfs3lfyzluoti72qtgu.webp" alt="lodos.md workflow engine" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "declarative" actually means here
&lt;/h2&gt;

&lt;p&gt;The lodos workflow engine exposes five primitives. That's it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;http_request&lt;/code&gt; — method, URL template, headers, body, &lt;code&gt;allowedEgress&lt;/code&gt; host list, &lt;code&gt;timeoutMs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db_query&lt;/code&gt; — SELECT-only against the local SQLite, with a static denylist for vault/billing tables&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ai_call&lt;/code&gt; — hardcoded &lt;code&gt;api.anthropic.com&lt;/code&gt; egress, &lt;code&gt;noSecrets: true&lt;/code&gt; (transcript stripped before send)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tool_call&lt;/code&gt; — read-only-auto MCP tools, guard-gated, supports optional-degrade&lt;/li&gt;
&lt;li&gt;Control flow — &lt;code&gt;wait&lt;/code&gt;, &lt;code&gt;if_else&lt;/code&gt;, &lt;code&gt;loop&lt;/code&gt; with &lt;strong&gt;closed-enum comparators&lt;/strong&gt; (&lt;code&gt;eq&lt;/code&gt;, &lt;code&gt;neq&lt;/code&gt;, &lt;code&gt;gt&lt;/code&gt;, &lt;code&gt;lt&lt;/code&gt;, &lt;code&gt;changed-since-last-run&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last line is the one I want to defend. Most workflow engines I've seen — n8n, Zapier, Temporal even — let you write a JS expression in the condition slot. &lt;em&gt;If this number is greater than 5, branch left.&lt;/em&gt; The expression evaluator is convenient for users and a cliff for security teams. So we don't have one. The comparator is an enum; the comparison is data; there is no path from a user-edited YAML field to a function call.&lt;/p&gt;

&lt;p&gt;You lose things. You can't write &lt;code&gt;if step.result.users.filter(u =&amp;gt; u.active).length &amp;gt; 5&lt;/code&gt;. You can write &lt;code&gt;if step.result.activeUserCount gt 5&lt;/code&gt; and produce &lt;code&gt;activeUserCount&lt;/code&gt; upstream in a &lt;code&gt;db_query&lt;/code&gt; or &lt;code&gt;ai_call&lt;/code&gt;. Computation moves to the primitives that already exist; the workflow definition stays declarative.&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%2F14n8j8865zmundjk2wb6.webp" 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%2F14n8j8865zmundjk2wb6.webp" alt="lodos.md declarative-bounded" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema discipline
&lt;/h2&gt;

&lt;p&gt;The workflow YAML parses through a Zod schema with &lt;code&gt;z.lazy&lt;/code&gt; for recursion (loops and &lt;code&gt;if_else&lt;/code&gt; can nest). Unbounded &lt;code&gt;z.lazy&lt;/code&gt; is a different name for eval — a malicious YAML can blow the stack or the heap before any handler runs. So the schema is double-bounded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;StepSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZodType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Step&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;discriminatedUnion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;HttpRequestStepSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;DbQueryStepSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;AiCallStepSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;ToolCallStepSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;WaitStepSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;IfElseStepSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// contains: steps[] (z.lazy, max 16)&lt;/span&gt;
    &lt;span class="nx"&gt;LoopStepSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// contains: body[]  (z.lazy, max 16)&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Walk-time enforced in addition to per-array caps:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_TOTAL_NODES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_DEPTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three bounds, every one of them necessary. &lt;code&gt;per-array .max(16)&lt;/code&gt; keeps any single block from being a megabyte of nested branches. &lt;code&gt;total-nodes 100&lt;/code&gt; bounds the whole graph (you can't smuggle complexity past the per-array cap by spreading thin). &lt;code&gt;depth 5&lt;/code&gt; keeps the recursive walk constant-time relative to the input. None of the three is paranoia; each closes a class of resource attack that a real workflow user would never write but a hostile YAML would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;secret_value_get&lt;/code&gt; doesn't exist
&lt;/h2&gt;

&lt;p&gt;There is no MCP tool in the system whose contract is &lt;em&gt;give me the plaintext of a secret&lt;/em&gt;. There's a six-layer &lt;code&gt;secret_inject_and_run&lt;/code&gt; that takes a secret reference and an argv, sets the value in a subprocess env, runs the command, and never returns the value to the calling AI. That's it.&lt;/p&gt;

&lt;p&gt;This isn't a policy ("don't add such a tool"). The build script asserts the absence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/verify-moat-invariants.mjs — INV-M1 (simplified)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FORBIDDEN_TOOL_NAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;bash&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;exec&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;shell&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/_value_get$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// catches secret_value_get, dek_value_get, anything _value_get&lt;/span&gt;
  &lt;span class="sr"&gt;/^secret_value_/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mcpIndexSrc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apps/mcp/src/index.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;FORBIDDEN_TOOL_NAMES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mcpIndexSrc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`INV-M1 VIOLATION: forbidden tool pattern &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&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;If some future me — or a future AI editor working on this codebase — convinces themselves they need a value-returning secret tool, the build refuses before the merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  INV-M5 — the eval tripwire
&lt;/h2&gt;

&lt;p&gt;The corresponding check for the workflow engine is even simpler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// INV-M5: no eval-class primitive in workflow / skill / deck surfaces&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FORBIDDEN_EXEC_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;eval&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;new&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+Function&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;vm&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;runIn|compileFunction&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;require&lt;/span&gt;&lt;span class="se"&gt;\([&lt;/span&gt;&lt;span class="sr"&gt;'"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;child_process&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;'"&lt;/span&gt;&lt;span class="se"&gt;]\)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;import&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+.*&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;from&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;'"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;child_process&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;'"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron/workflow/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron/skills/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron/deck/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;walkTs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;FORBIDDEN_EXEC_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`INV-M5 VIOLATION in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;Fifty lines of Node, zero dependencies, runs in the standard &lt;code&gt;pnpm verify&lt;/code&gt; step. The whole class of CVE that n8n's CVSS-9.9 expression-RCE lives in is closed by a grep.&lt;/p&gt;

&lt;p&gt;The discipline that makes this real is the &lt;strong&gt;negative proof&lt;/strong&gt;: the script ships with its own fake violation. A throwaway &lt;code&gt;__probe.ts&lt;/code&gt; file with a literal &lt;code&gt;eval(&lt;/code&gt; in it. CI runs the script twice — first with the probe injected (must exit 1), then with it removed (must exit 0). A refusal-detector that never fails is indistinguishable from no detector at all; the negative proof is what gives this script teeth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature growth through narrowing, not relaxation
&lt;/h2&gt;

&lt;p&gt;The skeptical reader's question: doesn't this break when you need more features?&lt;/p&gt;

&lt;p&gt;When I shipped &lt;code&gt;web_fetch&lt;/code&gt; for L2 web egress, I didn't add an evaluator. I added: narrow-host allowlist + per-source byte budget + redirect-reauth + SSRF-against-self + render-firewall + tainted-content propagation. Twenty-six new test cases, zero new eval surface. The engine got more powerful by adding &lt;em&gt;bounds&lt;/em&gt;, not by adding &lt;em&gt;expression power&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You can do this. Most growth in workflow tools is interpreted as "give the user more eval surface." It can be interpreted instead as "give the user more bounded primitives." The latter is harder to design and impossible to grow into a CVSS 9.9.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this costs and why I paid it
&lt;/h2&gt;

&lt;p&gt;You give up: arbitrary in-step computation, JS expression conditions, dynamic field projection, whatever the user can do in n8n's &lt;code&gt;{{ $json.foo.map(x =&amp;gt; x * 2) }}&lt;/code&gt;. Real workflow users do reach for these, and when they do in lodos they reach for them by writing one more &lt;code&gt;ai_call&lt;/code&gt; or &lt;code&gt;db_query&lt;/code&gt; step. The workflow definition stays declarative; the eval surface stays empty.&lt;/p&gt;

&lt;p&gt;You get: a workflow engine where CVE-2025-68613 is structurally impossible, where the schema enforces bounded resource consumption, and where every refusal is enforced by a grep, not a docstring.&lt;/p&gt;




&lt;p&gt;The next post in the series goes one layer down into the vault. “AI never see sk_live” the AI cannot see your Stripe live key is the architectural claim, and the schema fragment that backs it up is the one I’m most proud of. Zero-Knowledge as Architectural Blindness explains why our Prisma schema literally cannot hold a plaintext field path, and how that buys us the SOC2-grade audit story for free.&lt;/p&gt;

&lt;p&gt;Join waitlist: &lt;a href="https://lodos.md/" rel="noopener noreferrer"&gt;https://lodos.md/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>architecture</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
