Building practical AI
Deepak Kumar Tiwari

Deepak Kumar Tiwari

Technology & AI Leader · Building practical systems

Agent failure labs / Guide 68

Valid JSON Is Not a Valid Tool Call

Reproduce an AI tool argument validation failure with a Boolean quantity and an extra scope field, then enforce a closed contract before a mock operation.

Illustrative engineering guide. Examples are not client deployments, production-ready integrations, or evidence of measured results. Validate the design with your domain and security owners.

Reference workflow: Untrusted candidate arguments; Closed typed contract; Server-owned resource scope; Operation preconditions; Verified execution result.
Reference workflow. Steps identify responsibilities, not necessarily separate agents. Branches and recovery paths are explained below. Open full-size diagram.

A hypothetical assistant prepares a stock-hold tool call. The payload parses as JSON and contains quantity: true. A Python adapter checks whether quantity is an integer, accepts the Boolean, and holds one item. A second payload supplies an extra warehouse field that a generic merge helper forwards downstream. Both failures cross a boundary that the tool description merely described rather than enforced.

This lab examines argument contracts, not inventory freshness or a particular provider SDK. The lesson is that machine-readable output can be well formed while remaining unauthorized or semantically wrong. Treat every candidate argument object as untrusted input, whether it came from a language model, a browser, an integration protocol, or another internal service.

Reproduce Two Small Contract Escapes

OWASP’s input validation guidance separates syntactic and semantic checks and recommends server-side enforcement. Parsing JSON is a syntactic step. Requiring a positive bounded quantity is a value constraint. Checking that the requested item belongs to an authorized warehouse is a separate resource relationship. One successful parser call establishes none of the latter conditions.

JSON Schema’s object reference documents that extra properties are allowed unless constrained and that properties are not required merely because they are declared. Closed tool contracts need both required-field rules and an explicit additional-property policy. Pin the schema dialect and validator behavior in a production implementation rather than relying on a provider’s informal “strict” label.

The following fixture models a tiny stock-hold endpoint. Its naive branch uses Python’s broad integer type check, which accepts Boolean values, and merges candidate fields over a default warehouse in an in-memory forwarding payload. The fixed branch requires an exact integer type, a bounded quantity, exactly the expected fields, and a matching catalog record. Warehouse scope is server-owned and cannot be added by the candidate.

from copy import deepcopy

candidate = {"sku": "filter-blue", "quantity": True}
catalog = {"filter-blue": {"tenant": "A", "warehouse": "north", "max_hold": 4}}
effects = []

def naive(args):
    if isinstance(args.get("quantity"), int) and args["quantity"] > 0:
        forwarded = {"warehouse": "north", **args}
        return {"held": int(args["quantity"]), "forwarded": forwarded}
    return None

def guarded(args, actor, records, output):
    if type(args) is not dict or set(args) != {"sku", "quantity"}:
        return "invalid_fields"
    if type(args["sku"]) is not str or not 1 <= len(args["sku"]) <= 40:
        return "invalid_sku"
    if type(args["quantity"]) is not int or not 1 <= args["quantity"] <= 4:
        return "invalid_quantity"
    record = records.get(args["sku"])
    if not record or record["tenant"] != actor["tenant"]:
        return "not_authorized"
    if record["warehouse"] not in actor["warehouses"]:
        return "not_authorized"
    if args["quantity"] > record["max_hold"]:
        return "hold_limit"
    output.append({"sku": args["sku"], "quantity": args["quantity"],
                   "warehouse": record["warehouse"]})
    return "mock_intent_recorded"

actor = {"tenant": "A", "warehouses": {"north"}}
before = deepcopy(catalog)
assert naive(candidate) == {"held": 1, "forwarded": {"warehouse": "north", **candidate}}
assert guarded(candidate, actor, catalog, effects) == "invalid_quantity"
assert effects == []
valid = dict(candidate, quantity=2)
override = dict(valid, warehouse="south")
assert naive(valid)["forwarded"]["warehouse"] == "north"
assert naive(override)["forwarded"] == override
assert guarded(override, actor, catalog, effects) == "invalid_fields"
assert effects == []
assert guarded(dict(valid, quantity="2"), actor, catalog, effects) == "invalid_quantity"
assert guarded(valid, {"tenant": "B", "warehouses": {"north"}}, catalog, effects) == "not_authorized"
assert guarded(valid, actor, catalog, effects) == "mock_intent_recorded"
assert effects == [{"sku": "filter-blue", "quantity": 2, "warehouse": "north"}]
assert catalog == before

