<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Staal Engineering]]></title><description><![CDATA[Discover Staal: An open-source, security-first language built in Rust and Cranelift to enforce strict compile-time safety and budgets on AI agents.]]></description><link>https://staal.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ae26d5d0b514c60a726b230/b6de1057-6315-4af2-b152-6ce1bc84e3c6.png</url><title>Staal Engineering</title><link>https://staal.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 30 Oct 2026 19:33:07 GMT</lastBuildDate><atom:link href="https://staal.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Least privilege, checked by a compiler]]></title><description><![CDATA[How Staal, a programming language for AI agents, turns the power to act into a value that code must hold, can only shrink, and cannot forge.]]></description><link>https://staal.hashnode.dev/least-privilege-checked-by-a-compiler</link><guid isPermaLink="true">https://staal.hashnode.dev/least-privilege-checked-by-a-compiler</guid><dc:creator><![CDATA[Laurels Ozy Echichinwo]]></dc:creator><pubDate>Fri, 30 Oct 2026 12:43:01 GMT</pubDate><content:encoded><![CDATA[<p><strong>TL;DR:</strong> In Staal, code can't send a message, write a file, call an API or move money unless it holds a <em>capability</em> for that action. Capabilities come only from the host app, can only be narrowed, are checked before every action, and are never handed to a model. Below: how that works, real compiler errors, a real run's log, and a benchmark against plain Python.</p>
<h2>Nine seconds</h2>
<p>On April 24, 2026, an AI coding agent deleted a company's production database in nine seconds.</p>
<p>It was doing routine maintenance in staging for PocketOS, which builds software for car rental businesses. It hit a credential mismatch, searched the filesystem, and found an API token in an unrelated file. The token had been created to manage custom domains, but it was root-scoped: it could perform any operation the hosting provider's API offered. One call later, the production volume was gone, along with every backup stored inside it.</p>
<p>The model made a bad call, but that isn't the interesting part. The task needed to fix a configuration in staging. The token could delete production. Nothing between the agent and the API knew what the task was, so nothing could say <em>that's too much power for this</em>.</p>
<p>It's a pattern. In July, researchers showed that an instruction left in a public GitHub issue could steer an agentic workflow into leaking private repository data. In Gravitee's survey of 750 organisations this year, excessive permissions was the most consistently reported failure: agents with more access than their task needs, often through shared service accounts and inherited credentials.</p>
<p>Models will keep being wrong, and keep being talked into things. What decides the damage is how much the agent was able to do.</p>
<h2>The payment networks already know the answer</h2>
<p>Where agents touch money, the industry has stopped handing them general-purpose credentials. Stripe's agentic commerce tokens are scoped to one merchant, one session and one amount range, and usable once. Mastercard's Agent Pay makes an agent present a signed record of what it was asked to do, and the network stops a transaction that goes beyond it.</p>
<p>That's capability security, an idea that's decades old: instead of asking <em>who are you and what may you do?</em>, hand out unforgeable tokens that each carry one bounded power. The payment networks are building it into their rails one network at a time. Staal builds it into the language, for every action an agent's code takes.</p>
<h2>Authority as a value</h2>
<p>A capability is part of a function's type, and the type says how far it reaches:</p>
<pre><code class="language-text">fn remind(patients: List&lt;Patient, 50&gt;, texts: cap sms.send(uses: 50, only: ["234*"])) -&gt; Int
fn archive(report: Report, files: cap fs.write(only: ["/reports/*"])) -&gt; Receipt
fn open_doors(lock: cap building.lock.open(uses: 2, only: ["north-door", "loading-bay"])) -&gt; Done
fn refund(r: Refund, pay: cap payments.refund(max: 500 USD, each: 50 USD)) -&gt; Receipt
</code></pre>
<p>The first can send at most 50 texts, only to Nigerian numbers. The second can write only under <code>/reports</code>. The third can open the north door and the loading bay, at most twice in all. The fourth can refund at most 50 USD at a time and 500 USD in total.</p>
<table>
<thead>
<tr>
<th>Limit</th>
<th>What it bounds</th>
</tr>
</thead>
<tbody><tr>
<td><code>only</code></td>
<td>which targets an action may touch: exact text, or a pattern with one <code>*</code></td>
</tr>
<tr>
<td><code>uses</code></td>
<td>how many actions may be attempted, failed ones included</td>
</tr>
<tr>
<td><code>each</code></td>
<td>the most money one action may move</td>
</tr>
<tr>
<td><code>max</code></td>
<td>the most money moved in total over the run</td>
</tr>
</tbody></table>
<p>The names after <code>cap</code> aren't built in. <code>sms.send</code>, <code>building.lock.open</code> and <code>payments.refund</code> are paths chosen by whoever deploys the program, and the rules treat them all alike. You can read a signature like these in code review and know the most the function can ever do before you read its body.</p>
<h2>Only the app creates authority</h2>
<p>Nothing inside a Staal program can create a capability: no function, no library, no model. They come from the host app that starts the run, which is ordinary Python or Node.js and the only place that knows which customer this is and what they're entitled to:</p>
<pre><code class="language-python">import staal

