Vercel AI SDK with Explicit Tool Boundaries

The risky part of an AI feature is not the chat UI. The risky part is what the chat is allowed to do.

It is easy to make an assistant feel powerful by giving it tools. With something like the Vercel AI SDK, it can read data, prepare changes, call actions, and explain the result in natural language. That can be useful. It can also become a hidden side door into important product behavior.

I prefer to treat AI tools as product boundaries. A tool should have a schema, a permission check, a clear result, and a decision about whether it is read-only or mutating.

const DraftChangeSchema = z.object({
  recordId: z.string(),
  summary: z.string(),
  proposedValue: z.string(),
});

This kind of schema is not only a TypeScript convenience. It describes what the assistant is allowed to ask for. It also limits what the server should accept.

For important changes, I like a confirmation step. The assistant can draft the change and explain the reason. The user reviews a summary and decides whether to apply it. Until confirmation, nothing important changes.

Assistant action: prepare change
User sees: summary, affected record, proposed value
User chooses: confirm or cancel
Server records: who confirmed, when, and what changed

That flow is slower than direct execution. That is the point. If the action affects durable data, permissions, billing, documents, or workflow state, a little friction can be useful.

Read-only tools are a good first step. They let the assistant answer questions without changing anything. Even then, permissions still matter. The assistant should not read data the current user could not read through the normal product UI.

The confirmation copy matters too. “Apply” is not enough if the action is risky. The user should know what will happen. A good confirmation panel names the affected item, the proposed change, and the consequence in plain language.

Audit logs become more important when AI is involved. If a user confirms an AI-assisted change, the system should record that sequence. Not because AI is magic, but because the path is less direct than a normal form submit. Future debugging needs to know what the assistant proposed and what the user approved.

The trade-off is product speed. Too many confirmations can make the feature annoying. Too few can make the system unsafe. I would rather start with stricter boundaries and relax them only for actions that are low-risk, reversible, and well understood.

I would not start an AI feature by giving it every tool the app has. I would start with one narrow task:

  • read the records the user can already see
  • draft a change using a strict schema
  • show the user what will happen
  • require confirmation before mutation
  • log the confirmed result

The open question is where the boundary should move over time. Some actions may become safe enough for direct execution. Others should always require review. The answer is not only technical. It depends on user trust, reversibility, and how expensive a mistake would be.

Related Posts

Astro for Documentation and a Professional Site

I use Astro because this site is mostly writing. I do not need a heavy app framework for pages that should load fast and be easy to edit. That sounds simple, but it is the mai

read more

Localization in Product Apps

Localization is not only replacing English strings with another language. In a product app, language touches workflow. It changes labels, validation messages, dates, empty states, permissions copy, d

read more

MCP as a Safe AI Integration Boundary

MCP is interesting because it makes AI integrations feel less like prompt magic and more like software boundaries. That is the part I care about. A model should no

read more

Zod, OpenAPI, and Swagger for API Contracts

A public API is not just backend code. It is a product surface for another developer. That means the contract has to be readable. It also has to be enforced at runtime. Types in the app are useful, b

read more

pg-boss for Durable Background Jobs

The customer problem was not "we need a queue". The problem was that a slow operation made the user wait with no clear answer. That distinction matters. A queue is an implementation detail. The produ

read more

Pragmatic Drag and Drop for Real Ordering Tasks

Drag and drop is easy to add for a demo and harder to make reliable for real work. The product question is not "can the item move on screen?" The question is whether the user can safely change an ord

read more

Prisma and PostgreSQL as the Product Source of Truth

I do not think of PostgreSQL as only infrastructure. In a product app, it is where the product remembers what happened. That makes database design a product decision. I

read more

React Router for Full-Stack Product Workflows

A route is not only a URL. In a product app, a route often represents a task the user is trying to finish. That sounds obvious, but it changes how I design the code. A settings page that starts an im

read more

shadcn-Style UI as an Owned Product System

I like copied UI primitives because they make the component library feel like part of the app, not something the app is borrowing. That is the part of the shadcn/ui-style ap

read more

Dense Operational UI with Tables and Editors

Sometimes a simple form is the wrong UI. If the user needs to compare many values and make careful edits, a table can be kinder than a long page of inputs. Dense UI has a bad reputation when it is us

read more

Vertical Slice Architecture with Dependency-Cruiser

I like vertical slices because they make a feature easier to delete, move, or review. The folder structure is not the main value. The value is that the code for one workflow is not spread across ten u

read more

Testing Product Workflows with Vitest and Playwright

I do not want a test suite that only proves functions work. I want it to protect the workflows that would hurt if they broke. That does not mean every rule needs a browser test. Browser tests are val

read more

Zod Beyond Validation

Zod is usually introduced as a validation library. That is true, but the more useful idea is boundary definition. A TypeScript type only helps after data is already inside the pro

read more