Note
Eight tools, zero write access: approval gates for AI agents as code
The useful question is not whether an agent gets it wrong, but what it can do when it does. How a tool contract, proposal-only writes and a version check limit that – and where the boundary stops helping.
- Published
- Christopher Böbel, Fullstack AI Engineer
- Christopher Böbel, Fullstack AI Engineer
- 5 min read
The most common question about AI agents is the wrong one. It goes: how do I stop the model from saying something false? The question that decides whether a system can run in production is a different one: what can the model actually do when it says something false?
The difference matters, because the first question has no dependable answer. A language model will occasionally be wrong; that is a property, not a configuration setting. The second question, by contrast, is purely a matter of construction. It can be answered in code, tested, and read back.
The short answer: do not secure the model, secure the tools. In Chelaro, my local finance workspace, the AI holds exactly eight typed tools. Four of them read. The other four write nothing; they create a proposal awaiting review. No tool can overwrite a confirmed figure, regardless of what the model currently believes to be correct.

The tool contract is code, not a prompt
An agent can only do what its tools allow. That sounds obvious and is rarely taken seriously: in many setups the model gets generic database or shell access and the limits live in the system prompt. The security boundary is then a piece of text, interpreted by the very model it was meant to constrain.
In Chelaro the boundary sits one layer lower. There is a fixed list of eight names:
| Reading | Proposing |
|---|---|
| Monthly finance overview | Create a receivable |
| List transactions | Update a receivable |
| List receivables | Record a payment |
| Read a single receivable | Reverse a payment |
Each tool has a strict JSON schema with additionalProperties: false, validated before execution. Amounts must match a money pattern with exactly two decimal places, IDs a UUID pattern, periods the format YYYY-MM. Arguments are capped at 16 KiB. Lists return at most 50 entries.
Beyond that the model gets nothing: no shell, no files, no processes, no network, no browser, no coding tools. It cannot do more damage than these eight signatures allow, because there is nothing else it can reach.
Writing means proposing
The four writing tools are called propose for a reason. They do not produce a canonical record but a proposal with its own status. Only when the owner accepts it does a confirmed figure exist.
Two details decide whether this is more than a confirmation dialog:
First, the rationale is mandatory. Every proposing tool requires a rationale field, in the schema rather than as a request in the prompt. A proposal without one is never created. That sounds cosmetic but it changes the review: whoever confirms does not just read an amount, they read the claim about why the amount is right. Wrong proposals surface earlier than they would behind a bare number.
Second, accepted and rejected proposals are logged alike. A rejected proposal does not disappear. It stays in the audit ledger. That makes it visible afterwards not only what happened, but what nearly happened.
The real gate is the version conflict
The most interesting part is the least conspicuous. Every modifying tool must supply the expected version of the record it touches. If it no longer matches, the proposal is rejected with a conflict rather than applied.
The reason is a timing problem that shows up more often with agents than with people: between reading and writing, a model usually runs a whole chain of intermediate steps. The record can change in that window, for instance because the owner corrected it. Without a version check the agent overwrites a fresher correction with a stale one, and nobody notices, because both values look plausible.
With a version check that becomes a visible failure instead of silent data loss. It is the most useful property of the whole construction and also the one most likely to be left out while building.
I built the same structure independently in a fullstack AI CRM for the surface coating industry, currently in pilot: five authenticated read tools, eleven action types requiring confirmation, each with a draft, a preview and a confirmation of limited validity, and likewise a conflict on a stale state. That the same pattern emerges in two very different domains is, to me, the best sign that it is not about the domain.
What this does not solve
The boundary limits damage; it does not guarantee quality. It says nothing about whether the proposals are any good. An agent that reliably produces nonsensical but formally valid proposals is not hindered by any of this; it merely costs review time instead of data.
It also shifts work onto a person. A review step invoked on every single proposal will eventually be clicked through. If the hit rate is not high enough, an approval gate is not a safety gain but a fatigue machine. Whether the proposals are good enough has to be measured separately, and that needs evals against reference cases rather than architectural arguments.
Finally: Chelaro is a public source preview under a source-available licence, not production-ready and without releases. OCR, transaction import, live banking, and backup and restore are all missing. What is described here is the access boundary and its test coverage, not a product proven in use.
The tool contract, the schema validation and the version check can all be read in the repository. That is the point: a security boundary you cannot read is not one.
Read the case studyChelaro