You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Spring AI supports tool calling, allowing models to invoke arbitrary functions, services, and APIs via ToolCallbacks. While powerful, this introduces risk:
Some tools can modify or delete data.
Some tools might be expensive or rate-limited.
Not all tools should be accessible to all conversations or tenants.
Today, once the model decides to call a tool, Spring AI executes it without any pluggable approval step.
This proposal introduces a small, focused Tool Approval Strategy that lets application developers decide, at runtime, whether a given tool call should be executed, rejected, or handled in a custom way, without breaking existing behavior.
2. Goals and Non-Goals
2.1 Goals
Provide a simple, Spring-style hook to approve or reject tool calls before execution.
Allow per-tool overrides (e.g., some tools always require approval, some never do).
Preserve backwards compatibility: existing applications behave exactly as they do today by default.
Keep the v1 surface area as small as possible while being clearly useful.
2.2 Non-Goals (for v1)
The following ideas are explicitly out of scope for this initial version and can be addressed in follow-up work:
A ToolApprovalStrategy functional interface that is consulted before any tool is executed.
A ToolApprovalDecision value type describing the outcome (approved/rejected).
A ToolApprovalException for signaling hard failures in the approval layer.
A requiresApproval flag on tool metadata to allow per-tool configuration.
A small change to DefaultToolCallingManager to invoke the strategy and handle rejections.
Default behavior is preserved via an AlwaysApproveStrategy that simply approves every tool call. Applications can provide their own strategy as a Spring bean to override this behavior.
4. Public API (v1)
4.1 ToolApprovalStrategy
A functional interface that receives information about the tool call and returns a decision.
@FunctionalInterfacepublicinterfaceToolApprovalStrategy {
/** * Decide whether the given tool call should be approved or rejected. * * @param toolCallback the tool definition and metadata * @param arguments the raw JSON arguments for the tool call * @param toolContext optional context (conversation, user, etc.), may be null * @return a ToolApprovalDecision indicating approve or reject * @throws ToolApprovalException to signal an error evaluating approval */ToolApprovalDecisionapprove(ToolCallbacktoolCallback,
Stringarguments,
@NullableToolContexttoolContext)
throwsToolApprovalException;
// Convenience implementations for common casesstaticToolApprovalStrategyalwaysApprove() {
return (tool, args, ctx) -> ToolApprovalDecision.approve();
}
staticToolApprovalStrategyalwaysReject(Stringreason) {
return (tool, args, ctx) -> ToolApprovalDecision.reject(reason);
}
}
This exception is not thrown for “normal” rejections (those are represented by ToolApprovalDecision); it is used when something goes wrong evaluating the approval (e.g., policy engine unavailable).
4.4 ToolMetadata.requiresApproval
Tools can opt in or out of approval on a per-tool basis.
publicinterfaceToolMetadata {
// existing methods.../** * Whether this tool requires approval before execution. * * <ul> * <li>null – use the global ToolApprovalStrategy (default behavior)</li> * <li>true – always apply the approval strategy for this tool</li> * <li>false – never apply the approval strategy for this tool</li> * </ul> */@NullableBooleanrequiresApproval();
}
For example:
A harmless, read-only tool can set requiresApproval = false to bypass approval even if a global strategy is configured.
A sensitive tool can set requiresApproval = true to ensure it is always subject to the strategy.
5. Default Behavior and Backwards Compatibility
To avoid breaking existing applications:
If no ToolApprovalStrategy bean is registered, Spring AI configures an AlwaysApproveStrategy internally.
If requiresApproval() returns null and the default strategy is used, the behavior is identical to today: all tools are executed as requested by the model.
Rejections are only possible when:
The application supplies a custom ToolApprovalStrategy, and
Either the tool’s requiresApproval is true, or the strategy is applied globally (see below).
6. DefaultToolCallingManager Integration
DefaultToolCallingManager is extended to consult the approval strategy before invoking a tool.
High-level flow for a single tool call:
Parse tool call from model output.
Resolve the corresponding ToolCallback.
Check if approval is required for this tool.
If required:
Call toolApprovalStrategy.approve(...).
If decision is approved, proceed with tool execution.
If decision is rejected, return a structured rejection back into the model conversation instead of executing the tool.
If approval is not required:
Execute the tool as today.
Pseudo-code:
publicclassDefaultToolCallingManagerimplementsToolCallingManager {
privatefinalToolApprovalStrategytoolApprovalStrategy;
publicDefaultToolCallingManager(ToolApprovalStrategytoolApprovalStrategy) {
this.toolApprovalStrategy = toolApprovalStrategy;
}
@OverridepublicToolExecutionResultexecuteToolCall(ToolCallbacktoolCallback,
Stringarguments,
@NullableToolContexttoolContext) {
if (isApprovalRequired(toolCallback)) {
ToolApprovalDecisiondecision = toolApprovalStrategy.approve(
toolCallback, arguments, toolContext
);
if (!decision.approved()) {
// Do not execute the tool; propagate rejection as a tool responsereturnToolExecutionResult.rejected(
toolCallback,
decision.reason(),
decision.metadata()
);
}
}
// Existing behavior: execute toolreturnexecuteTool(toolCallback, arguments, toolContext);
}
privatebooleanisApprovalRequired(ToolCallbacktoolCallback) {
BooleanperTool = toolCallback.getToolMetadata().requiresApproval();
if (perTool != null) {
returnperTool;
}
// Fallback: by default, consult the strategy.// The default strategy simply approves everything.returntrue;
}
}
Notes:
ToolExecutionResult.rejected(...) is a placeholder for the existing response type used to represent tool outputs. In v1, rejections would be encoded as a tool response that the model can see (e.g., “Tool execution was denied: ”).
The strategy is always invoked when isApprovalRequired() returns true. With the default AlwaysApproveStrategy, this is O(1) and effectively a no-op.
7. Example Usage
7.1 Application configuration: simple blacklist
An application might define a simple blacklist strategy:
@ConfigurationpublicclassToolApprovalConfig {
@BeanpublicToolApprovalStrategytoolApprovalStrategy() {
Set<String> blockedTools = Set.of("deleteAccount", "dropDatabase");
return (tool, args, ctx) -> {
if (blockedTools.contains(tool.getName())) {
returnToolApprovalDecision.reject(
"Tool '" + tool.getName() + "' is not allowed in this environment.");
}
returnToolApprovalDecision.approve();
};
}
}
With no changes to the rest of the application, any attempt by the model to call deleteAccount or dropDatabase will be rejected before execution.
7.2 Per-tool override: safe tools
@Tool(
name = "getWeather",
description = "Get current weather for a given city"
)
publicclassWeatherTool {
@ToolMetadataImpl(
requiresApproval = false// read-only, no approval needed
)
publicStringgetWeather(@P("city") Stringcity) {
// ...
}
}
Even if the global ToolApprovalStrategy implements complex policies, this tool bypasses approval.
8. Future Extensions (Out of Scope for v1)
The v1 design intentionally limits scope to a small, composable core. Potential future enhancements include:
Composite strategies (e.g., combine multiple strategies with AND/OR logic).
Async / human-in-the-loop approval, where a strategy can pause execution until external approval is received.
Auditor interfaces for central logging of all tool decisions.
Rich metrics and tracing using Spring Observability, powered by ToolApprovalDecision.metadata.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
1. Motivation
Spring AI supports tool calling, allowing models to invoke arbitrary functions, services, and APIs via
ToolCallbacks. While powerful, this introduces risk:Today, once the model decides to call a tool, Spring AI executes it without any pluggable approval step.
This proposal introduces a small, focused Tool Approval Strategy that lets application developers decide, at runtime, whether a given tool call should be executed, rejected, or handled in a custom way, without breaking existing behavior.
2. Goals and Non-Goals
2.1 Goals
2.2 Non-Goals (for v1)
The following ideas are explicitly out of scope for this initial version and can be addressed in follow-up work:
3. High-Level Design
At a high level, the design adds:
ToolApprovalStrategyfunctional interface that is consulted before any tool is executed.ToolApprovalDecisionvalue type describing the outcome (approved/rejected).ToolApprovalExceptionfor signaling hard failures in the approval layer.requiresApprovalflag on tool metadata to allow per-tool configuration.DefaultToolCallingManagerto invoke the strategy and handle rejections.Default behavior is preserved via an
AlwaysApproveStrategythat simply approves every tool call. Applications can provide their own strategy as a Spring bean to override this behavior.4. Public API (v1)
4.1
ToolApprovalStrategyA functional interface that receives information about the tool call and returns a decision.
A simple default implementation:
Framework default:
AlwaysApproveStrategy.4.2
ToolApprovalDecisionA small value type that represents the outcome of a strategy decision.
reasonis intended primarily for logging and optional model/user-facing messages.metadatais included for future extensibility (e.g., attaching policy IDs, severity, tenant IDs). v1 does not prescribe a specific schema.4.3
ToolApprovalExceptionA dedicated exception type for approval-layer failures.
This exception is not thrown for “normal” rejections (those are represented by
ToolApprovalDecision); it is used when something goes wrong evaluating the approval (e.g., policy engine unavailable).4.4
ToolMetadata.requiresApprovalTools can opt in or out of approval on a per-tool basis.
For example:
requiresApproval = falseto bypass approval even if a global strategy is configured.requiresApproval = trueto ensure it is always subject to the strategy.5. Default Behavior and Backwards Compatibility
To avoid breaking existing applications:
ToolApprovalStrategybean is registered, Spring AI configures anAlwaysApproveStrategyinternally.requiresApproval()returnsnulland the default strategy is used, the behavior is identical to today: all tools are executed as requested by the model.ToolApprovalStrategy, andrequiresApprovalistrue, or the strategy is applied globally (see below).6.
DefaultToolCallingManagerIntegrationDefaultToolCallingManageris extended to consult the approval strategy before invoking a tool.High-level flow for a single tool call:
ToolCallback.toolApprovalStrategy.approve(...).Pseudo-code:
Notes:
ToolExecutionResult.rejected(...)is a placeholder for the existing response type used to represent tool outputs. In v1, rejections would be encoded as a tool response that the model can see (e.g., “Tool execution was denied: ”).isApprovalRequired()returnstrue. With the defaultAlwaysApproveStrategy, this is O(1) and effectively a no-op.7. Example Usage
7.1 Application configuration: simple blacklist
An application might define a simple blacklist strategy:
With no changes to the rest of the application, any attempt by the model to call
deleteAccountordropDatabasewill be rejected before execution.7.2 Per-tool override: safe tools
Even if the global
ToolApprovalStrategyimplements complex policies, this tool bypasses approval.8. Future Extensions (Out of Scope for v1)
The v1 design intentionally limits scope to a small, composable core. Potential future enhancements include:
ToolApprovalDecision.metadata.These can be added incrementally on top of the v1 foundation without breaking changes.
End of v1 doc
Beta Was this translation helpful? Give feedback.
All reactions