Skip to main content

Input & Output Controls

Agent Firewall

agent-firewall checks the tool calls in model traffic and allows or blocks each one. For example, an agent may call search and read_* tools but never a deletion tool. It can also limit how many actions one request contains, stop on suspicious commands, and escalate large payments for approval.

Every limit applies to one request or response. The control keeps no counters between requests, so max_daily_total means the total in this request, not in a day.

Configuration​

The control has a simple shape and an expanded shape. Use one of them in a block. Lint rejects a block that mixes them.

Start with the simple shape. Save it as policy-config.yaml in a test directory, and use the config-first workflow to apply it.

policy-config.yaml
pack:
  name: agent-firewall-simple
  version: 1.0.0
  enabled: true
policies:
  chain:
    - agent-firewall
policy:
  agent-firewall:
    allowed_tools:
      - read_*
      - search
    blocked_tools:
      - rm_rf
    max_actions_per_session: 50

search and read_document are allowed. rm_rf is blocked, and write_document is rejected because no rule allows it.

Expanded shape: role, count, and transaction rules

Use this shape for rules by role. Do not combine it with the simple shape.

policy-config.yaml
pack:
  name: agent-firewall-expanded
  version: 1.0.0
  enabled: true
policies:
  chain:
    - agent-firewall
policy:
  agent-firewall:
    tools:
      roles:
        analyst:
          allowed:
            - read_database
            - export_csv
          denied:
            - delete_database
    rate_limits:
      export_csv: 1
    transaction_limits:
      max_single_transaction: 5000.0
      max_daily_total: 20000.0
      require_approval_above: 1000.0
    kill_switches:
      halt_on_suspicious_pattern: true
      halt_on_pii_in_action: true

Try it without a provider​

The control reads tool calls, so give each test case a raw request with an assistant tool call. Add this to the simple configuration:

Add to policy-config.yaml
testing:
  suites:
    - name: agent-firewall
      cases:
        - name: allows-search
          input:
            request:
              model: test-model
              messages:
                - role: user
                  content: "Find the report."
                - role: assistant
                  tool_calls:
                    - id: call_1
                      type: function
                      function: {name: search, arguments: "{\"q\":\"report\"}"}
          expected: {verdict: allow, reason_code: ok}
        - name: blocks-rm-rf
          input:
            request:
              model: test-model
              messages:
                - role: user
                  content: "Clean up."
                - role: assistant
                  tool_calls:
                    - id: call_1
                      type: function
                      function: {name: rm_rf, arguments: "{}"}
          expected: {verdict: block, reason_code: agent_firewall.blocked_tool.rm_rf}
        - name: blocks-unlisted-tool
          input:
            request:
              model: test-model
              messages:
                - role: user
                  content: "Save it."
                - role: assistant
                  tool_calls:
                    - id: call_1
                      type: function
                      function: {name: write_document, arguments: "{}"}
          expected: {verdict: block, reason_code: agent_firewall.tool_not_allowed}

Run verdictan policy test --json. All three cases pass. Then send a real tool call through a test gateway before you connect a tool executor.

Fields​

FieldTypeDefaultDescription
allowed_toolsstring[]["*"]Simple shape. Tool name patterns that are allowed. * matches any sequence, including an empty one.
blocked_toolsstring[][]Simple shape. Tool name patterns that are blocked. A block wins over an allow.
max_actions_per_sessioninteger0Both shapes. Maximum number of assistant messages in the request. 0 turns the check off. The schema shows 500.
tools.roles.<role>.allowedstring[][]Expanded shape. Tool patterns allowed for one role of the verified identity
tools.roles.<role>.deniedstring[][]Expanded shape. Tool patterns denied for the role
rate_limits.defaultinteger0Expanded shape. Maximum tool actions in one evaluation. 0 turns it off.
rate_limits.<tool>integerExpanded shape. Maximum calls of one tool in one evaluation
transaction_limits.max_single_transactionnumber0.0Expanded shape. Largest dollar amount allowed in the tool text. 0.0 turns it off.
transaction_limits.max_daily_totalnumber0.0Expanded shape. Largest allowed sum of dollar amounts in the tool text. 0.0 turns it off.
transaction_limits.require_approval_abovenumber0.0Expanded shape. Escalates when the largest amount reaches this value. 0.0 turns it off.
kill_switches.halt_on_suspicious_patternbooleanfalseExpanded shape. Blocks when the tool text contains a built-in suspicious pattern
kill_switches.halt_on_pii_in_actionbooleanfalseExpanded shape. Blocks when a tool argument contains PII

