Skip to main content
Attesta integrates with the Vercel AI SDK (ai package) through two mechanisms:
  1. gatedVercelTool — wraps a single tool definition with Attesta approval before execute runs.
  2. createAttestaMiddleware — returns middleware that intercepts all tool calls in generateText / streamText pipelines.
Both are TypeScript-only and ship with @kyberon/attesta.

Installation

npm install @kyberon/attesta ai

Tool Wrapper: gatedVercelTool

Wraps a Vercel AI SDK tool() definition. The wrapped tool runs the full Attesta evaluation pipeline before delegating to the original execute function.

API

gatedVercelTool<T extends VercelAIToolLike>(
  name: string,
  tool: T,
  options?: GatedVercelToolOptions
): T
ParameterTypeDescription
namestringAction name for the ActionContext
toolTA Vercel AI SDK tool definition
optionsGatedVercelToolOptionsConfiguration options (see below)
GatedVercelToolOptions:
OptionTypeDefaultDescription
agentIdstringAgent ID attached to every action context
sessionIdstringSession ID for tracking
environmentstring"development"Environment name
metadataRecord<string, unknown>{}Extra metadata
riskHintsRecord<string, unknown>Risk hints for the scorer
riskOverridestringForce a specific risk level
onDenied(result) => unknownthrows AttestaDeniedCustom denial handler

Full Example

import { tool, generateText } from "ai";
import { z } from "zod";
import { openai } from "@ai-sdk/openai";
import { gatedVercelTool } from "@kyberon/attesta/integrations";

// Define a tool
const deleteFileTool = tool({
  description: "Delete a file from the filesystem",
  parameters: z.object({
    path: z.string().describe("Absolute file path to delete"),
  }),
  execute: async ({ path }) => {
    // This only runs if Attesta approves
    await fs.unlink(path);
    return `Deleted ${path}`;
  },
});

// Wrap with Attesta
const safeDeleteTool = gatedVercelTool("deleteFile", deleteFileTool, {
  agentId: "file-manager",
  environment: "production",
  riskHints: { destructive: true },
});

// Use in generateText
const result = await generateText({
  model: openai("gpt-4o"),
  tools: { deleteFile: safeDeleteTool },
  prompt: "Delete the old log file at /var/log/app.log",
});

Behavior on Denial

By default, gatedVercelTool throws an AttestaDenied error when a tool call is denied:
AttestaDenied: Action "deleteFile" denied by attesta. Risk: critical (0.92).
You can customize this with the onDenied callback:
const safeDeleteTool = gatedVercelTool("deleteFile", deleteFileTool, {
  onDenied: (result) => {
    return {
      error: `Denied (risk: ${result.riskAssessment.level})`,
      suggestion: "Try a less destructive approach",
    };
  },
});
gatedVercelTool creates a shallow copy of the original tool with the wrapped execute function. The original tool object is not mutated. If the tool has no execute function, the original tool is returned unchanged.

Middleware: createAttestaMiddleware

Returns an object with experimental_onToolCall that can be spread into generateText or streamText options. This intercepts all tool calls in the pipeline, not just individually wrapped tools.

API

createAttestaMiddleware(options?: AttestaMiddlewareOptions): {
  experimental_onToolCall: (params) => Promise<void>;
}
AttestaMiddlewareOptions:
OptionTypeDefaultDescription
agentIdstringAgent ID
sessionIdstringSession ID
environmentstring"development"Environment name
metadataRecord<string, unknown>{}Extra metadata (includes vercelToolName automatically)
riskHintsRecord<string, unknown>Risk hints
getActionName(toolName, args) => stringidentityCustom function to derive the action name from the tool name

Full Example

import { generateText, tool } from "ai";
import { z } from "zod";
import { openai } from "@ai-sdk/openai";
import { createAttestaMiddleware } from "@kyberon/attesta/integrations";

// Create middleware
const attesta = createAttestaMiddleware({
  agentId: "data-pipeline",
  environment: "production",
  riskHints: { pii: true },
});

// Define tools (not individually wrapped)
const tools = {
  queryDatabase: tool({
    description: "Run a SQL query",
    parameters: z.object({ sql: z.string() }),
    execute: async ({ sql }) => {
      return await db.query(sql);
    },
  }),
  exportData: tool({
    description: "Export data to CSV",
    parameters: z.object({ table: z.string(), path: z.string() }),
    execute: async ({ table, path }) => {
      return await exportToCsv(table, path);
    },
  }),
};

// Use middleware -- it intercepts ALL tool calls
const result = await generateText({
  model: openai("gpt-4o"),
  tools,
  prompt: "Export the users table to /tmp/users.csv",
  ...attesta,  // Spreads experimental_onToolCall
});

Behavior on Denial

The middleware throws AttestaDenied when a tool call is denied. Unlike gatedVercelTool, there is no onDenied callback — the error propagates to the caller:
try {
  const result = await generateText({
    model: openai("gpt-4o"),
    tools,
    prompt: "DROP TABLE users",
    ...attesta,
  });
} catch (error) {
  if (error instanceof AttestaDenied) {
    console.log("Tool call denied:", error.message);
    // "Tool call "queryDatabase" denied by attesta. Risk: critical (0.95)."
  }
}

Streaming with streamText

The middleware works with streamText as well:
import { streamText } from "ai";
import { createAttestaMiddleware } from "@kyberon/attesta/integrations";

const attesta = createAttestaMiddleware({
  agentId: "chat-agent",
  environment: "production",
});

const result = await streamText({
  model: openai("gpt-4o"),
  tools,
  prompt: "Analyze and clean the dataset",
  ...attesta,
});

for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}
When using streamText, the experimental_onToolCall hook fires before the tool executes. If the tool call is denied, the AttestaDenied error interrupts the stream. Make sure your error handling accounts for stream interruption.

Custom Action Names

Use getActionName to derive meaningful action names from tool calls:
const attesta = createAttestaMiddleware({
  getActionName: (toolName, args) => {
    // Prefix with the module for better audit trails
    if (toolName.startsWith("db_")) return `database.${toolName}`;
    if (toolName.startsWith("fs_")) return `filesystem.${toolName}`;
    return toolName;
  },
});
This is useful when tool names are generic (e.g., query, execute) but the risk depends on the context.

Tool Wrapper vs. Middleware

FeaturegatedVercelToolcreateAttestaMiddleware
ScopeSingle toolAll tools in the pipeline
Denial handlingonDenied callback or AttestaDeniedAttestaDenied only
Per-tool configYes (different options per tool)No (same options for all tools)
Tool mutationCreates shallow copyNo mutation
Best forFine-grained per-tool policiesBlanket approval across all tools
Do not combine gatedVercelTool and createAttestaMiddleware on the same tool. This would evaluate the tool call twice — once by the wrapper and once by the middleware. Use one or the other.

LangChain (TS)

TypeScript LangChain integration

Integrations Overview

Compare all framework integrations