What actually stops an AI agent from running a bad DELETE?
Not the prompt. A request to delete every inactive customer walks through ownership, permission, schema and policy checks — then waits for a person to approve exactly those records.
Someone types: "Delete every customer inactive since 2023." The model, being helpful, plans exactly that. It finds 32 matching customers and proposes a call to a delete tool.
The honest answer to "what stops it?" is not the system prompt. Instructions shape what a model proposes. They are not a control, and a hostile note in your own data can argue with them. What stops a bad delete is a set of checks that run outside the model, every time, in the same order.
The trace, step by step
The proposal enters the execution trace with the raw arguments the model asked for. Then:
- Ownership. The tool exists, this agent has it, and the connection behind it belongs to this workspace.
- Schema. The arguments are validated against the tool's own schema. Fields it does not declare are dropped before the connector sees them, and the trace records that they were asked for.
- Permission. The agent acts for the person asking. A delete changes data, so it needs
tool.execute, which starts at the manager role. A member asking for the same thing is refused here, and the refusal names the missing permission. - Policy. A delete is rated critical, and critical calls need approval by default. That floor cannot be lowered: a delete tool cannot be marked as a read, whatever anybody clicks.
The run stops in an awaiting-approval state. Nothing has been deleted. A manager, admin or owner sees the tool, the connection, and the exact arguments, and decides.
Four guards worth naming
No raw SQL path. On a database connection the agent acts through generated tools — list, count, create, update and delete per table — not through SQL it writes itself. Ad-hoc SQL is a separate tool, off by default. When it is on, every statement, generated or not, passes the same check: one statement only, an allow-listed leading keyword, no schema changes anywhere, and values bound as parameters rather than pasted into the text.
No blanket DELETE. An UPDATE or DELETE with no WHERE clause is refused outright, with or without approval. The generated delete and update tools insist on at least one filter naming a real column, and a single write whose filter matches more rows than the connection's row limit is refused before anything changes.
Bulk means approval. Scale is invisible to a per-tool risk rating. "Delete customer 123" and "delete every inactive customer" are the same tool at very different sizes, so a change whose arguments say it will touch more than 10 records — a list of more than 10 ids, or a limit above 10 — waits, even when the tool on its own would not. A change called with no filter at all is treated as touching everything, and waits too.
The approval is bound to the arguments. Approving stores a SHA-256 fingerprint of the exact arguments the reviewer was shown. It is recomputed when the call runs. If the model re-plans and the arguments change, the fingerprint no longer matches and the call stops again. Where the arguments name 32 customers, the decision covers those 32 and no others.
What the reviewer is protected from
An approval also expires — after 24 hours by default — because a plan goes stale, and approving a day-old deletion is not the decision you think you are making. Rejecting one tells the agent why, so it can explain rather than pretend it succeeded.
And if the table has a deleted_at or similar column, the connector marks the rows rather than removing them, and tells the agent which kind of delete it is about to do before it does it.
None of this depends on the model behaving. That is the point: the model is an untrusted planner, and a planner is allowed to be wrong.
0 comments
Sign in to join the discussion.