Tool Policy¶
A ToolPolicy is an allow/deny gate evaluated before an agent invokes a tool. It is the first concrete piece of the governed execution model: a small, dedicated SPI that you can ship without buying into a larger advisor framework.
When to use it¶
- Hard limits: forbid an agent from ever calling
shell.executeorfilesystem.write. - Argument-aware rules: refuse
payment.transferwhen theamountexceeds a threshold. - Per-agent capability scope: the researcher may call
web.search, the writer may not.
ToolPolicy is intentionally narrow. It does not modify arguments, sandbox the execution, or rewrite the result.
The SPI¶
@FunctionalInterface
public interface ToolPolicy {
Decision check(String toolName, Map<String, Object> arguments);
// factories: allowList(...), denyList(...), when(...), ALLOW_ALL, DENY_ALL
// composition: policy.and(other)
}
Decision carries allowed plus a denial reason. When denied, the wrapping ToolCallback throws ToolPolicyViolation, which Spring AI surfaces as a failed tool call — the model sees the error and can react (retry, ask for help, skip).
Attaching a policy to an executor¶
ToolPolicy policy = ToolPolicy.allowList("web.search", "retrieval.search")
.and(ToolPolicy.denyList("shell.execute"));
ExecutorAgent researcher = ExecutorAgent.builder()
.name("researcher")
.chatClient(chatClient)
.tools(webSearchTool, retrievalTool, shellTool)
.toolPolicy(policy)
.build();
If the model tries to call shell.execute, the call is short-circuited before it reaches the underlying ToolCallback. The audit record (via RecordingToolCallback) captures the denied attempt with the reason.
Composing rules¶
ToolPolicy allowed = ToolPolicy.allowList("web.search", "payment.transfer");
ToolPolicy noBigTransfers = ToolPolicy.when(
(name, args) -> !("payment.transfer".equals(name)
&& args.get("amount") instanceof Number n
&& n.doubleValue() > 1000.0),
"amount above 1000 requires approval");
ExecutorAgent agent = ExecutorAgent.builder()
// ...
.toolPolicy(allowed.and(noBigTransfers))
.build();
.and(other) returns a new policy that requires both checks to pass. The first denial short-circuits — useful when one of the rules is expensive.
Default behaviour¶
The default policy on an ExecutorAgent.Builder is ToolPolicy.ALLOW_ALL. No wrapping happens, so there is zero overhead when a policy is not configured.
Relationship to other governance pieces¶
ToolPolicy is the first piece of a wider governance story. Companion pieces planned:
StatePolicy— control read / write access perStateKey<T>.ApprovalGate— pause the workflow on flagged operations using the existingInterruptRequest+ checkpoint flow.
Each policy ships independently, so adopters can pick exactly the level of governance they need.