> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwedai.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# QWED Open Responses guards for verified AI agents

> Reference for QWED Open Responses guards including SchemaGuard and ToolGuard, with configuration options, error semantics, and tool-call validation examples.

QWED Open Responses provides 6 verification guards.

***

## SchemaGuard

Validates AI outputs against JSON Schema.

```python theme={null}
from qwed_open_responses import SchemaGuard

guard = SchemaGuard(schema={
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "integer", "minimum": 0}
    },
    "required": ["name", "age"]
})

result = guard.check({"output": {"name": "John", "age": 30}})
# ✅ Passed
```

### Options

| Option | Type | Default | Description |
| - | - | - | - |
| `schema` | dict | required | JSON Schema |
| `strict` | bool | True | Fail on any error |
| `allow_additional_properties` | bool | False | When `True`, SchemaGuard does not add a default `additionalProperties: false` to the root object. In TypeScript, pass `{ allowAdditionalProperties: true }` as the second constructor argument |

### Undeclared fields are rejected by default

In v0.6.0, SchemaGuard closes the root object of your schema by adding `additionalProperties: false`. A response with a top-level field that the schema doesn't declare fails validation. An explicit `additionalProperties` value in your schema always takes precedence over this default.

<Warning>
  **Breaking change in v0.6.0.** Schemas that don't set `additionalProperties` used to accept extra root-level fields. They now reject them. If your outputs legitimately carry extra fields, declare them, set `additionalProperties` in the schema, or opt out in the constructor.

  ```python theme={null}
  schema = {
      "type": "object",
      "properties": {"name": {"type": "string"}},
      "required": ["name"],
  }

  # Before v0.6.0: passed
  # v0.6.0 and later: fails because "debug" is not declared
  SchemaGuard(schema).check({"output": {"name": "John", "debug": True}})

  # Option 1: allow extra fields in the schema itself
  SchemaGuard({**schema, "additionalProperties": True})

  # Option 2: opt out in the constructor
  SchemaGuard(schema, allow_additional_properties=True)
  ```

  ```typescript theme={null}
  // TypeScript opt-out
  new SchemaGuard(schema, { allowAdditionalProperties: true });
  ```
</Warning>

### Format validation

The Python and npm packages validate the JSON Schema `format` keyword with the same rules for `email`, `uuid`, `date-time`, `uri`, and `ipv4`. A value that one runtime rejects is rejected by the other.

### Error reporting

SchemaGuard stops at the first validation error. The failed result has the message "Schema validation failed (first error shown)" and its details contain:

* `errors`: a list with only the first error
* `total_errors` (`totalErrors` in TypeScript): `None`/`null`, because the total count is unknown
* `errors_truncated` (`errorsTruncated` in TypeScript): `True`

Fix the reported error and re-run verification to find any further violations.

***

## ToolGuard

Blocks dangerous tool calls and patterns.

```python theme={null}
from qwed_open_responses import ToolGuard

guard = ToolGuard(
    blocked_tools=["execute_shell", "delete_file"],
    allowed_tools=["search", "calculator"],  # Whitelist mode
    dangerous_patterns=[r"DROP TABLE", r"rm -rf"],
)

result = guard.check({
    "tool_name": "execute_sql",
    "arguments": {"query": "DROP TABLE users"}
})
# ❌ BLOCKED: Dangerous pattern detected
```

### Default blocked tools

Blocklist matching normalizes tool names before comparing them. `ToolGuard` ignores case, surrounding whitespace, dashes, underscores, and camelCase boundaries. `Bash`, `POWERSHELL`, `Execute-Shell`, and `executeShell` are all blocked the same as `bash`, `powershell`, and `execute_shell`.

Allowlists are stricter. `allowed_tools` matching ignores case and surrounding whitespace, but separators must match exactly as configured. An allowlist entry of `read_file` does not authorize `read-file` or `readFile`, so one registered tool can't authorize another.

