Guarding the Way

There's a test in my agent runtime that has never passed. It can't. The entire security model depends on it failing.

// This test proves that CapabilityToken::new() is inaccessible outside the
// enforcement module. It must fail to compile.
fn main() {
let _token = CapabilityToken::new(Tier::Observe);
}

The test harness compiles this file and asserts the compiler rejects it. If anyone ever refactors the token so that code outside the enforcement layer can construct one, CI goes red. Not because a reviewer caught it. Because the compiler did.

Every AI agent runtime faces the same core problem: the model proposes actions, and something has to decide which ones execute. Every runtime claims to have a layer for this. The question that matters is whether that layer is the only path from model output to tool execution, and who enforces the word "only."

This week I went looking for that answer in my biggest competitor's codebase.

The competition

IronClaw is NEAR AI's agent runtime. It's a serious project: 74 crates, 95 distinct committers in the last three months (a few of them bots), 2,467 commits since March. NEAR co-founder Illia Polosukhin has personally landed over 200 commits in that window. Releases ship roughly every two weeks.

Their architecture documents describe capability hosts, time-scoped approval leases, trust-class routing that separates host actions from user actions, and a runtime policy resolver where constraints can only reduce authority. On paper, it's the same thesis as mine: the model proposes, a deterministic layer decides.

Full disclosure: I build a competing runtime called Cherub. Alone, around a day job and three kids. Read everything below with that bias in mind. Every claim comes with a file path against IronClaw's main branch as of June 9, 2026 (commit fabaddc7, just past v0.29.1), so you can check my work.

Finding 1: the architecture in the docs is not the architecture that runs

IronClaw's new capability-gated engine connects to the rest of the system through a bridge module. The doc comment at the top of src/bridge/mod.rs is admirably honest:

Engine v2 bridge — connects ironclaw_engine to existing infrastructure.

Strategy C: parallel deployment. When ENGINE_V2=true, user messages route through the engine instead of the existing agentic loop. All existing behavior is unchanged when the flag is off.

And in src/config/agent.rs:

engine_v2: parse_bool_env("ENGINE_V2", false)?,

The flag defaults to off.

The engine with the capability projector, the gate controller, the per-tool permission surface: all real code, all tested, all shipped in the binary. And not running. The default execution path is the legacy agentic loop, the one all of this was built to replace. The same is true of the newer "Reborn" kernel crates (ironclaw_authorization, ironclaw_approvals, ironclaw_runtime_policy): present in the workspace, well-designed, and not the path your message takes when you install IronClaw today.

None of this is hidden. The comment says "parallel deployment" right at the top. But every README and architecture doc describes the destination. The binary ships the starting point.

Finding 2: the capability lease derives Clone

When IronClaw's new engine approves an action, the approval becomes a CapabilityLease: a token representing authority to execute. Here is its definition in crates/ironclaw_authorization/src/lib.rs:

/// Capability lease issued from an approved request or policy workflow.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityLease {
pub scope: ResourceScope,
pub grant: CapabilityGrant,
pub invocation_fingerprint: Option<InvocationFingerprint>,
pub status: CapabilityLeaseStatus,
}

impl CapabilityLease {
pub fn new(scope: ResourceScope, grant: CapabilityGrant) -> Self { /* ... */ }
}

Public constructor. Public fields. Clone. Deserialize.

Any code in any of the 74 crates can mint a lease, copy a lease, or parse one out of JSON. Nothing distinguishes a lease issued by the approval resolver from a lease constructed by a struct literal three crates away. The authority is a plain old data type.

Nobody decided to make authority forgeable. Someone needed the lease inside a struct that derived Serialize, the derive macro was the path of least resistance, and the compiler had no opinion. When enforcement is convention, every contributor is one derive away from breaking it, and the build stays green.

Cherub's equivalent token has a pub(super) constructor, no Clone, no Copy, no Default, no From. The compile-error test at the top of this post exists so that this stays true after I've forgotten why it matters.

Finding 3: MUST is not a type

Here is the doc comment on IronClaw's channel-facing tool dispatcher, src/tools/dispatch.rs, lightly trimmed:

Sanitization scope. sanitize_tool_output runs only against the audit-row payload, not against the value returned to the caller. This mirrors Worker::execute_tool (the agent loop also receives the raw output so its reasoning can be reproduced from history). Channels that forward dispatcher output to end users (gateway responses, webhook replies, etc.) MUST run their own boundary sanitization at the channel edge.

Approval checks are skipped — channel-initiated operations are user-confirmed by definition.

Read that as a security engineer. It's a correct, thoughtful description of a real invariant, written by someone who clearly understands the system. And it's enforced by nothing. Every future channel author has to find this comment, understand it, and remember it. The word MUST is doing the work a type should do.

Notice the other two things it tells you. There are at least two parallel paths that execute tools, and one "mirrors" the other. Mirrors drift. And approval checks are skipped on this path because channel operations are user-confirmed "by definition." Definitions don't compile either.

Why this happens, and why it's not an IronClaw problem

Let me be fair, because the easy version of this post would be a hit piece and that's not what the evidence supports.

IronClaw has shipped things Cherub hasn't: eight channels, a web UI with SSO, WASM-sandboxed tools with credential injection at the host boundary and leak scanning in both directions. Some of that is genuinely ahead of my own work. And the team obviously knows where their gaps are. The new engine and the Reborn kernel exist precisely because they're migrating toward compiler-checkable boundaries. A migration across 74 crates with 95 contributors takes quarters. Flags default to off during migrations. That's responsible engineering, not negligence.

The lesson is not that IronClaw is insecure. Part of why this matters is that you cannot evaluate an agent runtime by reading its architecture docs. In a fast-moving codebase, the docs describe intent. The diff between intent and the default execution path is exactly where the next generation of agent security incidents will live.

Five questions to ask any agent runtime

These took me an afternoon to answer for a codebase I'd never read. They'll work on any runtime.

Can the capability token be constructed outside the enforcement layer? Open the struct. Look for pub fields, a pub fn new, or derive(Clone, Default, Deserialize). Each one is a forging path.

Is the policy engine the default path or a flag? Grep for the environment variable that turns it on. If security is opt-in, the default is the product.

How many routes lead from model output to tool execution? One chokepoint, or several paths that "mirror" each other? Count them. Each extra route is a place the rules get re-implemented, or don't.

What does the model see when it's denied? If rejections include rule names or pattern text, the policy leaks to the thing being policed, and the model can probe its way around it. Cherub returns "action not permitted." Nothing else.

What happens to an unknown tool or an unmatched action? Deny, allow, or ask? Ask-by-default with every tool visible is allow-by-default wearing a seatbelt.

Where this leaves me

Honest accounting: Cherub is one crate with one connector, 727 passing tests, no web UI, no marketplace, no funding, and a development cadence best described as "when the kids are asleep." IronClaw will out-ship me forever. That stopped bothering me when I understood it isn't the game.

The game is that some invariants are too important to be requests. A security invariant in a comment is a request. A security invariant in the type system is a fact. When you pick an agent runtime, or build one, count the facts.

OpenClaw, IronClaw. The agent space has a naming problem. I named mine after the cherubim posted at the gate of Eden. Their entire job was guarding the way.

Both codebases are open: Cherub and IronClaw. Pick whichever runtime you're betting on and spend an afternoon tracing one tool call from model output to execution. You'll learn more than any README will tell you.