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