← All articles Engineering

The tool list is the boundary

28 August 2026 · 6 min read

Ask an agent to review your infrastructure without changing anything, and there are two ways to get it. Tell the model not to write. Or hand it a tool list with no write tools in it.

Only one survives a model that misreads the instruction.

OBTO's planning modes take the second route for the four tools that edit app artifacts. In plan or explain mode, those four are filtered out of the catalog before the model is given it:

const PLAN_WRITE_TOOLS = new Set([ "obto_upsert_record", "obto_patch_artifact", "obto_create_route", "obto_update_route", ]); tools: readOnlyMode ? this.tools.filter((t) => t && !PLAN_WRITE_TOOLS.has(t.name)) : this.tools,

The model is never offered obto_upsert_record. It cannot decide to call it, because from inside that session the tool does not exist. No instruction to forget, no prompt to talk around.

Note the size of that set, because it is the honest boundary. Four tools are removed. Other mutating tools stay in the catalog, governed by a line in the prompt telling the model not to modify anything. For those, plan mode is an instruction. Naming which is which is the point of what follows, and we do not exempt ourselves from it.

The distinction is worth naming, because "the agent can't do that" gets said about four different things, and a buyer is entitled to know which one is on offer.

Four grades of enforcement

GradeWhat it means
RemovedThe capability is absent from what the caller is offered.
RefusedThe server rejects the call unconditionally. The write is not applied.
CheckedA precondition runs when the caller supplies it. Omit it and the write proceeds.
AdvisedDocumented somewhere. No server-side consequence.

The word "guardrail" covers all four without distinguishing them. The gap between the second and third is where the surprises live.

Removed

Plan mode is the clearest case, for the four tools it covers. A second sits in the dynamic tool loader, where a tool record that is not active is never registered into the session. It does not become a tool that returns an error when called. It never enters the catalog.

Removal is the only grade that does not depend on the model behaving. Everything below it does, at least a little. If you are deciding how much of your platform an agent may reach, the size and shape of the catalog you hand it is the strongest control you have.

Refused

Below removal sits the server saying no. Rejecting a malformed payload is table stakes. The refusals worth examining fire when the platform's own check cannot run.

Creating a route on OBTO looks up whether that route already exists. If it does, the create is refused. If the lookup itself errors, the create is refused too, with read_failed, on the principle that a tool which cannot see the target has no business writing to it. Schedule job creation does the same under collision_check_failed.

A check that could not run is treated as a check that failed.

That is the invariant, and it is worth more than any individual guard. Tenant reset carries it furthest, across two layers. A hardcoded set of platform and reserved slugs is refused unconditionally, with deliberately no override parameter. A second list, held as a configuration property, covers real production tenants. When that property cannot be read, the operation stops: that list is the only guard covering real tenants, so it refuses rather than proceed with the guard disabled. Nothing is read and nothing is written.

This part is structurally awkward for a pipeline, and not because pipelines are badly built. In a build-and-deploy model the precondition is evaluated in one process and the write happens later in another. "The check errored" and "the check passed" both arrive at the deploy step as "the job moved on", unless somebody wired the failure path by hand. When the check and the write are the same call, that path is the default rather than something you remembered to configure. We looked at the wider version of this in the questions a deploy pipeline answers for you.

Checked

obto_patch_artifact accepts two preconditions. anchorText carries the text you expect to find at the line you are editing. expectedContentHash carries a hash of the whole file as you last saw it. Supply either and a patch against changed content is refused before anything is written. That mechanism is real, and we have described how it works.

Both are optional. Omit them and the patch applies at the line numbers given. A third case is worth knowing: on a pure insertion, an anchor you did supply is accepted and then never compared. So the honest sentence is "a patch is refused when a supplied anchor does not match a replacement it applies to", which is narrower than "a patch cannot be applied to a file that changed underneath you". The first is a check the caller opts into. The second would be a guarantee, and we do not have it. An optional argument is not a gate.

The approval step in the hosted agent sits at the same grade, and its scope is narrower than "destructive tool calls" suggests. Six tools are covered: four always intercepted with approve or reject, plus artifact patches and overwrites of an existing record, which also offer edit. It is on by default outside read-only mode. It is scoped to that agent, so an external client on the same MCP endpoint does not pass through it, and the lookup that classifies an overwrite fails open if it throws. Useful, on by default, not a property of the server.

Advised

The bottom grade is documentation. A tool description saying "never auto-activate" is a promise about the calling convention. Only the handler is a promise about behaviour. The two can drift, and when they do, the description is the one that reads well in an evaluation.

So do not ask what the docs say a tool does. Ask what the code does when you pass the argument the docs told you not to pass.

Three things you can measure

What this does not buy you

Scoping a catalog bounds what an agent can reach. It says nothing about whether what it writes is right. Hash and anchor matching catch corrupted bytes and stale edits, not a well formed change that is simply wrong. An agent with exactly the correct tools can still be confidently incorrect with every one of them, which is why knowing the blast radius up front matters as much as the guards during the run, and why reading back what was stored is not optional.

Tests, review, and a person who cares are still yours. What a scoped catalog does is make the set of things you have to reason about smaller and knowable in advance. That is a narrower claim than "safe", and a more testable one.

If you are evaluating platforms in this space, the four grades are a decent worksheet. Sort each safety claim into a column, then ask the vendor to point at the line of code that puts it there.

Frequently asked questions

How do you stop an AI agent from calling a tool?

Take the tool out of the catalog the model is given, rather than instructing it not to use that tool. An instruction is text the model weighs against everything else in its context. A filtered tool list is a smaller set of things the model can name at all. On OBTO, a session in plan or explain mode has four artifact-write tools filtered out of the array, so the model is never offered those four. Any mutating tool still in the list is governed by the prompt, which is a weaker thing, and worth counting for yourself.

What is the difference between a refused write and a checked write?

A refusal is unconditional: the server rejects the call and the write is not applied, whatever the caller passed. A check is a precondition the caller opts into, such as sending a content hash with a patch. If the caller omits it, the check does not run and the write proceeds. Both are useful. Only one of them is a guarantee, and the two are easy to describe in the same words.

What should a platform do when a safety check cannot run?

Treat a check that could not run as a check that failed. If a collision lookup errors, the safe response is to refuse the create rather than proceed without knowing. OBTO returns read_failed on route creation and collision_check_failed on schedule job creation for exactly this case. Whichever way a platform goes here, it will not show up in a normal evaluation, because the path only fires when something else is already wrong. It is worth asking about directly.

How can I tell whether a vendor's safety claim is enforced?

Read the handler, not the tool description. A description is a promise about the calling convention and carries no server-side consequence. Three concrete tests: ask for the tool list in read-only mode and count it; make the precondition store unreachable and see whether the write still lands; send a parameter the documentation says is ignored and see whether it is ignored or honoured.

Does removing write tools make an agent's output correct?

No. Scoping a tool catalog bounds what an agent can touch. It says nothing about whether what it writes is right. Structural checks such as hash matching and anchor matching catch corrupted bytes and stale edits, not a well formed change that is simply wrong. Tests, review, and a person who cares remain your responsibility. Our guide to agent guardrails covers the layers around this one.

See the whole tool list

OBTO gives agents a scoped MCP surface over your apps, data, and deployments, with the handler behaviour visible to you.

Get started free