lib = staal.Library()
TIER_LIMIT = {"gold": 500, "standard": 50}  # USD per refund, decided by your app

def handle(customer, order, complaint, model):
    limit = min(TIER_LIMIT[customer["tier"]], order["total"])
    return lib.run("refund.staal", {
        "entry": "main",
        "inputs": {"complaint": complaint},
        "grants": {"pay": {"cap": "payments.refund",
                            "each": {"amount": str(limit), "currency": "USD"},
                            "only": [order["payment_id"]]}},
        "model": model,
    })
</code></pre>
<p>The app decides; Staal enforces, and so does the code's own signature. If the program declares <code>each: 500 USD</code> and the app grants 800 by mistake, the run gets 500. A grant can narrow what the code accepts, never widen it.</p>
<p>The config is validated before anything runs: a missing grant or an unknown key means nothing starts. And capabilities, receipts and outcomes are never accepted as inputs at any depth, so neither input data nor a model can forge one.</p>
<p>That's the answer to the token in the unrelated file. A token found lying around is only text. Acting with it still needs a capability for that API, and only the app can grant one.</p>
<h2>Narrowed, never widened</h2>
<p>A capability can be passed on, and every time it's passed on it can only shrink:</p>
<pre><code class="language-text">type Patient { name: Text, phone: Text }

fn remind(patients: List&lt;Patient, 50&gt;, texts: cap sms.send(uses: 50, only: ["234*"])) -&gt; Int {
    let sent = 0
    for p in patients {
        let r = try notify(p, texts)
        if r.ok {
            set sent = sent + 1
        }
    }
    return sent
}

fn notify(p: Patient, texts: cap sms.send(uses: 1, only: ["234*"])) -&gt; Receipt {
    return texts.send(p.phone, "Hello " + p.name + ", see you tomorrow.")
}
</code></pre>
<p>Each call to <code>notify</code> gets a child capability with one use. Everything the child does counts against its parent, and the parent's parent, up to the grant. That's delegation in a multi-agent system, checked by a compiler.</p>
<p>Going the other way doesn't compile:</p>
<pre><code class="language-text">widen.staal:4:21: error[E003]: in call to 'blast': capability limited to 1 uses cannot be widened to 100 uses
</code></pre>
<p>And neither does acting without one:</p>
<pre><code class="language-text">nocap.staal:2:12: error[E002]: 'files.write' acts on the world but 'files' is not a capability in scope
</code></pre>
<p>Both come from <code>staalc check</code>, before anything runs. Widening covers every limit: a higher <code>max</code>, <code>each</code> or <code>uses</code>, a dropped limit, or an <code>only</code> pattern that reaches outside the original. The pattern check is deliberately conservative.</p>
<h2>Checked before every action</h2>
<p>The compiler proves the program never asks for more than it holds. The runtime makes sure it never uses more either.</p>
<p>Every capability lives in a broker, and a program only ever holds a handle. When the program acts, the broker checks the action against that capability and every capability it was narrowed from, before the service is called. Here's the reminder program above, granted two uses for this run, over four patients. The third has a UK number:</p>
<pre><code class="language-json">{"entry": "remind",
 "inputs": {"patients": [
   {"name": "Ada",   "phone": "2348012345678"},
   {"name": "Tunde", "phone": "2348098765432"},
   {"name": "Mary",  "phone": "447700900123"},
   {"name": "Chidi", "phone": "2348055555555"}]},
 "grants": {"texts": {"cap": "sms.send", "uses": 2}}}