The schema shows defaults that the runtime does not use: rate_limits.default: 60, transaction limits of 10000, 50000, and 5000, and both kill switches true. At runtime, a missing field turns its check off. Set every limit you need.

Lint also accepts kill_switches.max_errors_before_halt and a monitoring section. The runtime ignores them.

How it works​

  1. The control reads tool calls from the request or response: messages[].tool_calls of assistant messages, choices[].message.tool_calls, Responses API output and input items, and Anthropic content blocks. A tool call that it cannot parse blocks with agent_firewall.malformed_tool_call.
  2. It also finds tool names in assistant text with patterns such as "name": "<tool>", function_call: <tool>, tool: <tool>, and calling <tool>.
  3. With tools.roles, it keeps the roles of the verified identity that appear in the table.
  4. With halt_on_suspicious_pattern, tool text that contains rm -rf, sudo , drop table, exfiltrate, or disable security blocks with agent_firewall.kill_switch.
  5. Too many actions in total, or too many calls of one tool, blocks with agent_firewall.rate_limit_exceeded.
  6. More assistant messages than max_actions_per_session blocks with agent_firewall.session_limit_exceeded.
  7. The control collects each $ amount in the tool text. Over a limit, it blocks with agent_firewall.transaction_value_exceeded or agent_firewall.daily_total_exceeded. At or above require_approval_above, it escalates with agent_firewall.approval_required.
  8. With halt_on_pii_in_action, a PII match in a tool argument blocks with agent_firewall.pii_in_tool_args.
  9. With tools.roles and tool actions present, an identity without a listed role is blocked with agent_firewall.authoritative_role_required.
  10. Each tool action is checked in this order: role denied, role allowed, blocked_tools, allowed_tools. The reason codes are agent_firewall.role_denied_tool, agent_firewall.role_tool_not_allowed, agent_firewall.blocked_tool.<tool>, and agent_firewall.tool_not_allowed.
  11. Otherwise, the request is allowed with agent_firewall.allowed.

The allow details contain action_count, session_action_count, role, authoritative_roles, authoritative_permissions, actions, and structured_action_count.

Phase and verdicts​

ItemValue
Phasetool, in the input and output stages
Request familiesChat Completions, Responses, Messages
Verdictsallow, block, escalate
Reason codesagent_firewall.<check>, listed below
  • agent_firewall.allowed
  • agent_firewall.malformed_tool_call
  • agent_firewall.kill_switch
  • agent_firewall.rate_limit_exceeded
  • agent_firewall.session_limit_exceeded
  • agent_firewall.transaction_value_exceeded
  • agent_firewall.daily_total_exceeded
  • agent_firewall.approval_required
  • agent_firewall.pii_in_tool_args
  • agent_firewall.authoritative_role_required
  • agent_firewall.role_denied_tool
  • agent_firewall.role_tool_not_allowed
  • agent_firewall.blocked_tool.<tool>
  • agent_firewall.tool_not_allowed

A block returns HTTP 400 with error code content_policy_violation. An escalation returns HTTP 200 with content: null, finish_reason: content_filter, and a verdictan.escalation object with the escalation ID and status queued.

Limits and failure behavior​

  • Counts cover one request or response only. The gateway keeps no counter between requests, so these are not durable rate limits.
  • Transaction checks read only $ amounts, such as $1,250.00. Other currency symbols and amounts without $ pass.
  • In the simple shape, an empty allowed_tools list allows nothing. Leave the field out to keep the ["*"] default.
  • With tools.roles, identity roles that the table does not list are ignored. If none is listed, every tool action is blocked. If several are listed, every one of them must allow the tool.
  • Role rules use only the verified identity. An X-User-Role header does not select a role.
  • The suspicious pattern check matches five fixed substrings. sudo matches only when a space follows it.
  • The PII check on tool arguments uses the base detector, without healthcare_mode or pci_mode.
  • With stream: true, the gateway holds the streamed tool calls until the stream ends, checks them, and then sends the stream or a final error.
  • A request without tool calls is never blocked by this control. To block on identity alone, use RBAC.

Next steps​

  • RBAC: allow tools by verified role, with data sensitivity limits.
  • Tool Security: check request payloads for unsafe tool definitions.
  • Resolve an Escalation: approve the payments that require_approval_above escalates.