Vercel AI SDK runtime authorization
Wrap Vercel AI SDK streaming tool calls with Veto. Each governed streaming tool call is evaluated before dispatch: allow, review, or deny, with an exportable decision record per governed decision.
What is Vercel AI SDK runtime authorization?
Vercel AI SDK runtime authorization intercepts tool calls made by agents built with the AI SDK. When an agent calls generateText or streamText with tools, each tool invocation is evaluated against your policies before execution. Allowed calls proceed. Denied calls return an error the agent can reason about. Sensitive operations get routed to human approval.
The problem: agents that can do anything
AI SDK 6 introduced first-class agent support with multi-step tool calling, streaming responses, and human review via needsApproval. But needsApproval is a per-tool boolean. It does not express "allow deletes in /tmp but block deletes in /etc" or "require approval for emails to external domains." Real enforcement requires policy logic, not flags.
An agent hallucinating a wrong answer is annoying. An agent hallucinating a wrong tool call can delete your production database, send emails to customers, or deploy untested code. The risk scales with the number of tools you expose.
Filesystem access
Agent asked to "clean up logs" decides to delete config files, environment variables, or SSH keys in the process.
Database mutations
Agent running analytics queries decides a TRUNCATE or DROP would be a faster way to "reset" a table.
External communication
Agent with email tools sends messages to external addresses, leaking internal information or triggering compliance violations.
Infrastructure changes
Agent with deploy tools pushes unreviewed code to production or scales infrastructure beyond budget limits.
First governed call
Install the SDK, wrap your tool executions with veto.guard(), and define policies in YAML. Wrap one tool, then expand policy by risk.
1. Install
npm install veto-sdk ai @ai-sdk/openai zod
2. Wrap your tools with a guard helper
import { generateText, tool } from "ai"
import { openai } from "@ai-sdk/openai"
import { z } from "zod"
import { Veto } from "veto-sdk"
const veto = await Veto.init({ apiKey: process.env.VETO_API_KEY })
function guardedTool<T extends z.ZodType>(opts: {
description: string
parameters: T
toolName: string
execute: (args: z.infer<T>) => Promise<unknown>
}) {
return tool({
description: opts.description,
parameters: opts.parameters,
execute: async (args) => {
const decision = await veto.guard({
tool: opts.toolName,
arguments: args,
})
if (decision.decision === 'deny') {
return { error: `Blocked: ${decision.reason}` }
}
if (decision.decision === 'require_approval') {
return { pending: true, approvalId: decision.approvalId }
}
return opts.execute(args)
},
})
}
const deleteFile = guardedTool({
toolName: "delete_file",
description: "Delete a file from the filesystem",
parameters: z.object({
path: z.string().describe("File path to delete"),
}),
execute: async ({ path }) => {
await fs.unlink(path)
return { deleted: path }
},
})
const queryDatabase = guardedTool({
toolName: "query_database",
description: "Run a SQL query",
parameters: z.object({
query: z.string().describe("SQL query to execute"),
}),
execute: async ({ query }) => {
const rows = await db.query(query)
return { rows, count: rows.length }
},
})
const result = await generateText({
model: openai(process.env.OPENAI_MODEL!),
tools: { delete_file: deleteFile, query_database: queryDatabase },
maxSteps: 10,
prompt: "Clean up stale user sessions older than 30 days",
})3. Define policies
version: "1.0"
name: Vercel AI SDK agent policies
rules:
- id: block-system-file-deletion
tools: [delete_file]
action: deny
conditions:
- field: arguments.path
operator: matches
value: "^/(etc|usr|bin|sys|proc)/.*"
reason: "System directory deletion is never allowed"
- id: approve-production-deploys
tools: [deploy]
action: require_approval
conditions:
- field: context.environment
operator: equals
value: "production"
approval:
timeout_minutes: 15
notify: [ops-team@approved.example]
- id: limit-email-recipients
tools: [send_email]
action: deny
conditions:
- field: arguments.to
operator: not_matches
value: "^.+@approved\.example\.com$"
reason: "Agents can only email internal addresses"
- id: block-destructive-queries
tools: [query_database]
action: deny
conditions:
- field: arguments.query
operator: matches
value: "^(DROP|TRUNCATE|DELETE FROM)\\s"
reason: "Destructive SQL operations blocked"Before and after
Your agent code stays the same. Enforcement wraps the tool execution, not the agent logic.
import { generateText, tool } from "ai"
import { openai } from "@ai-sdk/openai"
import { z } from "zod"
const result = await generateText({
model: openai(process.env.OPENAI_MODEL!),
tools: {
delete_file: tool({
description: "Delete a file from the filesystem",
parameters: z.object({
path: z.string().describe("File path to delete"),
}),
execute: async ({ path }) => {
await fs.unlink(path)
return { deleted: path }
},
}),
send_email: tool({
description: "Send an email",
parameters: z.object({
to: z.string(),
subject: z.string(),
body: z.string(),
}),
execute: async ({ to, subject, body }) => {
await mailer.send({ to, subject, body })
return { sent: true }
},
}),
},
maxSteps: 10,
prompt: "Delete old logs and email the team a summary",
})import { generateText, tool } from "ai"
import { openai } from "@ai-sdk/openai"
import { z } from "zod"
import { Veto } from "veto-sdk"
const veto = await Veto.init({
apiKey: process.env.VETO_API_KEY,
projectId: "proj_abc123",
})
const result = await generateText({
model: openai(process.env.OPENAI_MODEL!),
tools: {
delete_file: tool({
description: "Delete a file from the filesystem",
parameters: z.object({
path: z.string().describe("File path to delete"),
}),
execute: async ({ path }) => {
const decision = await veto.guard({
tool: "delete_file",
arguments: { path },
context: { user: currentUser.id },
})
if (decision.decision === 'deny') {
return { error: decision.reason }
}
if (decision.decision === 'require_approval') {
return {
status: "pending_approval",
approvalId: decision.approvalId,
}
}
await fs.unlink(path)
return { deleted: path }
},
}),
send_email: tool({
description: "Send an email",
parameters: z.object({
to: z.string(),
subject: z.string(),
body: z.string(),
}),
execute: async ({ to, subject, body }) => {
const decision = await veto.guard({
tool: "send_email",
arguments: { to, subject, body },
context: { user: currentUser.id },
})
if (decision.decision === 'deny') {
return { error: decision.reason }
}
await mailer.send({ to, subject, body })
return { sent: true }
},
}),
},
maxSteps: 10,
prompt: "Delete old logs and email the team a summary",
})Streaming guardrails
When agents stream responses with streamText, tool calls happen mid-stream. Veto evaluates each call in-process before dispatch, so streaming stays responsive. Denied tool calls return error messages that the agent can reason about and adapt to in real time.
import { streamText, tool } from "ai"
import { openai } from "@ai-sdk/openai"
import { Veto } from "veto-sdk"
const veto = await Veto.init({ apiKey: process.env.VETO_API_KEY })
const result = streamText({
model: openai(process.env.OPENAI_MODEL!),
tools: {
deploy: tool({
description: "Deploy to production",
parameters: z.object({
service: z.string(),
version: z.string(),
}),
execute: async ({ service, version }) => {
const decision = await veto.guard({
tool: "deploy",
arguments: { service, version },
context: {
environment: "production",
user: currentUser.id,
role: currentUser.role,
},
})
if (decision.decision === 'deny') {
return { error: decision.reason }
}
if (decision.decision === 'require_approval') {
return {
status: "awaiting_approval",
approvalId: decision.approvalId,
message: "Production deploy requires team lead approval",
}
}
await deployService(service, version)
return { deployed: true, service, version }
},
}),
},
maxSteps: 5,
prompt: "Deploy the billing service v2.3.1 to production",
})
for await (const chunk of result.textStream) {
process.stdout.write(chunk)
}How it works with AI SDK 6
AI SDK 6 introduced needsApproval for basic human review control. Veto complements this with fine-grained policy evaluation.
| Capability | AI SDK needsApproval | Veto |
|---|---|---|
| Per-tool approval flag | ||
| Argument-level conditions | ||
| User/role-based policies | ||
| Approval routing (configured channels) | ||
| YAML policy-as-code | ||
| Decision records | ||
| Rate limiting per tool | ||
| Review queue + records |
Guardrail patterns
In-process evaluation
Policy evaluation runs in your Node.js process. Local policies stay on the in-process decision path. Runs in-process before dispatch keeps streaming agents responsive.
Context-aware rules
Policies can reference user identity, role, environment, time of day, and session state for dynamic policy decisions.
Graceful denials
Denied tool calls return structured error responses. The agent receives the denial reason and can retry with different arguments or inform the user.
Multi-step action control
With maxSteps, agents chain multiple tool calls. Each step is allowed independently, preventing escalation across a multi-step workflow.
Frequently asked questions
How is this different from AI SDK's built-in needsApproval?
Does enforcement slow down streaming responses?
Does this work with useChat and useCompletion hooks?
What happens when an agent's tool call is denied mid-stream?
Can I use Veto with AI SDK's agent abstraction?
Related integrations
Run AI SDK agents that respect boundaries.