SafetyCard Request a review

Vercel AI SDK UI

Framework-agnostic chat and agent UI layer of the Vercel AI SDK: useChat/useCompletion/useObject hooks, the UI message stream protocol, client-side tools and tool-approval flows.

github.com/vercel/ai › packages/react · 2026-10-04 · 08d7f0a

Defense-in-depth score

3.6 / 10

AI SDK UI ships a careful tool-approval design, with exact-input approval prompts, schema re-validation and optional HMAC-signed approvals, but approval is off by default: in the documented chat route every tool the model calls runs immediately with the server's full credentials. The server also trusts the message history the browser posts, so without the experimental approval secret a modified client can replay fabricated tool calls and approvals to run any registered tool. Prompt injection through tool results is the dominant risk; deployers should enable toolApproval and experimental_toolApprovalSecret, authenticate the route, forward req.signal, and cap automatic resubmissions.

Safest setup

AI SDK UI ships a careful approval flow, schema checks on every tool call, generic error messages to the browser and one model step per request, and it rejects system messages posted by the browser. Approval, signing, logging and limits are all off in the documented route, so the safest setup turns them on and scopes what the server can reach.

SetupHowWhat it gives you
Scoped, low-limit keysGive the server only the provider key and tool credentials this app needs, each scoped with a spending limit at the providerWhatever a tool call does, it reaches only the systems and spend you chose C1
An authenticated chat routePut your own authentication in front of the chat route and tie stored chats to the signed-in userOnly your users can reach the tools, and each sees only their own history C1
Per-call approvaltoolApproval on streamText, requiring approval for every tool with side effectsA person sees the exact tool input and decides before each consequential call runs C2
Signed approvalsexperimental_toolApprovalSecret: <secret> on streamText, from your secret storeOnly approvals your server issued are accepted when the browser sends them back C2
Only the tools neededRegister only the tools the app uses and narrow each step with activeToolsThe model can call only the tools the current task needs C3
Server-side stop and timeoutPass abortSignal: req.signal and timeout: <ms> to streamTextStopping in the browser also ends the model call on the server, and long requests end on their own C10
A capped resubmit loopKeep the default stopWhen and have sendAutomaticallyWhen return false after <n> roundsEach request runs one step and the browser-driven loop ends after a fixed number of rounds C10
A tool-call recordregisterTelemetry(<integration>) with an integration that writes to storage the app can't modifyA durable record of every server tool execution, with its arguments and result C9

Approving with confidence

  • Read each prompt and approve calls one at a time; the prompt shows the exact tool input.
  • Client-side tools run in the browser through your own callbacks, so add your own confirmation to any that act.

Handling data

  • A telemetry integration receives tool arguments and results; store its output like the data your tools read.
  • A custom onError replaces the default generic error message, so keep it from returning raw error text.

Key gaps (2)

  1. Server tools run with the server's ambient credentials for any caller, and a client-fabricated tool call plus approval in the posted history executes any registered tool unless the opt-in approval secret is set. C1 · Identity & least privilege
  2. Approval is off by default, so content injected through a tool result can drive any registered tool, including exfiltration and irreversible actions, with no human involved. C5 · Untrusted input blast radius

Criteria

C1 Identity & least privilege

0.00 / 1.00

AI SDK UI has no notion of who is asking. The documented chat route reads whatever message history the browser posts and runs the developer's server-side tools with the server process's own credentials (provider API keys from environment variables, plus whatever the tools hold), with no per-request authorization hook in the UI layer. Because the client supplies the whole history, a caller can also fabricate an assistant tool call with a matching approval and have the server execute any registered tool with inputs of its choosing, unless the developer opts into approval signing. Every tool therefore runs with the server's full authority on behalf of any caller who can reach the route.

C2 Approval gates

0.38 / 1.00

