M

Mastra runtime authorization

Wrap Mastra agents, tools, and workflows with Veto. Each governed tool call is evaluated before dispatch: allow, review, or deny, with an exportable decision record per governed decision.

Why Mastra needs guardrails

Mastra is a TypeScript path from prompt to agent. Define a createTool with a Zod input schema, attach it to an Agent, and the model can call it. The schema validates argument shape. It does not validate intent. When a model decides to call delete_customer with cascade: true, the schema says "valid input" and Mastra calls execute().

Mastra's workflow engine has a larger action surface. Workflows chain steps with retries and conditional branches. A single prompt injection can travel through three steps before hitting a side effect that does not reverse. You need guardrails at each step boundary, not just at the workflow entry. See why prompt instructions are not guardrails.

Schema is not policy

Zod validates types and ranges. It cannot express "no cascade deletes in production" or "refunds over $500 need finance approval".

Workflows compound risk

Mastra workflows chain tool calls. Without per-step guardrails, a poisoned intermediate result drives a destructive final step.

No audit by default

Mastra's telemetry captures observability. It does not produce a decision record for SOC 2 or HIPAA review.

Before and after Veto

The left tab shows a standard Mastra agent. The model picks a tool and Mastra's executor runs the body unconditionally. The right tab adds Veto inside each tool's execute function. Same agent, same tools, each governed call evaluated against policy first.

ts
import { Mastra } from '@mastra/core'
import { Agent } from '@mastra/core/agent'
import { createTool } from '@mastra/core/tools'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'

const deleteCustomer = createTool({
  id: 'delete_customer',
  description: 'Delete a customer record from the database',
  inputSchema: z.object({
    customer_id: z.string(),
    cascade: z.boolean().optional(),
  }),
  execute: async ({ context }) => {
    return await db.customers.delete(context.customer_id, { cascade: context.cascade })
  },
})

const refundPayment = createTool({
  id: 'refund_payment',
  description: 'Refund a customer payment',
  inputSchema: z.object({
    payment_id: z.string(),
    amount_cents: z.number(),
  }),
  execute: async ({ context }) => {
    return await stripe.refunds.create({
      payment_intent: context.payment_id,
      amount: context.amount_cents,
    })
  },
})

const supportAgent = new Agent({
  name: 'support-agent',
  instructions: 'You handle customer support including refunds and account changes.',
  model: openai(process.env.OPENAI_MODEL!),
  tools: { deleteCustomer, refundPayment },
})

const mastra = new Mastra({ agents: { supportAgent } })

// The model picks deleteCustomer with cascade: true on the wrong record.
// Mastra calls execute(). The customer and their orders are gone.
const response = await mastra.getAgent('supportAgent').generate(userMessage)

Mastra workflow steps

Workflow steps are guarded the same way as agent tools. Call veto.guard() at the top of each step's execute body. A denial throws; Mastra's retry and branching behavior stays intact.

workflows/outreach.ts
import { Workflow, Step } from '@mastra/core/workflows'
import { Veto } from 'veto-sdk'
import { z } from 'zod'

const veto = await Veto.init({ apiKey: process.env.VETO_API_KEY })

const sendOutreach = new Step({
  id: 'send-outreach',
  inputSchema: z.object({
    recipient: z.string(),
    template: z.string(),
  }),
  execute: async ({ context }) => {
    const decision = await veto.guard('send_outreach', context.inputData, {
      user_role: context.user?.role,
      org_id: context.org?.id,
    })
    if (decision.decision !== 'allow') {
      throw new Error(`Outreach blocked: ${decision.reason}`)
    }
    return await mailer.send(context.inputData.recipient, context.inputData.template)
  },
})

const outreachWorkflow = new Workflow({
  name: 'outreach-workflow',
  triggerSchema: z.object({ leads: z.array(z.object({ email: z.string() })) }),
}).step(sendOutreach)

// Workflow steps are guarded individually: pause at the first denial.
outreachWorkflow.commit()

Policy configuration

Author guardrail rules in declarative YAML. Match on tool name, constrain arguments, set actions. See the runtime authorization glossary for terminology.

veto/policies.yaml
rules:
  - name: protect_production_deletes
    description: Block customer deletes outside dev
    tool: delete_customer
    when: context.environment != "development"
    action: deny
    message: "Customer deletes require dev environment or manual override"

  - name: prevent_cascade_deletes
    description: Cascade needs guardrails for agents
    tool: delete_customer
    when: args.cascade == true
    action: deny
    message: "Cascade deletes must be issued by a human operator"

  - name: cap_refund_amount
    description: Refunds over $500 require finance review
    tool: refund_payment
    when: args.amount_cents > 50000
    action: require_approval
    approvers: [finance-team]
    timeout: 1h

  - name: outreach_domain_allowlist
    description: Only contact verified lead domains
    tool: send_outreach
    when: "!args.recipient.match(/@approved\\.invalid$/)"
    action: deny
    message: "Outreach restricted to allowlisted domains"

How Veto fits

1

Install the SDK

npm install veto-sdk @mastra/core
2

Define policies

Create veto/policies.yaml with rules for each tool ID. Reference the same ID used in createTool.

3

Guard each tool execute

Call await veto.guard() at the guarded execute bodies. Mastra agents, workflows, and memory stay outside the policy surface.

Use cases

Support agent refunds

Mastra support agents handle Stripe refunds. Cap refunds at $500 auto-approve, route larger amounts to finance, record governed actions for chargeback disputes.

Outbound outreach workflows

Mastra workflows generate and send sales outreach. Restrict recipient domains, cap send volume per hour, and block opted-out lead contact on the governed path.

Database write tools

Mastra tools that mutate the database get policy-level protection. Block cascade deletes, restrict schema changes to dev, require approval for production migrations.

Decision record

Guarded calls emit decision records when configured. Queryable via API, exportable for SOC 2 evidence. Mastra telemetry stays for observability, Veto handles the compliance log.

Frequently asked questions

Does Veto work with Mastra workflows and agents?
Yes. Veto guards inside the execute function of any Mastra createTool or workflow Step. The agent definition, model choice, and workflow topology stay outside the policy surface. Whether the call originated from an Agent.generate() or a Workflow step, the integration point is the same.
How does Veto interact with Mastra memory?
Mastra's memory system stores conversation context. Veto evaluates each tool call against policy at execution time. Memory-aware policies can read user context from the call site to make per-user decisions, but Veto does not modify or read Mastra's memory store.
What about Mastra's built-in evals?
Evals measure agent quality offline. Veto applies guardrails online. They run side by side: evals catch behavior drift, Veto blocks disallowed actions in production. Same agent, different concerns.
Can I share policies across multiple Mastra agents?
Yes. Policies match on tool name and arguments, not on agent identity. A shared policy file can cover the agents in your Mastra instance. Per-agent overrides are possible via context tags passed to veto.guard().

Related integrations

Wrap one Mastra tool path and inspect the decision record.