An agent-tool contract test proves more than “the JSON parsed.” It verifies that advertised schemas accept intended requests and reject invalid ones, that results match their declared shape, that errors are stable, and that policy middleware blocks unauthorized effects. Test the contract from both producer and agent-runtime perspectives before measuring model behavior.
Define the complete contract
For each tool, version these surfaces:
| Surface | Contract questions |
|---|---|
| Discovery | Is the name unique and description accurate? |
| Input | Required fields, types, bounds, formats, unknown properties? |
| Output | Success and error variants, nullability, provenance? |
| Semantics | Preconditions, postconditions, idempotency, side effects? |
| Policy | Who may call it, for which tenant and resources? |
| Operations | Deadline, rate limit, retry and cancellation behavior? |
JSON Schema Draft 2020-12 defines structural and validation vocabularies, but implementations can differ in supported vocabularies and format behavior. Declare $schema, choose a conforming validator, and test the validator configuration itself. JSON Schema specification
The schema is not the whole contract. It cannot prove that an account belongs to the caller or that a refund is authorized.
Start with a restrictive schema
This illustrative schema makes accidental inputs easier to reject:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.test/tools/get-order-input.schema.json",
"type": "object",
"properties": {
"orderId": {
"type": "string",
"pattern": "^ord_[A-Za-z0-9]{12}$"
}
},
"required": ["orderId"],
"additionalProperties": false
}
Test the schema against its meta-schema during build. Compile it using the same dialect and options as production. If format validation matters, configure and test it explicitly rather than assuming every validator treats formats as assertions.
Create positive, boundary, and negative examples
Every input contract needs at least:
valid: smallest intended request
valid: every optional field present
boundary: minimum and maximum values
invalid: each required property missing
invalid: wrong primitive type and null
invalid: empty, oversized, Unicode, and malformed identifiers
invalid: unknown property
invalid: mutually inconsistent fields
adversarial: path traversal, URL abuse, control characters, injection-shaped text
Generate some cases from the schema, but hand-write semantic boundaries. Schema-derived generation will not know that endDate must follow startDate unless you encode or separately test that rule. JSON Schema’s official use cases explicitly include contract, property-based, fuzzing, and invalid-instance generation. JSON Schema use cases
Do not feed unbounded generated values to live infrastructure. Run fuzzing against an isolated implementation with time, memory, request-size, and effect limits.
Test outputs as aggressively as inputs
Validate every success response before returning it to the model. Test errors as discriminated, machine-readable results:
type OrderResult =
| { readonly ok: true; readonly order: { readonly id: string; readonly status: string } }
| { readonly ok: false; readonly code: "NOT_FOUND" | "FORBIDDEN" | "UNAVAILABLE"; readonly retryable: boolean };
Check that:
- absent resources do not leak whether another tenant owns them;
- secret fields never appear, including in errors;
- partial responses are represented explicitly;
- retryability matches the failure class;
- errors do not contain stack traces or hostile upstream content; and
- output provenance is retained when the agent must cite or validate it.
Treat upstream text as untrusted. An indirect prompt injection returned by a search or MCP tool is data, not a new instruction. OWASP’s agent guidance recommends structured validation, least privilege, and adversarial tests for tool misuse and exfiltration. OWASP AI Agent Security Cheat Sheet
Put authorization behind the contract boundary
Test the broker that executes a tool, not only the handler:
principal + tenant + requested capability + arguments
→ authenticate
→ authorize exact resource and operation
→ validate input
→ enforce deadline and idempotency
→ execute in constrained environment
→ validate and redact output
→ append audit event
Build a permission matrix containing allowed and denied cases for roles, tenants, resource ownership, expired credentials, revoked capabilities, and approval state. For a destructive tool, assert the database or sandbox remains unchanged after every denial.
Model behavior is not a security control. Even if the system prompt says “never access another tenant,” the executor must enforce it.
Verify transport and failure semantics
Inject failures at each boundary:
- connection refusal and DNS failure;
- timeout before and after the server accepts work;
- malformed or truncated response;
- duplicate delivery;
- cancellation;
- rate-limit response with and without retry guidance;
- server restart; and
- success response arriving after the caller’s deadline.
For writes, distinguish retry-safe transport failures from unknown outcomes. Use an idempotency key bound to principal, operation, and normalized arguments when the service supports it. A retry policy must never assume that a timed-out write did not happen.
Test concurrency: two requests using the same idempotency key, conflicting updates, stale versions, and cancellation racing with completion.
Check producer-consumer compatibility
Store contract fixtures from both sides:
- provider tests prove the current implementation satisfies advertised schemas and semantics;
- consumer tests prove the agent runtime can discover, call, parse, and classify the tool; and
- compatibility tests run old consumers against the candidate provider and the candidate consumer against supported providers.
OpenAPI 3.1 bases its Schema Object on JSON Schema Draft 2020-12 while defining an OpenAPI dialect. Validate the full OpenAPI document and use the correct dialect; “valid JSON Schema” alone does not validate an API description. OpenAPI 3.1.1 specification, JSON Schema guidance for OpenAPI 3.1
Classify changes:
| Change | Typical compatibility risk |
|---|---|
| Add optional input | older provider may reject it; older consumer unaffected |
| Add required input | breaking for existing callers |
| Remove enum value | breaking if callers send it |
| Add output field | breaks consumers that incorrectly reject unknown fields |
| Change side effect or retryability | semantic break even if schema is unchanged |
Version behavior, not just shapes. A description change that makes the model choose a tool differently also deserves an agent evaluation.
Keep contract tests distinct from agent evals
Contract tests answer whether the tool boundary behaves as declared. Agent evals answer whether a stochastic system chooses and uses it correctly. Run contracts first; a broken tool makes agent scores uninterpretable. Then add selection, argument grounding, and recovery cases to the agent eval suite.
Contract-test checklist
- Declare and validate the schema dialect.
- Test valid, boundary, invalid, adversarial, and oversized instances.
- Validate success and error outputs before model exposure.
- Enforce authentication, tenant isolation, authorization, and approvals in code.
- Assert denied calls have no side effects.
- Inject timeout, duplicate, cancellation, partial, and restart failures.
- Define idempotency and unknown-outcome behavior for writes.
- Test supported producer-consumer version combinations.
- Treat description and semantic changes as versioned changes.
- Run all destructive tests in isolated fixtures.