`custom_validators` keys are matched the same way as allowlist entries. Keys that differ only in case or surrounding whitespace conflict, and `ToolGuard` raises `ValueError` at construction. Distinct names like `read_file` and `read-file` stay separate validators.

The default blocklist covers:

* Execution primitives: `execute_shell`, `shell`, `exec`, `eval`
* Command-tool aliases: `run_command`, `execute_command`, `command_line`, `terminal`
* Common shells and OS command interpreters: `bash`, `sh`, `ash`, `dash`, `zsh`, `ksh`, `csh`, `tcsh`, `fish`, `cmd`, `powershell`, `pwsh`, plus their `.exe` variants and `osascript`, `wscript`, `cscript`
* File mutation: `delete_file`, `remove_file`, `write_file`, `modify_file`
* Side effects: `send_email`, `transfer_money`, `make_payment`
* Client action tools (v0.6.1 and later): `local_shell`, `apply_patch`, `computer_use`

### Default dangerous patterns

`ToolGuard` ships a unified set of 14 dangerous-command patterns, matched case-insensitively. The Python and TypeScript packages enforce the identical superset, so `RM -RF /` is blocked on both runtimes:

* SQL: `DROP TABLE`, `DELETE FROM`, `TRUNCATE TABLE`
* Filesystem: `rm -rf`, `rmdir /s`, `del /f`, `format c:`
* Privilege and permissions: `sudo`, `chmod 777`
* Code execution: `eval(`, `exec(`, `__import__`, `subprocess`, `os.system`

Custom patterns passed via `dangerous_patterns` are also compiled case-insensitively.

`ToolGuard` runs patterns against the raw parsed argument strings. This includes nested values and arguments of JSON-encoded calls. Because the original string is scanned, a custom pattern like `rm\s+-rf` matches across tabs and newlines. Arguments that are cyclic or nested too deeply to inspect fail closed.

Each string is also scanned in a normalized form that catches common command spellings:

* Comment-separated keywords, such as `DROP/*x*/TABLE`
* Spaced dots, such as `os . system`
* Reordered or split `rm` flags, such as `rm -fr`, `rm -r -f`, and `rm --recursive --force`

Normalization also applies to JSON-encoded and base64-encoded arguments.

Argument scanning additionally decodes bounded base64 tokens (7 or more alphabet characters) found inside serialized arguments and scans the decoded text with the same patterns, so a payload like `ZXhlYyg=` (`exec(`) cannot slip through as an encoded string. Pattern scanning is a heuristic, not a security boundary. For real enforcement, prefer `allowed_tools` allowlists plus OS-level sandboxing.

### Recognized tool-call shapes

`ToolGuard` extracts tool calls from these response envelopes:

| Shape | Format |
| - | - |
| Direct tool call | `{"type": "tool_call", "tool_name": ..., "arguments": ...}` |
| Direct function call | `{"type": "function_call", "name": ..., "arguments": ...}` (Responses API) |
| Tool call list | `{"tool_calls": [...]}`, including OpenAI `{"function": {"name", "arguments"}}` wrappers |
| Chat Completions | `{"choices": [{"message": {"tool_calls": [...]}}]}` |
| Anthropic content blocks | `{"content": [{"type": "tool_use", "name": ..., "input": ...}]}` |

The `type` match is case-insensitive, so a `Tool_Use` block is treated as a tool call. JSON-encoded argument strings are parsed before blocklist and dangerous-pattern checks run, so a call cannot hide arguments inside a string.

### Responses API client action items

Since v0.6.1, `ToolGuard` treats Responses API items that ask your client to run an action as tool calls. The blocklist, allowlist, dangerous-pattern, and custom validator checks all apply to them. Before v0.6.1, these items read as tool-free content and passed verification.

`ToolGuard` checks each item under a canonical tool name:

| Item type | Checked as | Arguments scanned |
| - | - | - |
| `local_shell_call` | `local_shell` | `action` |
| `shell_call` | `shell` | `action` |
| `apply_patch_call` | `apply_patch` | `operation` |
| `computer_call` | `computer_use` | `action` |
| `custom_tool_call` | The item's own `name` | Item payload |
| `mcp_approval_request` | The item's own `name` | Item payload |

