Skip to content

Pre-validate inner tool args at the worker-tool gateway#25

Merged
fxspeiser merged 1 commit into
mainfrom
feature/worker-tool-arg-validation
May 26, 2026
Merged

Pre-validate inner tool args at the worker-tool gateway#25
fxspeiser merged 1 commit into
mainfrom
feature/worker-tool-arg-validation

Conversation

@fxspeiser
Copy link
Copy Markdown
Owner

Summary

Closes the PR #21 follow-up. The gateway now validates inner tool args against the tool's `inputSchema` BEFORE dispatching — workers get a clean, structured refusal (with a `schema_error` field) instead of whatever ad-hoc error envelope the inner tool happens to produce.

Validation order: allowlist → input schema → session_id injection → dispatch. session_id is added AFTER the schema check so a missing session_id never trips the validator.

Refusal payload extended: `_worker_tools_refusal(name, reason, hint, *, schema_error=None)`. When the refusal stems from schema validation, the worker sees the validator message in a dedicated `schema_error` field and can self-correct on the next hop within the remaining hop budget.

Telemetry: `worker_inner_validation_fail` ndjson event fires per rejected call so operators can spot workers repeatedly misusing inner tools.

Test plan

  • New `scripts/test_worker_tool_arg_validation.py` covers missing-required / wrong-type / unknown-extra-arg for both `fetch` and `verify`; regression checks for valid args, allowlist denial (no `schema_error`), and end-to-end recovery (invalid call → refusal → valid call within hop budget)
  • Full suite (37 scripts) passes locally

🤖 Generated with Claude Code

Closes the PR #21 follow-up. Today the gateway only enforces the
allowlist; bad args fell through to the inner tool which returned its
own ad-hoc error envelope. Workers got inconsistent refusal shapes
across rejection reasons (allowlist vs. schema vs. hop budget vs. cost
cap) and the inner tool burned code paths handling garbage.

The fix:
- Look up the inner tool's `inputSchema` from `TOOLS[name]` and run
  the existing boundary validator `_validate_input` BEFORE dispatch.
- On validation failure, return a refusal payload carrying a
  structured `schema_error` field with the validator message — the
  worker can read it programmatically and self-correct on the next
  hop (within the remaining hop budget).
- Emit a `worker_inner_validation_fail` ndjson event so operators can
  see when a worker is repeatedly misusing inner tools.

Validation order is: allowlist -> input schema -> session_id injection
-> dispatch. session_id is added AFTER the schema check so a missing
session_id arg never trips the validator (it's always optional on the
inner tools).

API change:
- `_worker_tools_refusal` grows an optional `schema_error` keyword
  argument; the payload gains a `schema_error` field when set.
- New helper `_worker_tool_input_schema(name)` for the lookup (defensive
  against module-load order: returns empty dict if TOOLS isn't bound yet).

Tests (scripts/test_worker_tool_arg_validation.py):
- `fetch` with no `url`: refusal with schema_error mentioning the missing field
- `fetch.url` wrong type: refusal mentioning type + field
- `verify` missing `checks`: refusal
- `verify.checks` wrong type: refusal mentioning array
- Unknown extra arg on `fetch` (additionalProperties=false): refusal
- VALID `fetch` args dispatch normally with session_id injected
- VALID `verify` args dispatch normally
- Allowlist refusal still works AND has no `schema_error` field (it
  short-circuits before the schema check)
- End-to-end recovery: worker emits invalid call, gets refusal with
  schema_error, recovers on next hop with valid args; only the valid
  call reaches the inner tool

Full suite (37 scripts) passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@fxspeiser fxspeiser merged commit ad6f2d9 into main May 26, 2026
1 check passed
@fxspeiser fxspeiser deleted the feature/worker-tool-arg-validation branch May 26, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant