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 DecisionGET /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
| Option | Type | Default | Description |
|---|---|---|---|
evalUrl | string | http://127.0.0.1:7071 | Eval-server URL |
subject | string | "" | End-user identity: "user:alice" |
mode | "enforce"|"observe" | "enforce" | Enforce blocks; observe logs only |
workspace | string | process.cwd() | Project path for policy + IAM lookup |
agent | string | "vercel-ai" | Agent label in telemetry |
eventType | string | "shell" | shell, network, file_write, file_read |
Event type mapping
The eventType option tells the policy engine which rule class to apply:
| Tool purpose | eventType 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:
| Language | HTTP client | Lines of adapter code |
|---|---|---|
| Node.js | openai npm + fetch | ~25 |
| Ruby | stdlib Net::HTTP | ~20 |
| Java 21 | java.net.http.HttpClient | ~25 |
| Rust | ureq (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
- Framework adapters overview — comparison table, hook points, IAM quick-start
- CLI reference — eval-server
- IAM — per-user permission profiles
- Warden policy engine — policy YAML, rule schema, observe vs enforce