Input & Output Controls
Tool Security
tool-security checks the whole request, including messages and tool definitions, for attack patterns and sensitive data before the model call. It blocks path traversal, rm -rf, the cloud metadata address, secrets such as private keys, and your own patterns. It can also ask an external firewall instead.
It checks the incoming request only, not the tool calls that the model generates later. Use Agent Firewall for those, and enforce permissions in the tool executor.
Configuration
pack:
name: tool-security-example-1
version: "1.0.0"
enabled: true
policies:
chain:
- tool-security
policy:
tool-security:
analysis_mode: local
blocked_patterns:
- delete_messageVerify a local block
Add these tests and run verdictan policy test --json:
testing:
suites:
- name: tool-security
cases:
- name: blocks-pattern
input: {messages: [{role: user, content: "Describe delete_message"}]}
expected: {verdict: block, reason_code: tool-security.matched_blocked_pattern:delete_message}
- name: allows-hello
input: {messages: [{role: user, content: "Hello"}]}
expected: {verdict: allow, reason_code: ok}Both pass. Note that the first message only names the tool and is still blocked: the control matches literal text, not intent.
Fields
| Field | Type | Default | Description |
|---|---|---|---|
analysis_mode | local or external | local | local runs the built-in checks. external sends the request to firewall_endpoint. |
firewall_endpoint | string | External firewall URL. Required with external. | |
secret_key_ref | { env: string } | Variable with the bearer token for the firewall. store is not supported. | |
fail_closed | boolean | true | A firewall error blocks with true and allows with false. |
blocked_entity_types | string[] | [] | Entity types that block in local mode. Empty uses the built-in set. |
blocked_patterns | string[] | [] | Substrings that block in local mode. Case does not matter. |
How it works
- The gateway serializes the request JSON into one string.
- With
externaland an endpoint, it asks the firewall and uses its verdict. See External mode. The local checks do not run. - In local mode, it looks for each
blocked_patternsentry in the lowercased string:tool-security.matched_blocked_pattern:<pattern>. - Then for each built-in attack pattern:
tool-security.matched_pattern:<pattern>. - Then it detects entities. A detected type in the blocked set gives
tool-security.detected_entity:<type>. - Nothing matched:
allowwithtool-security.ok. - The details record
flaggedandreason.
Built-in attack patterns
../..\drop tablerm -rf169.254.169.254file://curl http://localhost
Built-in blocked entity types
Used when blocked_entity_types is empty:
account_numberaws_access_keycvvhealth_plan_beneficiaryjwtmrnpanprivate_keyssn
External mode
The firewall receives the full request JSON and the detected entities. Use a trusted endpoint that is approved for that data. The local rules do not run in this mode.
The gateway sends a POST with a 3-second timeout and a JSON body with two fields: request (the full request) and entities (what the local detector found). If secret_key_ref names a variable with a value, it is sent as a bearer token.
The gateway reads the answer in this order:
| Field | Behavior |
|---|---|
verdict or action | Blocks for deny, block, blocked, reject, rejected, flagged, or review. Any other value allows. |
flagged | Used only when verdict and action are missing |
reason | Recorded as the reason. Otherwise the reason is external_firewall_checked:<endpoint>. |
matched_entities | Recorded in the decision |
The reason code is tool-security.<reason> for both allow and block.
Phase and verdicts
| Item | Value |
|---|---|
| Input phase | The serialized request JSON |
| Output phase | Not checked |
| Verdicts | allow, block |
| Local, no match | tool-security.ok |
| Local block | tool-security.matched_blocked_pattern:<pattern>, tool-security.matched_pattern:<pattern>, tool-security.detected_entity:<type> |
| Firewall error | tool-security.external_firewall_client_error:<error>, tool-security.external_firewall_request_failed:<error>, tool-security.external_firewall_invalid_response:<error> |
| Firewall verdict | tool-security.<reason> or tool-security.external_firewall_checked:<endpoint> |
Examples
Local mode with the built-in checks
policy:
tool-security:
analysis_mode: localExternal firewall with fail-closed errors
policy:
tool-security:
analysis_mode: external
firewall_endpoint: https://firewall.example.internal/check
secret_key_ref:
env: TOOL_FIREWALL_TOKEN
fail_closed: trueLimits and failure behavior
- Local checks are substring matches on the serialized JSON. They do not parse tool arguments.
../also matches a relative path in a message. - Model tool calls are not checked. Use
agent-firewallfor them. - Lint rejects
externalwithoutfirewall_endpoint. A runtime configuration without an endpoint runs the local checks. - A firewall error blocks with
fail_closed: trueand allows withfalse. Either way, the reason code names the error. - The 3-second timeout is fixed. A slow firewall counts as a failed request.
- Your
blocked_entity_typeslist replaces the built-in set, so include the built-in types you still need. - The first match stops the check. The reason code names one pattern or entity type.
Next steps
- Tool Validation: declared tools and schemas.
- Tool Budget:
max_tokensper declared tool. - Agent Firewall: tool calls from the model.