`ToolGuard` also joins `local_shell` argv lists into a single command line before pattern scanning. This normalization applies to direct items, `output[]` members of a response object, `tool_calls[]` entries, and the streaming middleware, in both the Python and TypeScript packages.

Provider-hosted items (`web_search_call`, `file_search_call`, `code_interpreter_call`, `image_generation_call`, and `mcp_call`) run on the provider side before the response returns. `ToolGuard` keeps their existing behavior.

<Warning>
  **Breaking change in v0.6.1.** The default blocklist now includes `local_shell`, `apply_patch`, and `computer_use`. `shell` was already on it. A `ToolGuard` with default settings blocks every `local_shell_call`, `shell_call`, `apply_patch_call`, and `computer_call` item.

  To run these tools, construct `ToolGuard` with `use_default_blocklist=False`. This drops the whole default blocklist, shells included. Re-add any names you still want blocked through `blocked_tools`. `allowed_tools` is optional, but for least-privilege execution, configure it as an allowlist and list every action tool you run.

  ```python theme={null}
  from qwed_open_responses import ToolGuard

  item = {
      "type": "local_shell_call",
      "call_id": "call_1",
      "action": {"type": "exec", "command": ["ls", "-la"]},
  }

  # Before v0.6.1: passed as tool-free content
  # v0.6.1 and later: blocked because local_shell is on the default blocklist
  ToolGuard().check(item)

  # Allow local_shell and apply_patch, keep shells blocked, allowlist everything you run
  guard = ToolGuard(
      use_default_blocklist=False,
      blocked_tools=["bash", "sh", "powershell", "execute_shell", "computer_use"],
      allowed_tools=["local_shell", "apply_patch", "search"],
  )
  guard.check(item)  # ✅ Passed
  ```

  ```typescript theme={null}
  import { ToolGuard } from "qwed-open-responses";

  const guard = new ToolGuard({
    useDefaultBlocklist: false,
    blockedTools: ["bash", "sh", "powershell", "execute_shell", "computer_use"],
    allowedTools: ["local_shell", "apply_patch", "search"],
  });
  ```
</Warning>

### Fail-closed rejections

`ToolGuard` blocks rather than passes when a response cannot be validated unambiguously:

* **Unrecognized tool-like content.** A response that looks like a tool call but matches none of the recognized shapes is blocked instead of passing with "No tool calls".
* **Malformed entries.** A non-object item inside `tool_calls`, `choices`, or `content` is blocked, never silently dropped.
* **Ambiguous hybrid envelopes.** A response that mixes a direct tool call (`type=tool_call` or `type=function_call`) with a sibling `tool_calls`, `choices`, or `content` collection is blocked, because validating one side would let the other escape policy checks.
* **Nameless calls.** A tool call without a non-blank string name is blocked. A blank name can never match blocklist or allowlist checks.
* **Oversized arguments.** JSON-encoded argument payloads over 10,000 characters or nested deeper than 128 levels are blocked before parsing.
* **Conflicting tool identities.** A call entry that names its tool in more than one place is blocked. This covers root fields (`tool_name`, `name`, `arguments`) combined with a nested `tool_call` or `function_call` object, more than one nested call object, or either of those combined with a `function: {name, arguments}` wrapper. `ToolGuard` can't tell which identity the runtime will execute, so it rejects the entry.
* **Unknown action items.** Any other `*_call` or `*_request` item in a protocol position is blocked as unrecognized. Protocol positions are streamed items, `output_item` events, and `output[]` members of a response object. Ordinary data that uses such type labels elsewhere, such as `pull_request`, is unaffected.
* **Ambiguous action items.** An action item that also carries a `tool_call`, `function_call`, `function`, or `tool_calls` envelope is blocked. A named item whose `tool_name` or `toolName` disagrees with its `name` is also blocked.
* **Truncated nested scans.** `ToolGuard` scans wrapper objects and arrays up to 12 levels deep for hidden tool calls. If the scan reaches that limit, the response is blocked instead of reported as "No tool calls".