</code></pre>
<p>The run returns <code>2</code>. Its log, condensed:</p>
<pre><code class="language-text">action_performed  sms.send 2348012345678
action_performed  sms.send 2348098765432
action_refused    'sms.send' may only act on ["234*"]; this action targets "447700900123"
action_refused    'sms.send' has used all 2 of its allowed uses
</code></pre>
<p>The UK number hit the limit the code declared. The fourth patient hit the limit the app granted. Neither request reached the SMS service.</p>
<p>Three details that matter:</p>
<ul>
<li><strong>Uses count attempts.</strong> A use is spent when the broker approves the action, so a failed request still costs one and a loop can't retry its way past a limit.</li>
<li><strong>Limits fail closed.</strong> A limit that can't determine the amount or target refuses the action.</li>
<li><strong>Money is charged even on failure</strong> when a request may have moved it, so failures can't be used to spend past <code>max</code>.</li>
</ul>
<h2>Models propose, programs act</h2>
<p>A model never holds a capability. <code>propose</code> keeps the instruction (which must be trusted) apart from the data (which may be anything), and the answer comes back as <code>Proposed&lt;T&gt;</code>, which can't reach an action until <code>validate</code> checks it against <code>T</code>. This gets both rules wrong:</p>
<pre><code class="language-text">fn handle(ticket: Text, pay: cap payments.refund(each: 50 USD)) -&gt; Text budget(calls: 1, tokens: 500) {
    let d = propose(Decision, instruct: ticket, data: [pay], tokens: 500)
    return "ok"
}
</code></pre>
<pre><code class="language-text">model.staal:3:41: error[E006]: untrusted value used as a model instruction; pass it in `data: [...]` instead, or mark its source @trusted
model.staal:3:56: error[E101]: a capability cannot be sent to a model as data
</code></pre>
<p>Everything that comes back from the world is untrusted by type: HTTP bodies, tool results, model output (even after validation). So an injected instruction can change what the model <em>asks for</em>, but the proposal still has to pass validation, and the action still has to pass the broker, which holds a limit the model can't see or reach.</p>
<h2>Keys out of reach</h2>
<p>Real services need keys, URLs and HTTP access. None of those belong to the program that wants to act. They belong to a library that declares a capability:</p>
<pre><code class="language-text">capability termii.sms {
    needs http: cap net.http.post
    needs key: Secret
    needs base: Text
    needs sender: Text

    canonical(phone: Text) -&gt; Text {
        return number(phone)
    }

    action send(to: Text @target, text: Text) -&gt; Text {
        ...   // one request through `http`
    }
}
</code></pre>
<p>The host grants the library its key (by environment variable name, so it never lands in a config, journal or log), its URL, and an HTTP capability narrowed to the one endpoint. The program asks for <code>cap termii.sms.send(uses: 50)</code> and holds the right to send texts, nothing else. <code>only</code> is matched against the library's canonical form, so <code>+234 801 234 5678</code> and <code>2348012345678</code> are the same number to the broker.</p>
<p>Secrets never become text. A <code>Secret</code> can't be printed, interpolated or encoded, even inside the library. When a benchmark edit tried to log request headers:</p>
<pre><code class="language-text">error[E101]: `json` cannot encode a Header: secrets never become text
</code></pre>
<h2>Why it holds as code changes</h2>
<p>Any careful team can build least privilege into an agent on the day it ships. The hard part is keeping it. So I built the same support agent twice, in Python alone and in Python with Staal, with the same limits, permissions, retries and crash recovery. Both passed all 47 scenarios: prompt injection, malformed model output, permission escalation, failing services, and the process killed before and after every external call. Python took 312 lines; Staal, 159.</p>
<p>Then I made six changes to each, the kind teams make months later:</p>
<table>
<thead>
<tr>
<th>Change</th>
<th>Python alone</th>
<th>Python with Staal</th>
</tr>
</thead>
<tbody><tr>
<td>Put the ticket into the model's instruction</td>
<td>Shipped; injection rule broken</td>
<td>Didn't compile (E006)</td>
</tr>
<tr>
<td>Drop validation of the plan</td>
<td>Shipped; malformed plans acted on</td>
<td>Didn't compile (E001)</td>
</tr>
<tr>
<td>Ask the model again to polish the email</td>
<td>Shipped; over the model budget</td>
<td>Didn't compile (E004)</td>
</tr>
<tr>
<td>Refactor each step into its own helper</td>
<td>Shipped; two refunds where one was allowed</td>
<td>Ran; limits held</td>
</tr>
<tr>
<td>Resend the email if the mail service fails</td>
<td>Shipped; a customer emailed twice</td>
<td>Ran; limits held</td>
</tr>
<tr>
<td>Log every request, headers included</td>
<td>Shipped; API keys written to a log</td>
<td>Didn't compile (E101)</td>
</tr>
</tbody></table>
<p>The refactor is the one worth studying. In Python, "one refund per job" lived in a policy object, and the refactor built a fresh one per step, silently resetting its counter. In Staal, the refactor passed the capability to a helper, the broker counted the use against the original grant, and the second refund was refused. The limit was never in the code that changed.</p>
<p>In ordinary code, least privilege is a convention every future change has to respect. In Staal, it's a property the compiler and the broker keep, whoever edits the code next.</p>
<h2>Try it</h2>
<p>Your app stays in Python or Node.js. Only the part that acts is Staal:</p>
<pre><code class="language-bash">pip install staal
npm install @staal/node
</code></pre>
<p>Packages are built and tested for Linux (x86_64, arm64) and macOS (Apple Silicon, Intel). Apache-2.0 with the LLVM exception.</p>
<ul>
<li>Repo and quickstart: <a href="https://github.com/laurells/staal">github.com/laurells/staal</a></li>
<li>Benchmark: <a href="https://github.com/laurells/staal/tree/main/bench/agent">bench/agent</a></li>
<li>Runtime spec: <a href="https://github.com/laurells/staal/blob/main/docs/RUNTIME.md">docs/RUNTIME.md</a></li>
</ul>
<p>If you're building agents that touch money, messages or records, I'd like to hear where this would and wouldn't fit. Leave a comment, or open an issue on the repo.</p>
<hr />
<p><strong>Sources:</strong> <a href="https://www.giskard.ai/knowledge/a-cursor-ai-agent-wiped-a-production-database-in-9-seconds-excessive-agency-ai-failure">Giskard on the PocketOS deletion</a> · <a href="https://curity.io/blog/5-agentic-ai-security-incidents-and-vulnerabilities/">Curity: 5 agentic AI security incidents</a> · <a href="https://www.gravitee.io/state-of-ai-agent-security">Gravitee: State of AI Agent Security 2026</a> · <a href="https://atxp.ai/blog/mastercard-agent-pay-stripe-acp-visa-x402-comparison-2026">ATXP: agent payment protocols compared</a></p>
]]></content:encoded></item><item><title><![CDATA[Beyond Prompt Engineering: Why I Built a New Programming Language for AI Agents in Rust]]></title><description><![CDATA[We are currently living through a massive architectural shift. We are moving quickly from simple chatbots to autonomous AI agents—systems designed to…]]></description><link>https://staal.hashnode.dev/beyond-prompt-engineering-why-i-built-a-new-programming-language-for-ai-agents-in-rust</link><guid isPermaLink="true">https://staal.hashnode.dev/beyond-prompt-engineering-why-i-built-a-new-programming-language-for-ai-agents-in-rust</guid><category><![CDATA[Rust]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[backend]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Laurels Ozy Echichinwo]]></dc:creator><pubDate>Wed, 28 Oct 2026 22:11:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ae26d5d0b514c60a726b230/ed24a681-1198-4172-b189-8c5c3235ae56.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We are currently living through a massive architectural shift. We are moving quickly from simple chatbots to autonomous AI agents—systems designed to execute multi-step workflows, modify databases, interface with external APIs, and manage real financial transactions. But as backend engineers, we are secretly terrified. Mainstream software environments were never designed to handle non-deterministic logic. When you pass an untrusted string generated by a large language model directly into a Python or Node.js runtime, you introduce a massive blast radius. A single hallucination or an unpredicted prompt injection can lead to catastrophic state errors, corrupted databases, or out-of-bounds API spending. Right now, the industry is writing massive, brittle wrapper structures just to keep these agents from breaking things. We are prompting, and we are hoping. I realized that trying to solve this problem defensively at the application layer was a losing battle. Safety and predictability shouldn't be an afterthought handled by your application framework; they should be enforced fundamentally by the compiler. This realization led me to build Staal—a specialized domain language designed specifically for software that acts on the real world.</p>
<p>The Architecture: A Sandboxed Policy Enforcer</p>
<p>The goal of Staal isn't to replace mainstream backend languages like Python or TypeScript, but rather to complement them. It acts as an isolated, sandboxed execution layer. Your primary application handles the high-level orchestrations while dropping down into a strictly bounded Staal runtime to execute dangerous real-world side effects. The underlying compiler (staalc) and reference runtime are built from scratch entirely in Rust, leveraging Cranelift for lightning-fast native machine code generation. By designing a dedicated language instead of an application macro library, I was able to bake four critical systems-level safety principles directly into the runtime semantics:</p>
<ol>
<li>Compile-Time Capability Brokering</li>
</ol>
<p>In a traditional microservice, if an application process has access to an API key or a database credential, every single function within that process shares that exact same authority. If an agent compromises one vector, it compromises everything. Staal shifts this security down to the language level. Before a single line of agent code can compile, the compiler runs through strict semantic safety rules (E001–E008) to prove that the execution path has been explicitly granted authority. If an agent attempts to hit a payment network or trigger a communication channel without a validated capability token passed directly into its scope, compilation fails instantly.</p>
<ol start="2">
<li>Crash-Safe Execution (Durable Journaling)</li>
</ol>
<p>In network environments, systems drop offline, servers lose power, and processes crash in the middle of transactions. When an autonomous agent is responsible for critical logic like moving capital, a half-executed state is highly dangerous. To solve this, the language runtime treats real-world actions like atomic database transactions. By binding execution tightly to a local, durable pre-and-post-write journal, the language creates a permanent transaction log of external intent. If the host environment abruptly restarts or loses its connection mid-flight, the system doesn't duplicate actions or panic. It simply reads the journal and safely resumes exactly where it dropped off.</p>
<ol start="3">
<li>Exact Money Primitives</li>
</ol>
<p>Mainstream runtimes struggle with exact financial representation because floating-point binary math introduces tiny, silent rounding bugs (like 0.1 + 0.2 not equaling 0.3). In a system designed to automate financial operations, those fractions matter. By baking exact decimal currency types natively into the language semantics, we completely eliminate rounding errors from the equation.</p>
<ol start="4">
<li>The Policy/Enforcer Split</li>
</ol>
<p>The integration boundary is designed to keep development ergonomic. Using our native C interface, we built wrappers for Python (pip install staal) and Node.js (npm install @staal/node). Your host app decides the dynamic policy constraints (e.g., matching thresholds by user tiers), passes those constraints into the runtime broker as capability grants, and Staal guarantees that the agent cannot breach them.</p>
<p>Rereturning to First Principles</p>
<p>Building a new programming language from scratch is a massive undertaking, but the frontier of AI automation requires a return to classic, rigorous systems-level safety. By combining the speed of native code generation via Cranelift with the absolute safety of a Rust-built compiler, we can create an iron-clad vault for autonomous workflows. Staal is currently pre-release and fully functional open-source software under the Apache-2.0 license (with LLVM exceptions). I am incredibly eager to see how other systems engineers, security researchers, and AI builders view this approach to agentic containment. If you are interested in compiler safety, type systems, or building resilient infrastructure for the next wave of automation, check out the repository on GitHub at laurells/staal. I would love to hear your thoughts, critiques, and feedback on the architecture!</p>
]]></content:encoded></item></channel></rss>