I gave an AI assistant thirteen tools. It logs my consulting hours, creates tasks and completes them, creates customers and projects, reads my calendar for the day, books Teams meetings, and sends email out of my own mailbox.

Twelve of my years in software went into a bill review platform used by carriers across the country. I know what a system with write access does on a bad day. When I get to the part about email, I can watch it land on the other person’s face — the eyebrows go up, and the next question is usually some version of “wait, it sends email?”

The thirteen tools are not the interesting part. Almost everything written about AI agents stops before the part that matters.

Capability lists and boundary lists

Every agent write-up is a capability list. It can do this, it can also do that, look how much it can do. A capability list is a description of what somebody hoped would happen.

The boundary list is the design. Mine is short, and most of it exists in code rather than in prose — I’ll be specific about the part that doesn’t. The alternative is a paragraph of instructions. A paragraph of instructions usually works.

A rule the model can drift past becomes a code path it cannot.

Confirmation is uneven on purpose

Logging hours just happens. Creating a task just happens. Creating a customer, creating a project, booking a meeting, sending email — every one of those is supposed to stop and wait for me to say yes.

That asymmetry was decided one tool at a time, on one question: does this action leave the building?

A logged hour is a row in my own records that I can correct in ten seconds. A meeting invite is on somebody else’s calendar. An email is gone — no undo, no draft folder, no version I get to see first. So the tools that touch only my own data run unattended, and the tools that touch another human are supposed to ask first.

Supposed to, because this one is not code, and I want to be exact about that. The asymmetry lives in the system prompt and in each tool’s own description, which makes it the weakest boundary I have. It is also why the confirm-gated list stays short. Make that list long enough and I start approving by reflex, and a stop I approve by reflex is decoration.

A meeting outside my hours comes back blocked

The meeting tool refuses to book anything outside 9 to 4 Pacific, Monday through Friday. Ask it for a Saturday call and it doesn’t book the meeting and apologize afterward. It comes back blocked, and the model has to tell me it couldn’t.

There is one way past it: a second call carrying an explicit override flag. That’s a deliberate speed bump rather than a signature — the code makes the exception loud and separate, and the part where I actually say yes is still prompt-level.

Not a policy. Not a line in the system prompt. A return value.

The prompt does also mention my working hours. That part is a courtesy, not a control. Two other limits are code rather than courtesy: the tool loop stops after eight rounds, and the model only ever sees my last twenty turns. Neither is a judgment it gets to make about itself.

It acts as me, not as itself

Calendar and mail run on a token issued on my behalf. Every action happens as me, the cloud audit log records a person rather than a piece of software, and the assistant cannot reach anyone else’s mailbox — not because it was told not to, but because the only credential it holds is mine.

Giving the application its own identity would have been easier. It also would have been wrong. An application with mail permission has mail permission everywhere. That shortcut saves an afternoon of token plumbing — call it a few hundred dollars of engineering time — and in exchange it hands a language model a key to every mailbox in the company, payroll and legal and mine, with an audit trail that says a service did it.

Grant, then revoke, in that order

The deployed API authenticates to its database as its own machine identity. Nothing in its configuration is a password, so there is nothing there to rotate and nothing to leak in a screenshot.

The old scoped database login stays alive until the identity path is proven in production. The revokes are already written, sitting in the script, waiting on that proof. Grant, then revoke, in that order. Doing it the other direction is how you find out at 9 p.m. that the new path had one permission missing.

The grant script for the new identity deliberately withholds five procedures the old login had. I searched the code, proved that nothing calls them, and left them off. Unused permissions are how a scoped login quietly stops being scoped — nobody ever widens it on purpose, it just never gets narrowed after the feature that needed it is gone.

It holds no permission on claim data at all

I run a company that does data integration and automation for workers’ comp and auto casualty medical bill review. The sensitive data in my world is claim data, and there is a lot of it within reach.

The assistant is designed so it never touches protected health information, and the enforcement is not a rule telling it to be careful. It holds no permission on any claims or bill data. Not read-only access, not filtered access, not a view. Nothing.

That boundary has a property none of the others have. There is no wording clever enough to talk it into a claim record — the claims database is not on the other end of any connection it has.

The first version of the loop was broken

The enforcement path is code, so it can be wrong. Mine was, and it took an hour of staring at a reply that read perfectly fine to see it.

The loop decided whether to keep going by comparing the model’s stop reason to a string, and the SDK renders that value differently than I assumed, so the comparison never matched. The assistant would announce what it was about to do, call a tool, and then hand me its own preamble as the final answer. Tool results never went back to the model. It looked like it was working, and it was answering from nothing.

The fix was to stop asking the response why it stopped and start asking what it did — keep going whenever the response contains tool calls. That is a better question in general, and it’s the one sentence I’d keep from the whole build.

I also got lucky in the direction it failed. A loop that misreads that same field the other way doesn’t go quiet. It spends its eight rounds calling tools nobody confirmed.

Where this can still go wrong

A review discipline, not a lock. The claim-data boundary is an absence, and an absence only holds while it stays absent. Somebody adds one convenient read from the wrong database, everything keeps working, and no test turns red. That is a weaker thing than a code path, and the only mechanism holding it is a habit of reading schema changes as security changes.

The confirmations are prose. They are the part of this design I trust least. Every other boundary here fails closed, and that one fails by being ignored — by the model on a strange day, or by me on a busy one.

One credential to lose instead of many. Acting as me is the right trade, and it concentrates the risk in one place. If my own account is compromised, the assistant’s boundary is whatever my session already allows, which is considerably more than the assistant needs.

Writing a better prompt is not on that list, and it never was.

The part nobody demos

The gap between what gets marketed about agents and what gets engineered into them is the widest I have seen in any category. The demo is the capability list. The work is the boundary list, and the boundary list is where the responsibility actually sits.

Most agent projects are still demos. You know which of your systems would be genuinely more useful with an assistant sitting in front of it. You know which one you would never hand a write. You know, within a client or two, which mistake would cost you the relationship — and roughly what that relationship bills you in a year.

The question about an agent was never what it can do.

It’s what happens the day it’s wrong.


Datos builds agents into the systems a company already runs, with a human holding the decisions that matter — see how that work is scoped or start the conversation.