Articles · Artificial intelligence · · 7 min read
Blast Radius Is a Tool-Boundary Problem
An agent's blast radius is set by its tool contracts, not its prompt. How to scope authority, gate writes, and keep the boundary testable rather than hopeful.
By Matthieu de La Mettrie · Founder, Skopura
You have decided to build the agent. The interesting question is no longer whether a model can be trusted to choose actions — it is what the worst reachable outcome looks like when it chooses badly, and whether that outcome is one you can live with at three in the morning without a human on call.
That worst reachable outcome is the blast radius. It is a property of the tool surface, not of the model, and not of the prompt. A prompt is a request. A tool contract is a grant of authority. Anything you can express only in the prompt is a preference; anything you express in the contract is a constraint. Designing an agentic system is mostly the work of moving statements from the first category to the second.
What actually defines the blast radius?
Enumerate every state change reachable from a single turn, then close the set transitively. If send_email can reach an inbox that triggers an automation that writes to a CRM, the CRM is inside the radius. If run_sql accepts arbitrary text against a connection with write privileges, the radius is the connection’s grant, not the query the model happened to emit. If a tool returns identifiers the agent can then pass to a second tool, the composition is part of the surface even though no single contract permits it.
This is why tool inventories written as lists mislead. The surface is a graph. Two individually harmless tools — one that resolves a customer name to an internal ID, one that issues a credit against an ID — compose into an unbounded credit facility.
The classification matters more than the count. A system with many read tools and one irreversible write is easier to reason about than a system with three tools where one quietly spans all four tiers.
Coarse tools or fine tools?
The instinct is to give the agent primitives and let it compose. Composition is exactly what you cannot bound. The alternative is to expose task-shaped tools whose arguments are narrow and whose effects are whole: not update_row, but apply_approved_discount(order_id, tier).
| Fine-grained primitives | Task-shaped tools | |
|---|---|---|
| Reachable state changes | Determined by composition, hard to enumerate | Determined by the contract, enumerable |
| Flexibility for unforeseen requests | High — the agent improvises | Low — unforeseen work falls back to a human |
| Where correctness lives | In the model's plan | In code you can test |
| Failure signature | Novel each time | Repeats, so it can be fixed once |
| Audit trail | Sequence of low-level effects | One business-meaningful event |
Fine-grained primitives are appropriate on read paths, where the cost of a wrong composition is a wasted call and a confused answer. On write paths, prefer the tool that encodes a decision your business already knows how to make. If you cannot name the tool in the language your operations team uses, it is probably too primitive to hand to an agent.
Where should the authority live?
Not in the agent process. Put a broker between the model’s chosen call and the effect handler, and give the broker the only credentials that matter. The agent proposes; the broker decides. This is unglamorous plumbing and it is the single structural change that most reduces blast radius, because it makes every constraint a property of code rather than of instruction-following.
Two details in that flow do disproportionate work. The first is resolving caller identity at the broker rather than trusting an identifier in the tool arguments — the agent should never be able to name whose data it is acting on. Row-level security filters a model’s data by who is asking, and that principle has to extend past the warehouse into the tool layer, or the agent becomes a confused deputy with a service account. The second is the typed error path: a rejection that returns structured feedback lets the agent adapt within the boundary instead of retrying blindly against it.
Which writes need a human gate?
Gates are expensive in latency and in goodwill, so spend them where reversal is hard. Work through each write tool once:
Gate it, and log the full proposed payload for review
The last row is the goal state for most write tools, and it is reached by narrowing contracts, not by trusting the model more.
What does a contained contract look like?
The shape to aim for: a schema that admits only well-formed intent, an idempotency key supplied by the broker, an explicit dry-run mode, and a result type that distinguishes refused from failed. Structured outputs constrain a model’s response to a supplied schema, which gets you well-formed arguments; the contract still has to decide what well-formed arguments are permitted to do.
const ApplyCredit = {
name: "apply_credit",
// The model names an order, never an account or an amount source.
input: z.object({
orderId: OrderId,
reasonCode: z.enum(["late_delivery", "damaged", "duplicate_charge"]),
note: z.string().max(280),
dryRun: z.boolean().default(true),
}),
// Refusals are data, not exceptions, so the agent can replan.
output: z.discriminatedUnion("status", [
z.object({ status: z.literal("applied"), creditId: z.string() }),
z.object({ status: z.literal("simulated"), wouldCredit: Money }),
z.object({ status: z.literal("refused"), reason: RefusalReason }),
]),
};
// The broker, not the agent, supplies identity, idempotency and ceiling.
async function invoke(call: ToolCall, ctx: BrokerContext) {
const args = ApplyCredit.input.parse(call.args);
const order = await orders.get(args.orderId, { asCaller: ctx.principal });
if (!order) return refuse("out_of_scope");
const amount = creditPolicy.resolve(order, args.reasonCode); // policy, not prompt
if (amount.exceedsCeiling(ctx.ceilingFor(ctx.principal))) return refuse("needs_approval");
if (args.dryRun) return { status: "simulated", wouldCredit: amount };
return effects.apply("credit", { order, amount }, { key: ctx.idempotencyKey });
}Notice what the model cannot express: an amount, an account, a caller. It selects a reason code from a closed set, and policy code turns that into money. The credit ceiling is a property of the principal held by the broker.
How do you make the boundary testable?
Stop testing the model and start testing the contract. Model-level evaluation tells you how often the agent picks a sensible plan; boundary testing tells you what happens when it does not. Both are needed, and they are different harnesses.
For the boundary, write adversarial cases that bypass the model entirely: call each tool directly with arguments the model could plausibly emit under prompt injection or a garbled plan. Hostile identifiers from another tenant. Reason codes that do not match the order state. The same call replayed with the same idempotency key. Two tools composed in the order your design did not anticipate. These tests are deterministic, fast, and they are the ones that hold when the model underneath is swapped.
Then instrument the effect log as a first-class dataset rather than application logging. Every proposal, every refusal, every applied effect, with the principal and the idempotency key. That log is what lets you answer, after an incident, which calls were reachable and which were merely attempted — and it is the input to the quieter, more useful metric: the ratio of refusals to applications per tool, watched over time. A tool whose refusal rate climbs is a contract drifting out of alignment with the work people are actually asking for. A tool that never refuses anything is probably not constraining anything either.
What is the failure mode of doing this well?
Over-narrowed contracts. If every tool is task-shaped and gated, the agent becomes a slow interface to a form, and you have paid for non-determinism without buying flexibility. The judgement call is which parts of the workload genuinely benefit from a planner and which are better expressed as a deterministic path with a model in one step. Build-versus-buy turns on whether the capability is a differentiator or a commodity; agent-versus-workflow turns on whether the variance in the task is real or imagined.
The discipline worth keeping is the sequencing. Decide the reachable set first, then the contracts that bound it, then the gates, then the prompt. Prompts written against a well-bounded surface are short and boring, which is the correct aesthetic for production. Prompts carrying the weight of safety are long, defensive, and one clever input away from being irrelevant.
agentic-ai · system-design · tool-boundaries · platform-security
Share on LinkedIn ↗Work with Skopura
Facing this bottleneck in your own estate?
Skopura builds production-grade lakehouse pipelines, evaluation harnesses, and provides fractional data leadership. Thirty-minute call, no deck.