Configured ToolGuards also inspect `structured_output` responses, including those produced by `verify_structured_output`. A structured output that carries a tool-shaped payload, such as `name` plus `arguments`, goes through the same blocklist and pattern checks as a direct tool call.

***

## MathGuard

Verifies mathematical calculations.

```python theme={null}
from qwed_open_responses import MathGuard

guard = MathGuard(tolerance=0.01)

result = guard.check({
    "output": {
        "subtotal": 100,
        "tax": 8,
        "shipping": 10,
        "total": 118  # Correct!
    }
})
# ✅ Passed
```

### What it checks

* Totals: one canonical formula per vocabulary, with absent components defaulting to zero:
  * `total = subtotal + tax + shipping - discount`
  * `net = gross - deductions`
  * `balance = credits - debits`
* Percentages: fields ending in `_percent` or `_rate` are verified against their base and amount
* Inline calculations in text: `"5 + 3 = 8"` (every equation in the string is verified, not just the first)
* Custom rules: configured `equals` and `range` rules run whenever their field is present

Each vocabulary (`total`, `net`, `balance`) is verified independently, so an invalid `net` is never hidden by a valid `total`.

### No verifiable math fails with a warning

A response that contains no verifiable math shape no longer passes vacuously. Prose, plain strings, and objects without recognized math fields fail with a warning-severity result:

```python theme={null}
result = guard.check({"output": "The weather is sunny today."})
# ⚠️ Failed (severity="warning"): "No verifiable math found in response"
```

In a `ResponseVerifier` with `strict_mode=True` (the default), this warning-severity failure blocks the response. Only include MathGuard in stacks where responses are expected to carry verifiable math.

### Non-finite values fail closed

`NaN`, infinity, `null`, blank strings, and other non-numeric values in totals, components, or percentage fields return explicit errors instead of silently passing tolerance checks.

***

## StateGuard

Validates state machine transitions.

```python theme={null}
from qwed_open_responses import StateGuard

guard = StateGuard(
    transitions={
        "pending": ["processing", "cancelled"],
        "processing": ["completed", "failed"],
        "completed": [],  # Terminal
    },
    current_state="pending"
)

result = guard.check({"new_state": "processing"})
# ✅ Valid transition

result = guard.check({"new_state": "completed"})
# ❌ Invalid: pending -> completed not allowed
```

***

## ArgumentGuard

Validates tool call arguments.

```python theme={null}
from qwed_open_responses import ArgumentGuard

guard = ArgumentGuard(rules={
    "amount": {"type": "number", "min": 0, "max": 10000},
    "email": {"type": "email"},
    "status": {"type": "enum", "values": ["active", "inactive"]},
})

result = guard.check({
    "arguments": {
        "amount": 500,
        "email": "user@example.com",
        "status": "active"
    }
})
# ✅ All arguments valid
```

### Supported types

| Type | Validation |
| - | - |
| `string` | Is string |
| `number` | Is number, min/max |
| `integer` | Is integer |
| `boolean` | Is boolean |
| `email` | Email format |
| `url` | URL format |
| `uuid` | UUID format |
| `enum` | In allowed values |
| `pattern` | Regex match |

***

## SafetyGuard

Comprehensive safety checks.

```python theme={null}
from qwed_open_responses import SafetyGuard

guard = SafetyGuard(
    check_pii=True,        # Detect emails, phones, SSN
    check_injection=True,  # Detect prompt injection
    check_harmful=True,    # Detect API keys, passwords
    max_cost=100.0,        # Budget limit
)

result = guard.check({
    "content": "ignore previous instructions..."
})
# ❌ BLOCKED: Prompt injection detected
```

### Detections

