raxit
Frameworks

Vercel AI SDK

Governing Vercel AI SDK agents with the explicit governed() wrapper — a live permit / deny / modify / defer demo.

The Vercel AI SDK's tool({ execute }) shape makes the enforcement point obvious: wrap the execute body with raxit's governed(resource, gov, body) and every call goes through the engine before the body runs — permit runs it, deny returns a [raxit BLOCKED] refusal string, defer suspends for human approval, and modify runs the body on engine-redacted args. This page walks a real payments-bot (mock Stripe-style tools) through all four outcomes, backed by a demo that produced every transcript line below.

Install

npm install @raxitlabs/raxit

Run it yourself

The walkthrough is deliberately no-LLM: the tools' execute functions are called directly in a scripted sequence, so the decisions are deterministic and the run needs zero API keys. The governance path exercised is exactly the one a generateText/streamText loop would hit.

The demo, step by step

Each step pairs the real code with the verbatim output from the captured run:

Vercel AI SDK payments-bot — permit / deny / modify / defer, live

Every Vercel AI SDK tool body is wrapped with governed(resource, gov, body). The first argument is the policy resource the engine matches on — the body only runs when the engine permits.

tools.ts
charge_card: tool({
  description: "Charge a card on file. amount_cents is the amount in the minor unit …",
  inputSchema: z.object({
    customer_id: z.string(),
    amount_cents: z.number().int(),
    currency: z.string().default("usd"),
  }),
  execute: governed("stripe.charge", gov, ({ customer_id, amount_cents, currency }) =>
    `stripe.charge OK — created charge ch_stub for customer ${customer_id} …`, onDecision),
}),
create_payout: tool({
  description: "Initiate a payout from the platform balance to an external bank account.",
  inputSchema: z.object({ amount_cents: z.number().int(), destination: z.string() }),
  execute: governed("stripe.payouts", gov, ({ amount_cents, destination }) =>
    `stripe.payouts OK — payout of ${amount_cents} to ${destination} (stub — should never run)`,
    onDecision),
}),
terminal
$ make -C examples demo-vercel-ai-governed-ts
raxit payments-bot full demo · agent=payments-bot · daemon=http://127.0.0.1:8352

How it works

The agent's tools are ordinary Vercel AI SDK tool() definitions; only the execute line changes. The first argument to governed() is the policy resource the engine matches on:

tools.ts
import { tool } from "ai";
import { z } from "zod";
import { governed } from "raxit";

charge_card: tool({
  description:
    "Charge a card on file. amount_cents is the amount in the minor unit (4200 = $42.00 USD).",
  inputSchema: z.object({
    customer_id: z.string(),
    amount_cents: z.number().int(),
    currency: z.string().default("usd"),
  }),
  execute: governed("stripe.charge", gov, ({ customer_id, amount_cents, currency }) =>
    `stripe.charge OK — created charge ch_stub for customer ${customer_id} amount=${amount_cents} ${currency} (stub)`),
}),

(The repo example installs the local SDK under the package alias raxit, hence from "raxit". With the published package, import from "@raxitlabs/raxit" instead.)

The whole policy is one file. default: block means anything not matched by a rule is denied — that's what caught create_payout in the demo:

.raxit/security.yaml
apiVersion: raxit.security/v1
agent: payments-bot
default: block
rules:
  - effect: allow
    resource: search_docs
  - effect: allow
    resource: stripe.charge
    when: amount_cents < 50000
  - effect: review              # review == defer (needs human approval)
    resource: stripe.charge
    when: amount_cents >= 50000
  - effect: allow
    resource: stripe.customers.read
  - resource: send_email        # MODIFY: redact PII in the body before sending
    modify: { redact: [body] }
  - effect: review
    resource: stripe.refund
  - effect: block
    resource: stripe.payouts

Note the two stripe.charge rules: the same resource is allowed under $500 and deferred to a human at or above it — an arg-conditional split the demo exercises with a $10 charge (permit, ran) while the refund_charge review rule shows the defer side.

Why explicit governed() and not autopatch here

raxit's TypeScript autopatch does cover the Vercel AI SDK zero-code — it hooks the SDK's own registerTelemetry/executeTool hook, governing permit/deny/defer before the tool body runs (tested against the real, package-installed SDK). But that hook has documented limits, so this example uses the explicit wrapper instead:

Four documented bypasses of the telemetry hook

experimental_telemetry: { isEnabled: false } disables the dispatcher; a caller-supplied experimental_telemetry: { integrations: [...] } replaces the global registry; a direct tool.execute() call outside generateText/streamText/Agent never passes through the dispatcher; and providerExecuted tools run server-side with nothing local to intercept. MODIFY is also not supported there — the SDK captures input before the wrapper runs, so args can't be rewritten in place. governed() is unaffected by all of these and is the only way to get MODIFY on this framework.

Rule of thumb: autopatch is fine for permit/deny/defer visibility on a standard generateText loop; wrap with governed() when you need MODIFY, or when the agent code (or a prompt-injected model) could plausibly reach any of the four bypasses. See the coverage table on Frameworks for the full point-by-point honesty notes.

What got denied and why

Both refusals in the demo carry a named reason code, straight from the transcript:

  • deny.default_or_forbidcreate_payout hit the explicit block rule on stripe.payouts (and would have been denied by default: block anyway). The agent sees:

    [raxit BLOCKED] 'stripe.payouts' denied (reason: deny.default_or_forbid). The operation was
    not executed. Details: raxit explain 14befbda893c4ee197f732688952caed
  • defer.policyrefund_charge matched the review rule on stripe.refund. The demo runs with RAXIT_DEFER_TIMEOUT=0 so nobody is kept waiting in CI; unapproved resolves to a refusal, not an execution:

    [raxit PENDING] 'stripe.refund' needs human approval and was not approved (reason:
    defer.policy). The operation was not executed. Details: raxit explain 74df5ee1a60a400e850bc0c0f2d83355

In an interactive run the defer prints an approval token instead, and raxit approvals approve <token> from another terminal resumes the call as a permit — the same flow as the LangChain.js worked example. Either way the refusal is a string returned to the model, not an exception that kills the agent loop, so the model can read the reason and route around it.

Every raxit explain <call_id> above resolves against the run's audit file — the demo's finale verified all 6 Decision Provenance Records signed and chain-intact (ok=true signatures_verified=true signed_count=6 count=6).

On this page