Skip to main content
Guardrails let you enforce content policies and tool access rules at the agent level, not inside individual handlers. Pass a guardrails array to defineAgent and every request through that route is automatically screened — user input before it reaches the model, model output before it reaches the client, and tool calls before they execute. Anvil ships four built-in guardrails covering the most common needs; you can also implement the Guardrail interface directly for custom logic.

The Guardrail interface

interface Guardrail {
  name: string;
  /** Transform text (return new text) or throw GuardrailError to block. */
  onText?(text: string, ctx: TextContext): string | void;
  /** Decide whether a tool may run: allow / deny / approve. */
  onToolCall?(ctx: ToolCallContext): ToolDecision | void;
}
  • onText receives the raw text and a TextContext with role: 'input' | 'output'. Return a (possibly modified) string to continue, or throw GuardrailError to block the run entirely.
  • onToolCall receives the tool name, its input, and a tainted flag indicating whether untrusted (tool or retrieved) content is already in the conversation. Return a ToolDecision to explicitly allow, deny, or route the call to human approval.

ToolDecision

type ToolDecision = { action: 'allow' | 'deny' | 'approve'; reason?: string };
The most restrictive decision across all guardrails in the chain wins: deny beats approve, approve beats allow.

GuardrailError

Throw GuardrailError from onText to immediately stop the run:
import { GuardrailError } from 'anvil/agent';

throw new GuardrailError('my-guardrail', 'Input contains disallowed content');
The guardrail property on the error holds the name string so downstream error handlers can identify which policy fired.

Built-in guardrails

contentFilter

Block or redact text matching custom regular expression patterns.
import { contentFilter } from 'anvil/agent';

contentFilter({
  deny: [/\bpassword\b/i, /\bsecret_key\b/i],
  mode: 'block',         // 'block' (default) | 'redact'
  replacement: '[REMOVED]', // used only when mode: 'redact'
  applyTo: ['input'],    // 'input' | 'output' | both (default)
})
deny
RegExp[]
required
Patterns to screen. Each pattern is tested against the incoming text; the first match triggers the configured action.
mode
'block' | 'redact'
default:"'block'"
What to do when a pattern matches. 'block' throws GuardrailError; 'redact' replaces every match with replacement.
replacement
string
default:"'[REDACTED]'"
Substitution string used in 'redact' mode.
applyTo
Array<'input' | 'output'>
default:"['input', 'output']"
Which text direction to inspect. Restrict to ['input'] to screen only user messages, or ['output'] for model responses.

redactPII

Automatically redact common PII patterns from input and/or output without needing to supply regexes:
import { redactPII } from 'anvil/agent';

redactPII()
// or restrict the direction:
redactPII({ applyTo: ['input'] })
The following patterns are redacted:
PII typeReplacement token
Email address[REDACTED_EMAIL]
Payment card number (13–16 digits)[REDACTED_CARD]
US Social Security Number (NNN-NN-NNNN)[REDACTED_SSN]
US phone number[REDACTED_PHONE]
applyTo
Array<'input' | 'output'>
default:"['input', 'output']"
Which text direction to redact. Defaults to both.

toolPolicy

Enforce an explicit allowlist, denylist, or approval requirement on tool names:
import { toolPolicy } from 'anvil/agent';

toolPolicy({
  allow: ['lookupOrder', 'getProductInfo'],  // only these tools may run
  deny: ['deleteAccount'],                   // always blocked
  requireApproval: ['issueRefund'],          // pauses run → HITL queue
})
allow
string[]
When set, only tools whose names appear in this list may run. Any other tool call returns a deny decision.
deny
string[]
Tools that are unconditionally blocked regardless of allow.
requireApproval
string[]
Tools that require human approval before executing. Matching tool calls receive an approve decision, suspending the run and adding it to the HITL queue.

injectionGuard

Defend against prompt-injection attacks. Once untrusted content (from a tool result or a retrieval) has entered the conversation, the model may have been instructed to make malicious tool calls. injectionGuard detects this “tainted” context and gates subsequent tool calls:
import { injectionGuard } from 'anvil/agent';

injectionGuard({
  mode: 'approve',                      // 'block' | 'approve' (default) | 'allow'
  allowlist: ['getWeather', 'lookupFAQ'], // always allowed even in tainted context
})
mode
'block' | 'approve' | 'allow'
default:"'approve'"
Action to take when a tool call arrives in a tainted context:
  • 'block' — denies the call outright with a deny decision.
  • 'approve' — routes the call to human approval via the HITL queue.
  • 'allow' — disables the guard (useful for temporarily turning it off without removing the guardrail).
allowlist
string[]
Tool names that are always permitted even when context is tainted. Use this for idempotent, read-only lookups that carry no injection risk.

Wiring multiple guardrails into defineAgent

import { defineAgent } from 'anvil/agent';
import {
  contentFilter,
  redactPII,
  toolPolicy,
  injectionGuard,
} from 'anvil/agent';
import { openai } from 'anvil/llm';

export const post = defineAgent({
  client: openai({ model: 'gpt-4o' }),
  guardrails: [
    // 1. Strip PII from both input and model output
    redactPII(),

    // 2. Block input that tries to override the system prompt
    contentFilter({
      deny: [/ignore previous instructions/i, /you are now/i],
      mode: 'block',
      applyTo: ['input'],
    }),

    // 3. Restrict which tools can run and require approval for refunds
    toolPolicy({
      allow: ['lookupOrder', 'getProductInfo', 'issueRefund'],
      requireApproval: ['issueRefund'],
    }),

    // 4. Gate tool calls that arise from tainted context
    injectionGuard({
      mode: 'approve',
      allowlist: ['lookupOrder'],
    }),
  ],
  tools: [lookupOrder, getProductInfo, issueRefund],
});
Guardrails run in the order they appear in the array. For onText, each guardrail’s return value is passed as input to the next — they compose as a transform pipeline. For onToolCall, the most restrictive decision across all guardrails wins (deny beats approve, approve beats allow).

Implementing a custom guardrail

import type { Guardrail } from 'anvil/agent';

const lengthGuard: Guardrail = {
  name: 'length-guard',
  onText(text, ctx) {
    if (ctx.role === 'input' && text.length > 10_000) {
      throw new GuardrailError('length-guard', 'Input exceeds 10,000 characters');
    }
  },
  onToolCall({ name }) {
    if (name === 'runArbitraryCode') {
      return { action: 'deny', reason: 'code execution not permitted' };
    }
  },
};
Return void or undefined from onText to pass the text through unchanged. Return void or undefined from onToolCall to abstain, letting other guardrails in the chain decide.