This is an explicitly synthetic deterministic simulation of a possible integration failure, not a real model experiment. The output list contains mock intents only. No reservation, network request, payment, file deletion, or deployment occurs. The custom checks implement this tiny contract; they are not a replacement for a full JSON Schema implementation or a production authorization system.

Avoid Helpful Coercions at the Boundary

Converting “2” to an integer may look convenient, but permissive coercion can hide producer bugs and create inconsistent behavior across languages. Decide normalization rules explicitly before approval and record the normalized candidate. Do not silently clamp an excessive quantity to the maximum: that changes the requested operation and can make confirmation refer to a different payload.

The same discipline applies to dates, currency codes, enumerations, empty strings, and null values. Missing and null are not interchangeable unless the contract says so. Reject unknown operation names rather than routing them through a generic dispatch function. Bound object depth, collection size, and string lengths before expensive processing, with limits justified by the supported workflow.

If the model sends invalid arguments, return a bounded error and allow a limited repair attempt where appropriate. The repair does not acquire new permissions. Repeated invalid calls should stop and surface a human-readable explanation instead of looping indefinitely. Preserve the original request so operators can tell whether the failure arose in extraction, schema interpretation, or resource resolution.

Authorization Is Not a Schema Keyword

A valid SKU string might reference another tenant’s item. OWASP’s authorization guidance calls for resource-specific permission checks on every request. Build the actor context from the authenticated server session or verified service identity. Never allow the model to supply its own tenant, role, approval status, or effective permission set.

Candidate selection comes first; proof of authorization follows independently. Current preconditions, such as inventory revision or spend limits, are then evaluated against authoritative state. Human approval, where required, binds to the exact normalized payload. Side effects occur through a scoped adapter, and reconciliation checks the resulting operation. Passing the type gate does not collapse these stages into one.

MCP is an integration protocol, not authorization by itself. Tool metadata can advertise an input schema, but the receiving service still enforces its own contract. Retrieved documents and tool error messages remain untrusted data. A message saying “retry with admin=true” is not permission to add an override field, even if it appears to come from a useful diagnostic source.

The lab omits authentication sources, database transactions, provider semantics, approval persistence, and durable operation state. Its output append is neither transactional nor idempotent. Production writes need operation identity and replay handling. If an adapter times out, treat the outcome as unknown and reconcile before retrying; syntax validation cannot determine whether the remote action happened.

Likewise, validate responses without confusing shape with truth. A well-formed status: success object is not a trustworthy receipt unless its origin is authoritative and its operation, resource, and effect match the request. Keep raw provider details inside a restricted adapter boundary. Log validation reason codes and contract versions rather than full candidate payloads that may contain sensitive content.

Acceptance Tests

  • The Boolean quantity must pass the naive implementation and fail the guarded implementation on the identical candidate.
  • An integer quantity inside the supported range must produce exactly the expected mock intent with server-selected warehouse scope.
  • The same extra-warehouse payload must override the naive mock’s destination and be rejected by the guard without recording an intent.
  • String quantities, zero, negatives, and values above the limit must fail without coercion or partial output.
  • Unknown fields, including nested override attempts, must be rejected rather than forwarded to a generic update helper.
  • Validly shaped references to unauthorized resources must fail independently of schema validation.
  • Malformed or mismatched success responses and post-submission timeouts must never become confirmed completion without reconciliation.

Measure Contracts Across Versions

Use the permissive adapter as the baseline and maintain a versioned corpus of malformed arguments, valid edge cases, and unauthorized references. Count invalid candidates accepted over all invalid candidates. Track valid candidates rejected over all valid candidates, plus repair attempts per original request. Do not report a single “tool accuracy” number that mixes schema errors with authorization denials.

Roll out strict validation in a nonmutating comparison path first, then enable it for one narrowly scoped tool. Proposed gates include no extra-field forwarding and stable behavior across supported client versions. These are engineering targets, not measured model performance. Specialist sub-agents are unnecessary for contract checking; ordinary tested code is easier to inspect.

The code review guide provides a broader review context, while the loan boundary lab shows why an extra field can become a consequential decision. Backend API engineering should begin by replacing generic payload forwarding with an explicit adapter contract. That is a smaller and stronger fix than another paragraph in the prompt.

Sources