A real per-call approval flow exists: the server can mark tools as needing user approval (statically, per tool, or by a function of the input), the browser receives the exact tool input in an approval-requested state, and denials are first-class. The server re-validates approved inputs against the tool schema and can verify an HMAC signature that binds the approval to the exact call. But approval is off by default: unless the developer configures it, every tool the model calls executes immediately. Signature checking is a separate experimental opt-in; without it, approvals are just fields in the client-posted history and a modified client can fabricate them. Client-side tools run in the browser through developer callbacks with no SDK gate.

C3 Tool & action scoping

0.53 / 1.00

Every tool call the model makes is parsed against the tool's declared input schema before execution, unknown tool names are rejected, and approvals replayed from client history are re-validated against the schema. That is typed validation, not allowlisting: the framework has no path, URL or quantity checks of its own, and what a tool can do is whatever the developer's execute function does. The developer chooses the tool set and can narrow it per step with activeTools.

C4 Code-execution isolation

N/A · full credit 1.00 / 1.00

Nothing in the scoped code interprets model output as code: the hooks, the UI message stream and streamText contain no shell, eval or interpreter. streamText accepts an experimental sandbox handle but only passes it through to developer tools; sandbox packages are separate and outside this scope. Developers who add code-running tools get no isolation from AI SDK UI.

C5 Untrusted input blast radius

0.00 / 1.00

Tool results, MCP tool output and any fetched content enter the model's context as ordinary tool messages with nothing marking them as untrusted, and nothing in the framework changes what the model may do after reading them. With approval off by default, an injected instruction can drive any registered tool, including ones that send data out. The one structural help is that client-posted system-role messages are rejected by default. The MCP App bridge can forward app-originated chat messages to the host if the developer wires that handler.

C6 Memory, context & configuration integrity

0.25 / 1.00

The UI layer keeps no memory store of its own, but its core loop re-injects the whole client-held conversation, including past tool results, into the model every turn, and the documented persistence pattern saves that history server-side for later sessions. The server trusts that history: assistant turns, tool results and approvals in it are taken as genuine unless the developer opts into approval signing or validateUIMessages. One real guard: system-role messages posted by the client are rejected by default. The persistence guide keys chats by a client-supplied id and explicitly leaves authorization out.

C7 Third-party extensions

0.35 / 1.00

The default chat setup loads no third-party code. @ai-sdk/react does export an experimental MCP App renderer that runs HTML supplied by an MCP server inside a double iframe; the inner frame has no same-origin access, tool calls from the app are denied unless the host allow-lists them, and device permissions are deny-by-default. The HTML is whatever the server serves at render time, with no pinning or integrity check, and a restrictive content security policy is only built when the server itself supplies CSP metadata. Enforcement of the inner sandbox depends on the developer-hosted proxy page.

C8 Secrets & sensitive-data protection

0.45 / 1.00

Provider API keys stay on the server and never go to the browser in the documented setup. Errors from local tools and the stream are masked to a generic message before reaching the client by default, telemetry sends nothing unless an integration is registered, and per-request context values are excluded from telemetry unless explicitly included. There is no redaction of what goes to the model provider, and the provider keys are long-lived environment variables.

C9 Audit & traceability

0.35 / 1.00

The framework emits structured lifecycle events, including the start and end of every tool execution, to telemetry integrations and callbacks, and the browser holds a structured record of each call in the message parts. None of it is kept by default: no integration is registered, so nothing is recorded server-side unless the developer adds one. Approvals and denials are not separate audit events, and client-side tool runs are only seen by the server as results in the next request.

C10 Limits & kill switch

0.30 / 1.00

Each server request runs one model step by default, which keeps a single request small. But the multi-step loop in AI SDK UI runs from the browser: with sendAutomaticallyWhen set, as the docs recommend for tools and approvals, the client resubmits after every completed tool round with no cap. Timeouts and token limits are opt-in. The stop() helper aborts the browser request, but the server keeps running the model call and any in-flight tool unless the developer forwards the request's abort signal, which the default route does not.