Runtime authorization for AI agents. The missing layer between identity and action.
Veto intercepts every tool call your AI agent makes, evaluates it against your policies, and decides: allow, deny, or escalate. Sub-10ms. Default deny.
- Default deny — no matching policy = blocked
- MCP native — drop-in middleware for Model Context Protocol servers
- Full audit trail — every decision logged
- Edge-first — powered by Cloudflare Workers
npm install @useveto/nodeimport { VetoClient } from "@useveto/node";
const veto = new VetoClient({ apiKey: process.env.VETO_API_KEY! });
// Check if an agent can perform an action
const result = await veto.authorize("support-bot", "send_email", {
to: "user@example.com",
subject: "Refund confirmation",
});
if (!result.allowed) {
console.log(`Blocked: ${result.reason}`);
}Wraps your tool handler — denied actions never execute.
import { VetoClient, createVetoGuard } from "@useveto/node";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const veto = new VetoClient({ apiKey: process.env.VETO_API_KEY! });
const protect = createVetoGuard(veto, { agentId: "support-bot" });
const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.tool(
"send-email",
{ to: z.string(), subject: z.string(), body: z.string() },
protect("send-email", async (params) => {
await sendEmail(params);
return { content: [{ type: "text", text: "Email sent!" }] };
}),
);When denied, returns { content: [{ type: 'text', text: 'Authorization denied: ...' }], isError: true }.
Call at the start of your handler — throws VetoError if denied.
import { VetoClient, vetoMiddleware } from "@useveto/node";
const veto = new VetoClient({ apiKey: process.env.VETO_API_KEY! });
const guard = vetoMiddleware(veto, { agentId: "support-bot" });
server.tool("send-email", schema, async (params) => {
await guard("send-email", params); // throws VetoError if denied
await sendEmail(params);
return { content: [{ type: "text", text: "Sent!" }] };
});const veto = new VetoClient({
apiKey: "veto_...", // required
endpoint: "https://api.veto.tools", // optional (default)
timeout: 5000, // optional, ms (default: 5000)
});veto.authorize(agentId, toolName, parameters?) → Promise<AuthorizationResult>veto.createAgent({ name, description? }) → Promise<Agent>
veto.listAgents() → Promise<Agent[]>
veto.getAgent(agentId) → Promise<Agent>
veto.deleteAgent(agentId) → Promise<void>veto.createPolicy({ agentId, name, rules, priority?, enabled? }) → Promise<Policy>
veto.listPolicies(agentId?) → Promise<Policy[]>
veto.getPolicy(policyId) → Promise<Policy>
veto.updatePolicy(policyId, { name?, rules?, priority?, enabled? }) → Promise<Policy>
veto.deletePolicy(policyId) → Promise<void>veto.queryAuditLog({ agentId?, action?, toolName?, result?, from?, to?, limit?, offset? }) → Promise<AuditLogEntry[]>import { VetoError, UnauthorizedError, RateLimitError } from "@useveto/node";
try {
await veto.authorize("agent", "tool");
} catch (error) {
if (error instanceof RateLimitError) {
console.log(`Retry after ${error.retryAfterMs}ms`);
} else if (error instanceof UnauthorizedError) {
console.log("Invalid API key");
} else if (error instanceof VetoError) {
console.log(`${error.code}: ${error.message}`);
}
}If Veto is unreachable (network error, timeout), the SDK denies by default. You can override this:
const protect = createVetoGuard(veto, {
agentId: "my-agent",
onError: "allow", // fail-open (not recommended for production)
});- Website: veto.tools
- Dashboard: app.veto.tools
- API Docs: docs.veto.tools
- GitHub: github.com/useveto/node
MIT