Tool Argument Schema Policy
The tool-argument-schema policy enforces OWASP LLM06a (excessive agency, sub-control "argument-level scoping"). It validates tool-call arguments against JSON schemas before invocation. This is the companion control to Tool Allowlist — where tool-allowlist answers "which tools can this agent call?", this handler answers "with what arguments?".
The classic exfiltration shape: delete_user is on the allowlist, but delete_user(role="admin") from a customer-facing surface is not. This handler blocks the second case even though the tool name is permitted.
Uses the jsonschema library when installed; falls back to a minimal hand-rolled validator covering type, required, enum, properties, and additionalProperties=false.
Rules
| Rule | Type | Default | Description |
|---|---|---|---|
schemas | object | {} | Map of tool_name → JSON Schema. Each call to the tool must validate against the schema. |
require_schema_for_all_tools | boolean | false | Strict mode — any tool call without a declared schema is rejected |
action_on_violation | string | "block" | "block" or "warn" |
How It Works
| Phase | Behavior |
|---|---|
before_workflow | Validates any tool calls already queued at workflow start (rare — workflow restart with queued actions) |
mid_execution | Primary enforcement — every queued tool call between LLM turns |
after_workflow | (no-op — argument validation is a pre-call concern) |
Per pending action, the handler:
- Skips non-tool-call entries (
kindmust be"tool_call","tool", or absent). - Looks up
schemas[tool_name]. - If no schema and
require_schema_for_all_tools=true→ BLOCK withmissing_schema. - If schema present, validates
args(orarguments) against it → BLOCK withschema_violationon failure.
Context Attributes Read
| Attribute | Phase | Purpose |
|---|---|---|
context.pending_actions | before, mid | List of {"kind": "tool_call", "tool": "...", "args": {...}} |
Example Policy
Atlassian-style policy — cap fund transfers at $10k, restrict delete_user to non-admin targets, and require schemas for everything else:
{
"schemas": {
"transfer_funds": {
"type": "object",
"required": ["amount", "recipient"],
"properties": {
"amount": {"type": "number", "maximum": 10000, "minimum": 0},
"recipient": {"type": "string", "pattern": "^acct_[a-z0-9]+$"},
"memo": {"type": "string", "maxLength": 200}
},
"additionalProperties": false
},
"delete_user": {
"type": "object",
"required": ["id"],
"properties": {
"id": {"type": "string"},
"role": {"type": "string", "enum": ["user"]}
},
"additionalProperties": false
},
"send_email": {
"type": "object",
"required": ["to", "subject", "body"],
"properties": {
"to": {"type": "string", "format": "email"},
"subject": {"type": "string", "maxLength": 200},
"body": {"type": "string", "maxLength": 5000}
}
}
},
"require_schema_for_all_tools": true,
"action_on_violation": "block"
}
SDK Integration
import waxell_observe as waxell
waxell.init()
@waxell.observe(agent_name="finance-bot", enforce_policy=True)
async def process(intent: str) -> str:
# When the agent queues a tool call, the runtime adds
# {"kind": "tool_call", "tool": "transfer_funds", "args": {...}}
# to context.pending_actions. mid_execution validates each entry
# against schemas["transfer_funds"] before the call is dispatched.
return await run_finance_workflow(intent)
Observability
| Field | Example |
|---|---|
| Category | tool-argument-schema |
| Action | block |
| Reason | "Tool 'transfer_funds' arguments failed schema validation: 25000 is greater than the maximum of 10000 (at amount)" |
| Metadata | {"phase": "mid", "tool": "transfer_funds", "signal": "schema_violation", "error": "25000 is greater than the maximum of 10000 (at amount)", "owasp": "LLM06"} |
Missing schema in strict mode:
| Field | Example |
|---|---|
| Reason | "Tool 'list_pages' has no declared argument schema and require_schema_for_all_tools is true." |
| Metadata | {"signal": "missing_schema", "tool": "list_pages", "owasp": "LLM06"} |
Common Gotchas
- Argument key is
argsORarguments. The handler readsaction_obj.get("args") or action_obj.get("arguments") or {}. Both shapes work — the rest of the runtime tends to useargs. - Tool name is
toolORname. Same fallback — most pending-action records usetool. - Invalid policy schemas fail-open. If your JSON Schema is itself malformed (
jsonschema.SchemaError), the handler logs a warning and returns ALLOW — so a bad schema never blocks legitimate work. Validate your schemas separately. - The minimal fallback validator is limited. Without
jsonschemainstalled, onlytype,required,enum,properties, andadditionalProperties=falseare checked. No$ref,oneOf,anyOf,pattern,format,minimum/maximum, etc. Production deploys should always havejsonschemainstalled. require_schema_for_all_tools=trueblocks ANY undeclared tool. Combine carefully with Tool Allowlist — if the allowlist permits a tool but you forget to add its schema, this handler will still reject it.- Error messages are truncated to 200 characters. Verbose
jsonschemapath/cause output would dominate the metadata payload; the policy keeps the human-readable message and the dotted path. - Empty
schemas+require_schema_for_all_tools=falseis effectively a no-op. The policy must have either schemas configured or strict mode enabled to do anything.
Next Steps
- Policy Categories — All 49 categories
- Tool Allowlist — Companion control on tool names
- Agent Service Account Scope — Companion OWASP LLM06b control
- Code Execution — Special-case argument validation for code-running tools