Back to Blog
A closed gate representing a validation checkpoint in front of AI agent tool execution

AI Agent Tool Validation: A Gate Before Execution

9 min read

A model can pick the right tool and still send the wrong arguments. Malformed JSON can crash an orchestration loop. A valid customer ID may belong to another tenant. A retry may submit the same refund twice. AI agent tool validation stops these failures by treating each generated call as an untrusted proposal. The model proposes an action. Deterministic application code decides whether that action is complete, authorized, and safe to run.

The execution gate described here handles parsing, schema checks, trusted identity, business validation, authorization, confirmation, idempotency, repair feedback, and audit records. The final test matrix proves that rejected calls never reach a business API.

Why strict tool schemas are necessary but insufficient

Function tools give a model a name, description, and argument shape. The OpenAI function-calling guide documents JSON Schema definitions and strict argument generation. Anthropic likewise documents input schemas and requires supplied tool-use examples to conform to those schemas in its tool-use implementation guide.

Use those features. A strict schema prevents many syntax and type errors, but it does not make a proposed action trustworthy.

Consider a tool with this input:

{
  "type": "object",
  "properties": {
    "invoice_id": { "type": "string" },
    "status": { "enum": ["approved", "rejected"] },
    "reason": { "type": "string" }
  },
  "required": ["invoice_id", "status"],
  "additionalProperties": false
}

A generated call can match that schema and still be wrong. The invoice may not exist, may belong to another organization, or may already be paid. The current employee may lack approval authority. The reason may contain text copied from an untrusted document. Schema validation proves the shape of an argument object. It says nothing about object ownership, current state, permission, or the user's intent.

Malformed output remains possible too, especially when an integration relaxes strict mode, streams partial arguments, translates between providers, or accepts tools from several sources. A Woo AI Manager issue about malformed tool arguments reports an orchestration loop crashing because it called json.loads without handling invalid JSON. The proposed repair was to catch the parse failure, return a tool error to the model, and log the offending payload instead of terminating the request.

The execution gate has to handle both classes of failure: broken representation and valid-looking arguments that are unsafe in context.

Put one gate in front of every tool executor

Do not scatter these checks across prompts, tool descriptions, and API clients. Route every proposed call through one gate. The gate returns execute, repair, confirm, or deny. Only the first disposition reaches the tool adapter.

The MCP tools specification requires servers to validate inputs and apply access controls. It also recommends confirmation for sensitive operations, timeouts, result validation, and audit logging on the client side. Those are separate controls, but they belong in one call lifecycle.

Process each call in this order:

  1. Capture the raw proposal without executing it.
  2. Parse arguments with a size limit and error handling.
  3. Validate the parsed object against the registered tool schema.
  4. Attach identity and tenant context from the authenticated server session.
  5. Resolve referenced business objects under that trusted scope.
  6. Authorize the requested action against current policy.
  7. Classify its side-effect and confirmation requirements.
  8. Assign an idempotency key to any retryable write.
  9. Execute through a timeout-aware adapter.
  10. Validate and sanitize the result before returning it to the model.
  11. Record the final disposition and outcome.

The order matters. Object lookup and authorization happen before execution, and neither step trusts identity fields generated by the model.

Keep generated arguments separate from trusted context

Never let the model choose user_id, tenant_id, role, database connection, or authorization scope when the server already knows those values. A model can refer to a business object, but trusted runtime code must decide which objects are visible to the caller.

Suppose an employee asks an internal agent to approve invoice inv_481. The model may generate:

{
  "invoice_id": "inv_481",
  "status": "approved",
  "tenant_id": "tenant_blue"
}

The gate should reject tenant_id as an unexpected property. It should then resolve inv_481 using the tenant from the authenticated session, not the proposed value. If the invoice is outside that scope, return a generic denial. Do not reveal whether another tenant owns it.

This separation makes tests clearer. Generated data contains the proposed tool name and business arguments. Trusted context contains the authenticated subject, organization, role, request ID, approved scopes, and policy version. Tool code receives a merged internal command only after validation and authorization pass.

Dynamic tool selection can reduce exposure further. If a viewer cannot approve invoices, omit the approval tool from that run. Omission is not authorization, though. A tool list may be stale, and the executor still needs a server-side check. The LangChain tools documentation shows runtime tool filtering by user role, while MCP requires access controls at the server. Use both layers when the framework supports them.

Implement the gate as deterministic code

The following Python-like pseudocode keeps the control flow outside the model prompt.

def gate_tool_call(raw_call, session, registry, policy, store):
    tool = registry.get(raw_call.name)
    if tool is None:
        return Repair(code="unknown_tool", public_message="Choose an available tool.")

    try:
        args = parse_json_limited(raw_call.arguments, max_bytes=16_384)
    except ParseError:
        return Repair(code="invalid_json", public_message="Return valid JSON arguments.")

    errors = tool.input_schema.validate(args)
    if errors:
        return Repair(
            code="invalid_arguments",
            public_message=format_schema_errors(errors),
        )

    context = TrustedContext(
        subject=session.user_id,
        tenant=session.tenant_id,
        roles=session.roles,
        request_id=session.request_id,
    )

    objects = tool.resolve_objects(args, context)
    decision = policy.authorize(
        subject=context.subject,
        tenant=context.tenant,
        action=tool.action,
        objects=objects,
        arguments=args,
    )
    if not decision.allowed:
        return Deny(code="not_authorized", public_message="This action is not allowed.")

    if tool.requires_confirmation(args, objects):
        return Confirm(summary=tool.confirmation_summary(args, objects))

    command = tool.build_command(args=args, context=context, objects=objects)
    key = derive_idempotency_key(context.request_id, raw_call.id, command)
    return Execute(command=command, idempotency_key=key)

Keep the public error message separate from the internal diagnostic. The model needs enough information to repair a missing field or invalid enum. It does not need a stack trace, database query, hidden policy condition, or the identity of a protected object.

The OpenAI Agents SDK guardrails documentation distinguishes input, output, and tool guardrails and supports tripwires around execution. A framework hook is a useful enforcement point, but the policy and object checks should remain ordinary application code that can be tested without a model.

Decide when to repair, deny, or ask for confirmation

Invalid calls need different responses depending on why they failed.

Return repair for bounded, non-sensitive errors that the model can correct without guessing. Examples include malformed JSON, a missing required field already present in the conversation, an unsupported enum value, or an unknown tool name. Limit repair attempts. Repeatedly regenerating arguments can consume the request budget and produce a loop.

Return deny when the call violates authorization, names an inaccessible object, exceeds a hard business limit, or requests a prohibited action. Do not ask the model to negotiate with policy. The denial should be stable across retries unless trusted context changes.

Return confirm when the caller is authorized but the action is consequential or ambiguous. Show the user the normalized action, target object, and important effect. Confirmation should bind to the exact validated command. If arguments change after confirmation, require a new confirmation.

Return execute only after every required check passes. Attach an idempotency key to writes so the key survives network retries. When the executor sees that key again, it should return the stored result rather than repeat the side effect. This protects a workflow that times out without knowing whether the first request succeeded.

Handle failures without creating a repair loop

A tool call can fail before execution, during the downstream request, or after the downstream system commits a change. Those states require different recovery paths.

Before execution, parse and validation errors are safe to repair within a small attempt budget. Authorization failures are terminal for that call. Confirmation requests pause the workflow without consuming another model attempt.

During execution, classify errors by the tool contract. A timeout is not proof that no side effect occurred. Query by idempotency key or operation ID before retrying a write. A validation error from the downstream API may be repairable if it describes a public business rule. An internal server error should not be pasted into model context.

After execution, validate the result shape and sanitize content before returning it to the model. MCP recommends validating tool results and sanitizing outputs because tools can return malformed or unsafe content. Store the raw response only in a restricted diagnostic channel if operational policy permits it. Give the model the minimum result needed for the next workflow step.

Set hard ceilings for argument size, repair attempts, tool calls per run, execution time, and response size. These are ordinary reliability controls. They also prevent a malformed proposal from expanding into an expensive loop.

Record the proposal, decision, and outcome

An audit record should explain why a call did or did not run without storing every prompt or secret. Capture:

  • request, run, and proposed-call identifiers
  • authenticated subject and tenant identifiers
  • tool name and registered schema version
  • policy version and disposition
  • referenced business-object identifiers
  • confirmation identity and timestamp when required
  • idempotency key for writes
  • start time, outcome category, and downstream operation ID
  • redaction markers for fields omitted from the record

Do not log reusable credentials or unrestricted raw arguments by default. Store a normalized, redacted representation appropriate to the tool. A failed parser can record a bounded hash and error category instead of the entire malformed payload if that payload may contain sensitive text.

These records separate model mistakes from policy denials, user cancellations, suppressed duplicates, downstream failures, and completed actions. They also reveal tools whose schemas or descriptions cause repeated repairs.

Test the gate with negative cases

A happy-path test does not prove the boundary. Build a fixture matrix for each consequential tool. In every case, assert that the adapter was called only when the expected disposition was execute.

Include at least these cases:

  • malformed and truncated JSON
  • missing required fields and unexpected properties
  • wrong primitive types and invalid enum values
  • an object that does not exist in the trusted tenant
  • a valid object owned by another tenant
  • a user who can read but cannot write
  • a high-impact action that lacks confirmation
  • changed arguments after confirmation
  • a duplicate write with the same idempotency key
  • a downstream timeout after an ambiguous commit
  • malformed or oversized tool output
  • one fully valid read and one fully valid write

For every denied case, assert both sides of the boundary: the business adapter received zero calls, and the model-facing response disclosed no protected object or policy detail. For duplicate writes, assert that the second attempt returns the recorded result without repeating the operation.

Run these tests without a live model first. Then add a small model-backed suite with ambiguous requests and prompt-injected documents. The deterministic suite proves the gate. The model-backed suite measures how often proposals reach each disposition and whether repair feedback works.

Test one risky write before adding another

Start with the highest-risk write tool in your workflow. Route it through one AI agent tool validation function. Remove identity fields from the model schema, then add fixtures for malformed JSON, cross-tenant access, missing confirmation, and duplicate execution. Connect the next write tool only after those tests prove that every denied path leaves the first adapter untouched.

References


About Fire In Belly: Independent senior engineering from Tallinn, Estonia. We design and build AI workflow automation with the typed execution gates, trusted-context separation, and negative-case testing described above, at published fixed prices. Schedule a call to discuss your next project.