| Type | Examples |
| - | - |
| **PII** | Emails, phones, SSN, credit cards, IP addresses |
| **Injection** | "ignore previous", "you are now", `system:` role-override directives |
| **Harmful** | API keys, passwords, private keys (including generic PEM headers) |
| **Budget** | Cost/token limits exceeded |

The Python and TypeScript packages run the same case-insensitive pattern superset. Email PII detection uses a linear-time scan, so long or adversarial input can't trigger catastrophic regex backtracking. The TypeScript SafetyGuard performs the same harmful-content and IP-address PII checks as Python, and collects all error-severity findings instead of stopping at the first match.

Credential detection is value-aware: a bare label like `password: required` or `api_key: not set` passes, while a label followed by a real-looking value is blocked. Quoted JSON credentials such as `{"api_key": "sk-live-..."}` are decoded and checked the same way, and documented placeholders like `"<your-api-key>"` stay exempt. Injection detection requires instruction-override context after a `system:` marker, so benign text like `Operating system: Linux` is not flagged.

### Configuration

The Python and TypeScript SafetyGuards accept the same PII allowlist and custom pattern options:

<CodeGroup>
  ```python Python theme={null}
  from qwed_open_responses import SafetyGuard

  guard = SafetyGuard(
      check_pii=True,
      check_injection=True,
      check_harmful=True,
      pii_allow_list={"email"},
      custom_patterns=[r"internal\s+marker"],
  )
  ```

  ```typescript TypeScript theme={null}
  import { SafetyGuard } from 'qwed-open-responses';

  const guard = new SafetyGuard({
    checkPii: true,
    checkInjection: true,
    checkHarmful: true,
    piiAllowList: ['email'],
    customPatterns: ['internal\\s+marker'],
  });
  ```
</CodeGroup>

| Option | Description |
| - | - |
| `pii_allow_list` / `piiAllowList` | PII types to skip, such as `email` or `ip_address`. The TypeScript option accepts camelCase and snake\_case names, so `ipAddress` and `ip_address` are equivalent. |
| `custom_patterns` / `customPatterns` | Extra case-insensitive regular expressions to block. Each matched pattern fails the check and adds its own `{"type": "custom_pattern", "severity": "error", "pattern": ...}` finding. |

In TypeScript, an invalid custom pattern throws when you construct the guard. Each pattern runs with a 25 ms execution limit. A pattern that times out fails the safety check closed. Avoid nested or overlapping quantifiers such as `(a+)+$`, which can cause catastrophic backtracking.

### Content extraction

SafetyGuard scans string content nested up to 12 levels deep, not just top-level keys. This includes:

* The canonical OpenAI shape: `choices[].message.content`
* Anthropic content block envelopes
* Strings nested inside arbitrary wrapper objects and arrays

Content hidden inside a nested structure is checked for PII, injection, and harmful patterns the same as top-level content.

Object field names are scanned too. A payload like `{"ignore previous instructions": true}` or a key that contains an email address is flagged for injection or PII, even when its value is benign. Field names don't count toward the content length limit.

### Resource limits

SafetyGuard scanning is bounded. It fails closed when content exceeds any of these limits:

| Limit | Value |
| - | - |
| Nesting depth | 12 levels |
| Nodes scanned | 10,000 |
| Content length | 100,000 characters |

Keep payloads you need verified within these bounds.

***

## Combining guards

```python theme={null}
from qwed_open_responses import (
    ResponseVerifier,
    ToolGuard,
    SchemaGuard,
    SafetyGuard,
)

verifier = ResponseVerifier(
    default_guards=[
        ToolGuard(),
        SchemaGuard(schema=my_schema),
        SafetyGuard(),
    ]
)

result = verifier.verify(response)
print(f"Passed: {result.guards_passed}")
print(f"Failed: {result.guards_failed}")
```

### Warning semantics

A guard result with `severity="warning"` passes the guard. Warnings do not flip `verified` to `False` on their own. They surface as a separate, visible state on the result:

```python theme={null}
result = verifier.verify(response)
print(result.verified)   # True even with warnings
for warning in result.warnings:
    print(f"⚠️ {warning.guard_name}: {warning.message}")
```

