Skip to main content
All notes

September 2026 · 4 min

I built an approval system nobody could approve anything with

I am building a local AI agent platform that runs unattended on a Mac mini. The design has one rule underneath everything else: the model is not the system administrator. A model can propose an action. Whether that action runs is decided by code the model cannot argue with.

So I built the gate. Four capability tiers. Reading a file is autonomous. Writing a draft gets logged. Sending an email, installing software, deleting things, those need a human to approve. And the approval is not a general permission, it is bound to a hash of the exact parameters, so approving "email Alice about the invoice" cannot be replayed to email someone else. Single use. Expires. Fails closed on anything unexpected.

I wrote the tests. They passed. I moved on.

What I had actually built

The next day I went to check whether a person could use it.

The grant and pending functions existed. They were Python methods, and nothing surfaced them. No command, no interface, no entry point. To approve a single email you would have to open a Python shell, import the policy engine, query SQLite by hand for the approval ID, and call the method yourself.

I had built a gate with no handle on it. Every task that needed approval would arrive, park, and sit there forever, because the mechanism for unparking it existed only in a form no human would ever reach for.

It was worse than that

While writing the issue, I found the second problem. When a task needed approval, the supervisor parked it by calling cancel. That removed it from the run loop, which is what I wanted.

Cancelled is terminal. It has no outgoing transitions, by design, because a cancelled task is finished.

So granting an approval updated a row in a table and changed nothing else. The task was already dead, and the approval said granted. The system would have reported success while doing nothing at all.

I reused cancel because it did the thing I wanted. It also did a thing I did not check for.

The fix, and the two bugs it surfaced

Parking is its own state now, and it leads back to the queue. Granting an approval returns the task to be run; denying it cancels with a reason.

Then I built the interface. The command that matters turned out not to be approve, it is show:

$ lab show 925cc5fe

Task       send invoice to client
Tier       approve

This approval authorises EXACTLY these parameters:
{
  "amount": 4200,
  "api_key": "<redacted>",
  "to": "alice@example.com"
}

Changing any parameter invalidates this approval.

That is the entire point of binding an approval to a hash of its parameters. If the person approving never sees the parameters, the binding buys nothing and approving is rubber-stamping with extra steps.

Running it found two more things the tests had not. Secrets were printing to the terminal, so anything whose key looks like a credential is masked now; approving an action should not become a way to read a secret off the screen. And every request showed an age of twelve hours when it was seconds old, because timestamps are stored in UTC and I was comparing them against local time. I am in New Zealand, which is UTC+12.

What I took from it

The unit tests were not wrong. They tested that the gate blocks things, and it does. They could not have caught any of this, because none of it is about whether the logic is correct. It is about whether the thing is usable by the person it was built for.

I now ask that as a separate question from whether the tests pass: who operates this, and can they?

A control nobody can operate does not fail loudly. It sits there looking implemented.