SchemaGuard
Validates AI outputs against JSON Schema.Options
Undeclared fields are rejected by default
In v0.6.0, SchemaGuard closes the root object of your schema by addingadditionalProperties: 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.
Format validation
The Python and npm packages validate the JSON Schemaformat 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 errortotal_errors(totalErrorsin TypeScript):None/null, because the total count is unknownerrors_truncated(errorsTruncatedin TypeScript):True
ToolGuard
Blocks dangerous tool calls and patterns.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.exevariants andosascript,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
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
rmflags, such asrm -fr,rm -r -f, andrm --recursive --force
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:
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:
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.
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, orcontentis blocked, never silently dropped. - Ambiguous hybrid envelopes. A response that mixes a direct tool call (
type=tool_callortype=function_call) with a siblingtool_calls,choices, orcontentcollection 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 nestedtool_callorfunction_callobject, more than one nested call object, or either of those combined with afunction: {name, arguments}wrapper.ToolGuardcan’t tell which identity the runtime will execute, so it rejects the entry. - Unknown action items. Any other
*_callor*_requestitem in a protocol position is blocked as unrecognized. Protocol positions are streamed items,output_itemevents, andoutput[]members of a response object. Ordinary data that uses such type labels elsewhere, such aspull_request, is unaffected. - Ambiguous action items. An action item that also carries a
tool_call,function_call,function, ortool_callsenvelope is blocked. A named item whosetool_nameortoolNamedisagrees with itsnameis also blocked. - Truncated nested scans.
ToolGuardscans 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”.
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.What it checks
- Totals: one canonical formula per vocabulary, with absent components defaulting to zero:
total = subtotal + tax + shipping - discountnet = gross - deductionsbalance = credits - debits
- Percentages: fields ending in
_percentor_rateare 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
equalsandrangerules run whenever their field is present
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: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.ArgumentGuard
Validates tool call arguments.Supported types
SafetyGuard
Comprehensive safety checks.Detections
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:
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
{"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:
Keep payloads you need verified within these bounds.
Combining guards
Warning semantics
A guard result withseverity="warning" passes the guard. Warnings do not flip verified to False on their own. They surface as a separate, visible state on the result:
allow_warnings=False:
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
ResponseVerifier guard result with severity="error", and the result is blocked=True in strict mode.
Tamper-evident bindings
Results produced byResponseVerifier.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:
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 arequest_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:
+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():
verification_context only when a callback emitter explicitly supplies that keyword; the standard LangChain agent path does not supply it. See the OpenAI integration and streaming middleware.
Zero guards fail closed
verify() with no guards configured returns verified=False. Absence of verification is not success.
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.