To escalate warnings to failures, create the verifier with `allow_warnings=False`:

```python theme={null}
verifier = ResponseVerifier(
    default_guards=[MathGuard(), SafetyGuard()],
    allow_warnings=False,  # warnings fail (and block in strict mode)
)
```

With `strict_mode=True` (the default), any failed guard blocks the response.

### Strict response parsing

`verify()` accepts a dict or a JSON string that parses to an object. A string that parses to a JSON scalar, array, or `null` raises `ValueError` instead of verifying content that guards never inspected. A plain non-JSON string is wrapped as `{"type": "text", "content": ...}`. The TypeScript `parseResponse` behaves identically.

JSON string parsing is resource-bounded. `verify()` returns a failed verdict instead of raising an exception when a JSON response:

* Is longer than 100,000 characters
* Nests deeper than 100 levels
* Contains an integer too large to convert safely

The failure comes from a `ResponseVerifier` guard result with `severity="error"`, and the result is `blocked=True` in strict mode.

### Tamper-evident bindings

Results produced by `ResponseVerifier.verify()` carry a `binding`: a SHA-256 digest covering the verified response and the guard names. Call `verify_binding()` to detect a result that was replayed against a different response or had its guard metadata altered:

```python theme={null}
result = verifier.verify(response)

result.verify_binding()               # True — digest matches
result.verify_binding(other_response) # False — replay detected
```

`verify_binding()` returns `False` for hand-constructed results with no binding. Binding digests are runtime-portable between Python and TypeScript. A binding detects mismatches only. It is public, recomputable data and does not authenticate the result, so treat results you did not produce in-process as untrusted.

### Correlating results with request IDs

Pass a `request_id` in the verification context to tie a verdict back to the response that produced it. The value is carried on `VerificationResult.request_id` (`requestId` in TypeScript) and included in `to_dict()` output:

```python theme={null}
result = verifier.verify(response, context={"request_id": "req_abc123"})
print(result.request_id)  # "req_abc123"
```

Result timestamps are timezone-aware UTC and carry the `+00:00` offset.

### Passing usage totals for budget checks

SafetyGuard budget limits (`max_cost`, `max_tokens`) compare a response's reported usage against trusted running totals in the context: `total_cost` and `total_tokens`. The `verify_tool_call` and `verify_structured_output` convenience methods accept the same `context` argument as `verify()`:

```python theme={null}
verifier = ResponseVerifier(default_guards=[SafetyGuard(max_cost=10.0, max_tokens=50_000)])

result = verifier.verify_tool_call(
    "search",
    {"query": "weather"},
    context={"total_cost": 9.50, "total_tokens": 48_000},
)
```

The OpenAI, streaming, and LlamaIndex integrations forward the context as well. LangChain integrations forward `verification_context` only when a callback emitter explicitly supplies that keyword; the standard LangChain agent path does not supply it. See the [OpenAI integration](/open-responses/openai#budget-limits-with-verification-context) and [streaming middleware](/open-responses/overview#streaming-middleware).

### Zero guards fail closed

`verify()` with no guards configured returns `verified=False`. Absence of verification is not success.

```python theme={null}
verifier = ResponseVerifier()  # no default_guards

result = verifier.verify(response)
print(result.verified)      # False
print(result.block_reason)  # "No guards configured — fail-closed (zero-guard verify)."
```

When `strict_mode=True` (the default), the result is also `blocked=True`. Pass at least one guard to `verify()` or set `default_guards` on the verifier. The TypeScript `ResponseVerifier` behaves the same way with `defaultGuards`.


## Related topics

- [QWED Open Responses examples for verified AI agents](/open-responses/examples.md)
- [QWED Open Responses: verified tool calls for AI agents](/open-responses/overview.md)
- [OpenAI integration for verified AI agents](/open-responses/openai.md)
- [Changelog](/changelog.md)
- [LangChain integration for verified AI agents](/open-responses/langchain.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.