What are the key production and security design concerns when exposing tools to an LLM, and how do you address them in Spring AI?
answer
- tool args = untrusted, prompt-injection steerable
- identity via ToolContext + Spring Security authz inside tool
- validate/allow-list every arg
- human approval via internalToolExecutionEnabled(false)
- bound result size + loops; ToolExecutionExceptionProcessor; Micrometer audit
basics
~20 sTreat every tool as an attack surface: the model (steered by user input) chooses which tools to call and with what arguments. Validate arguments, enforce authorization inside the tool (via ToolContext identity, not model-supplied ids), gate destructive actions with human approval, limit result size, and add observability.
solid answer
~50 sThe model's tool arguments are effectively untrusted input shaped by user prompts, so prompt injection can steer tool calls. Design defensively: (1) pass identity/authorization data through ToolContext, never as model-chosen @ToolParams, and enforce authz inside the tool with your normal Spring Security checks; (2) validate and constrain every argument (types, ranges, allow-lists) before acting; (3) make destructive/irreversible tools require human approval by disabling internal tool execution (internalToolExecutionEnabled=false) so a person confirms before you run and resubmit; (4) keep tools least-privileged and idempotent where possible; (5) bound cost/latency — trim large tool results that re-enter the prompt, and cap tool-call loops; (6) handle tool exceptions gracefully via a ToolExecutionExceptionProcessor so failures become model-readable messages, not stack traces; (7) add observability (Micrometer tracing/metrics Spring AI emits) to audit which tools ran with what inputs. Net: the tool boundary is a security boundary, not a convenience.
code
java · 28 linesclass AdminTools {
private final RefundService refunds;
AdminTools(RefundService refunds) { this.refunds = refunds; }
// Identity from ToolContext (not the model); authz enforced inside
@Tool(description = "Issue a refund for an order (requires approval)")
RefundResult refund(
@ToolParam(description = "Order ID") String orderId,
@ToolParam(description = "Amount in cents") long amountCents,
ToolContext ctx) {
String actor = (String) ctx.getContext().get("userId");
if (!refunds.canRefund(actor, orderId)) {
throw new AccessDeniedException("Not authorized to refund " + orderId);
}
if (amountCents <= 0 || amountCents > refunds.maxRefund(orderId)) {
throw new IllegalArgumentException("Refund amount out of range");
}
return refunds.issue(orderId, amountCents); // reached only after validation + authz
}
}
// Destructive tool => user-controlled execution so a human approves first
ChatOptions opts = ToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new AdminTools(refunds)))
.internalToolExecutionEnabled(false)
.build();go deeper
Aware that tool inputs shouldn't be blindly trusted.
Validates arguments and keeps identity out of model-chosen params.
Enforces authz inside tools, gates destructive actions, and handles errors/cost deliberately.
Treats the tool layer as a trust boundary end-to-end: injection-aware design, least privilege, approval workflows, exception mapping, and full observability/audit.
## The core threat model When you expose a tool, you let a model — whose behavior is influenced by **untrusted user input** — decide *which* method to invoke and *what arguments* to pass. Combined with **prompt injection** (a user or retrieved document instructing the model to misbehave), this means: **tool arguments are untrusted, and tool selection can be adversarially steered.** Every tool must be designed as if a malicious actor is choosing its inputs. ## Concern 1 — Authorization and identity - **Never** let the model supply identity/authorization inputs (user id, tenant, role) as `@ToolParam` — it can fabricate or swap them. Inject them via **`ToolContext`** from the authenticated request. - Enforce authorization **inside** the tool using your existing Spring Security context (e.g. method security / `@PreAuthorize`, or explicit checks against the `ToolContext` principal). The model requesting a tool is not authorization to perform it. - Apply least privilege: a tool should touch only what that user may access. ## Concern 2 — Input validation Validate every argument the model provides: types, ranges, formats, and **allow-lists** for things like table names, file paths, or shell/SQL fragments (avoid passing model text into SQL/OS commands at all — classic injection). Reject rather than coerce ambiguous inputs. ## Concern 3 — Destructive actions & human-in-the-loop For irreversible or high-impact tools (refunds, deletes, sends), disable internal execution (`ToolCallingChatOptions.internalToolExecutionEnabled(false)`) so the tool-call request is surfaced for **human approval** before you execute and resubmit. Prefer idempotent operations and confirmation tokens. ## Concern 4 — Cost, latency, and loops - Tool results **re-enter the prompt** as tokens; large payloads inflate cost and can blow the context window. Return only what's needed. - The model can loop (call after call). Cap iterations / set timeouts to avoid runaway cost. - Consider `returnDirect=true` when the tool output is the answer, to skip an extra model round-trip. ## Concern 5 — Error handling Don't leak stack traces to the model. Spring AI's `ToolExecutionExceptionProcessor` lets you convert exceptions into controlled, model-readable messages (so the model can recover or apologize) while you log the real error server-side. Decide per tool whether a failure should be retried, surfaced, or aborted. ## Concern 6 — Observability & auditing Treat tool calls as auditable events. Spring AI integrates with **Micrometer** for tracing/metrics; capture which tool ran, with what (sanitized) inputs, latency, and outcome. This is essential for debugging model behavior and for security forensics. ## Concern 7 — Statelessness & concurrency Tools may be invoked concurrently and repeatedly; keep them thread-safe and avoid hidden shared mutable state. Per-request context belongs in `ToolContext`, not instance fields. ## Putting it together A well-designed tool layer: identity via `ToolContext` + Spring Security authz inside the tool; strict argument validation; approval gating for destructive ops; bounded results and loop counts; graceful exception mapping; and full observability. The recurring principle: **the tool boundary is a trust boundary** between an untrusted, injectable model and your real systems — engineer it with the same rigor as a public API endpoint. ## Gotchas - Assuming 'the model wouldn't do that' — prompt injection routinely makes it do exactly that. - Passing model text into SQL/OS/HTTP without validation → injection. - Trusting model-supplied ids for authorization. - Letting huge tool outputs silently balloon token cost. - No audit trail, so misbehavior is undiagnosable.
- Why can't you trust the authorization to the model deciding to call a tool?The model's decision is shaped by user input and can be manipulated via prompt injection; it isn't an authenticated authorization decision. You must re-check permissions inside the tool against the real authenticated principal (from ToolContext / Spring Security), exactly as you would for any API endpoint.
- How do you stop a large tool result from blowing up cost or the context window?Return only the fields the model needs (project/trim/paginate the payload), summarize before returning, or use returnDirect=true when the raw output is the final answer so it never re-enters the prompt. Also cap the number of tool-call iterations.
- How should tool exceptions be handled so they don't break the conversation?Use a ToolExecutionExceptionProcessor to convert exceptions into controlled, model-readable messages so the model can recover, while logging the real error server-side — never leak stack traces or internals into the prompt.
saying these in an interview costs you the question
- 'The model won't pass malicious arguments' — ignores prompt injection
- Using model-supplied user ids for authorization
- Interpolating model text straight into SQL/OS commands
- No approval gate on destructive tools
- No auditing/observability of tool calls