Threads · 04 / 18 · Sep 7, 2026 · Apache-2.0

Autonomy belongs to the capability, not to the agent

One switch for the whole agent forces a choice between an agent that is useless and one that is dangerous. Reading a balance can be automatic while issuing a credit note needs approval — in the same agent, in the same turn.

The test

Two capabilities of the same agent, one policy table, both set to run unattended, both with the same ceiling. The amount goes over the line:

const lectura = run(readBalance,  policy["billing.balance.read"], {});
const credito = run(issueCredit,  policy["billing.credit.issue"], { amount: "250.00" });

expect(lectura.kind).toBe("executed");        // still runs
expect(credito.kind).toBe("needs_approval");  // steps back

Same agent, same turn, same table, same ceiling — different answers. That is the whole thesis, and it is four lines.

Why one switch is not a smaller version of this

With a single autonomy setting, going over the ceiling on the credit note stops the balance read too. The agent becomes useless exactly when the work gets interesting, and what happens next is predictable: the person turns the switch off, or turns it all the way up. Both are worse than where you started.

The unit that autonomy attaches to is the capability, because that is the unit where the risk lives. An agent is not risky; issuing credit is.

It degrades with a reason, and that is not a nicety

{ kind: "needs_approval", because: "over_cap", detail: "250.00 is over the ceiling of 100.00" }

"It did not run" leaves the owner choosing between three different fixes — raise the ceiling, change the switch, check the permission — and no way to tell which. because is the difference between a system people configure and one they give up on.

Three reasons, and they are genuinely different situations:

because What happened What the owner does
policy The switch is not on automatic Change the switch, if they want to
no_cap Guarded, and nothing bounds it Configure a ceiling
over_cap Bounded, and this one is over Approve it, or raise the ceiling

The switch is a request, not a permission

run(issueCredit, { autonomy: "auto", cap: null }, { amount: "1.00" })
// → needs_approval, because: "no_cap"

A dollar would have been fine. It still does not run, because nothing bounds it — and an unbounded "automatic" is not automation, it is an unguarded write with a friendlier name. The switch says what the owner wants; whether it is safe is a separate question, and both have to answer yes.

What the code deliberately does not know

run() has no idea what the capabilities do. It does not know one reads and one spends. It asks the same questions of both, and the answers differ because the policy differs, not because the code branches on the name. That is the property that lets a fifth capability arrive without touching the engine.

Where this comes from

Extracted from niiko, where it runs in production over a table of per-capability policy. The policy engine itself is published as @vorluno/ratchet — it answers the two questions that matter and nothing else.

The next failure in this family is worth knowing before you build it: a cap in the wrong unit is not a weak cap, it is one you cannot compare — see caps-in-the-right-unit.

License

Apache-2.0 — see LICENSE. This is a demonstration, not a package. If you want it in production, copy it; that is what the licence is for.


Built by Vorluno — a software studio from Panamá.

// next threadClassify every column, or fail the build