Vercel AI SDK — Prismor Warden adapter

The Vercel AI SDK adapter (prismor-warden) is a TypeScript package that intercepts every tool execute() call before it runs. It is the reference implementation of the HTTP adapter pattern: a thin language-native client calls the Warden eval-server (a sidecar Python process) to evaluate each tool call against policy, returning a JSON decision in milliseconds.

How it works

The Python runtime stays canonical. The TypeScript adapter is ~80 lines of HTTP client code with no Prismor Python dependency.

Prerequisites

Start the eval-server once alongside your app (Python, from the prismor repo or any machine with prismor installed):

python3 -m prismor.runtime.eval_server --port 7071 --workspace .

The server exposes:

  • POST /v1/evaluate — accepts a tool-call JSON body, returns a Decision
  • GET /health — liveness probe

Install

npm install prismor-warden

Quick start

import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
import { wardenTools } from "prismor-warden";

const run_shell = tool({
  description: "Execute a shell command",
  parameters: z.object({ command: z.string() }),
  execute: async ({ command }) => { /* your implementation */ },
});

const fetch_url = tool({
  description: "Fetch a URL",
  parameters: z.object({ url: z.string() }),
  execute: async ({ url }) => { /* your implementation */ },
});

// Wrap all tools — every execute() is now policy-checked before it runs
const tools = wardenTools(
  { run_shell, fetch_url },
  { subject: `user:${userId}` }
);

const result = await generateText({ model: openai("gpt-4o-mini"), tools, prompt });

Blocking dangerous calls

A denied call in enforce mode throws WardenBlocked:

import { WardenBlocked, wardenTools } from "prismor-warden";

// In a Next.js API route:
export async function POST(req: Request) {
  const { prompt, userId } = await req.json();
  const tools = wardenTools(myTools, { subject: `user:${userId}` });

  try {
    const result = await generateText({ model, tools, prompt });
    return Response.json({ text: result.text });
  } catch (e) {
    if (e instanceof WardenBlocked) {
      return Response.json({ error: e.message }, { status: 403 });
    }
    throw e;
  }
}

Per-user (multi-tenant)

Pass subject per request. The subject is forwarded to the eval-server where it is resolved to per-user IAM policies and attributed in telemetry:

// Each incoming request gets its own subject — no shared state
const tools = wardenTools(myTools, {
  subject: `user:${session.userId}`,
  mode: "enforce",
  workspace: "/path/to/project",
});

Subjects are "user:<id>" or the structured form "user=alice;team=data". Users without an explicit IAM profile fall through to org-wide defaults.

Options

OptionTypeDefaultDescription
evalUrlstringhttp://127.0.0.1:7071Eval-server URL
subjectstring""End-user identity: "user:alice"
mode"enforce"|"observe""enforce"Enforce blocks; observe logs only
workspacestringprocess.cwd()Project path for policy + IAM lookup
agentstring"vercel-ai"Agent label in telemetry
eventTypestring"shell"shell, network, file_write, file_read

Event type mapping

The eventType option tells the policy engine which rule class to apply:

Tool purposeeventType to use
Shell/code execution"shell" (default)
HTTP fetch / URL access"network"
Writing files"file_write"
Reading files"file_read"

Set it per-tool when wrapping individually:

const tools = {
  ...wardenTool("run_shell", run_shell, { eventType: "shell" }),
  ...wardenTool("fetch_url", fetch_url, { eventType: "network" }),
};

Or set a default for all tools when using wardenTools:

const tools = wardenTools(myTools, { eventType: "network" });

Fail-open design

If the eval-server is unreachable (down, not yet started, crashed), the adapter warns and allows — it never breaks the agent because of infrastructure issues. Run python3 -m prismor.runtime.eval_server as a long-lived sidecar process or a Docker container to minimise downtime.

// Adapter behaviour when eval-server is down:
// console.warn("[prismor-warden] eval-server returned 503 — failing open")
// → tool.execute() is called normally

Modes

// Observe: log findings, never block (safe rollout)
const tools = wardenTools(myTools, { mode: "observe" });

// Enforce: block denied calls before execution (default)
const tools = wardenTools(myTools, { mode: "enforce" });

Start in observe to understand your agent's blast radius without disrupting users. Switch to enforce once confident. Policy is YAML — change it without redeploying TypeScript code.

Other languages

The eval-server speaks plain JSON over HTTP, so any language can act as an adapter. Validated on a Linux host with real OpenAI function calls:

LanguageHTTP clientLines of adapter code
Node.jsopenai npm + fetch~25
Rubystdlib Net::HTTP~20
Java 21java.net.http.HttpClient~25
Rustureq (sync)~25

See examples/multilang/ for the full runnable examples for each language.

Eval-server API reference

POST /v1/evaluate

Request body:

{
  "tool_name": "run_shell",
  "arguments": { "command": "rm -rf /" },
  "event_type": "shell",
  "agent": "vercel-ai",
  "mode": "enforce",
  "session_id": "optional-session-id",
  "subject": "user:alice",
  "workspace": "/path/to/project"
}

Subject can also be passed via X-Warden-Subject header (takes precedence over body field).

Response:

{
  "allow": false,
  "reason": "[CRITICAL] Blocks rm -rf /, mkfs, dd to disk, shutdown, reboot",
  "findings": [...],
  "blocking": { ... },
  "subject": { "user_id": "alice", "team_id": null }
}

allow: true → proceed; allow: false + enforce mode → block and surface reason to the user.

GET /health

{ "status": "ok", "ts": "2026-06-28T23:15:11.209223+00:00" }

See also