AI agents

Validate AI agent tool calls before they run.

An agent is one wrong tool call away from deleting data, emailing the wrong list, or spending money. Put a policy check between the model’s decision and the code that executes it.

Where the check goes in an agent loop

The model returns a tool call; your code executes it. The check goes in between. That position is what makes it reliable: no matter how the model was convinced to propose the call, through a confusing request or instructions injected in a web page, the call itself is evaluated against your rule before anything happens.

Return the outcome to the model as the tool result. A clear “blocked: needs user confirmation” lets the agent explain itself or ask, instead of retrying blindly.

Where the check fits

One call between the step that prepares something and the step that sends, publishes, or runs it. Your code applies the decision.

See it decide

The same policy evaluated on three cases. Pick one and run the check to see which evidence it relies on.

Policy

Destructive database operations must do exactly what the user asked for in data.conversation. Deny any operation that would touch more or different data than the user described. If the user’s request is ambiguous about what will be deleted, ask the user first.

data

{
"action": {
"tool": "database.execute_sql",
"args": {
"query": "DELETE FROM orders WHERE customer_email = 'qa@ourco.dev' AND created_at > current_date"
}
},
"conversation": [
{
"role": "user",
"content": "Delete the test orders I placed this morning with the QA account."
}
],
"environment": "production"
}

Result

violationProbability—
reviewProbability—

Why: The delete is limited to the QA account’s orders from today, which is what the user asked for.

Your code: Execute the tool.

Response JSON
{
  "decision": "allow",
  "allowed": true,
  "violationProbability": 0.04,
  "reviewProbability": 0.05,
  "policyId": null,
  "policyVersion": null,
  "requestId": "req_7f3a…"
}

Illustrative example. Real probabilities depend on your policy wording and the evidence you send.

allow · Matches the request

The delete is limited to the QA account’s orders from today, which is what the user asked for.

deny · Broader than asked

Real customers signed up yesterday too. Nothing in the query limits it to test accounts, so it deletes far more than “test data”.

review · Ambiguous delete

The delete is plausible but “old stuff” doesn’t say which rows, and 1,240 rows aren’t confirmed.

What evidence to send

A tool call alone is rarely enough to judge. Send what a careful reviewer would want to see:

  • The exact call: tool name and arguments, after the model filled them in.
  • The conversation, or at least the user turns that led here. Confirmations live there.
  • Environment and impact: production or staging, number of affected rows, recipients, amount.
  • Who is asking: the user’s role, if your rules depend on it.

Review means “ask”, not “skip”

Enforly keeps no state between checks. When a call comes back review, ask the user to confirm that specific operation, then check the original call again with the confirmation in the conversation. Don’t send the word “yes” on its own as a new check.

Set it up

  1. 1

    List your risky tools

    Database writes, payments, emails, file deletion, external APIs. Write one policy per risk, or one per tool.

  2. 2

    Wrap execution

    Before executing a tool, call guard.check with the call and the conversation.

  3. 3

    Feed the result back

    Execute on allow. Otherwise return a tool result that explains the refusal or asks for confirmation.

Generic agent loop
import { Enforly } from '@enforly/sdk'

const guard = new Enforly({ apiKey: process.env.ENFORLY_API_KEY })

for (const call of response.toolCalls) {
  const result = await guard.check({
    policyIds: [DESTRUCTIVE_OPS_ID, DATA_SHARING_ID],
    data: { action: { tool: call.name, args: call.arguments }, conversation, environment: 'production' },
  }).catch(() => ({ decision: 'deny', allowed: false })) // never fail open

  const output = result.allowed
    ? await tools[call.name](call.arguments)
    : { blocked: true, decision: result.decision, note: result.decision === 'review'
        ? 'Ask the user to confirm this exact operation.'
        : 'Not allowed by policy. Propose a safer alternative.' }

  messages.push({ role: 'tool', toolCallId: call.id, content: JSON.stringify(output) })
}

Full request and response fields are in the API reference. Using a coding agent? Point it to Build with AI.

What Enforly doesn’t do

Enforly evaluates the information you send and returns a decision. It doesn’t run operations, and it doesn’t guarantee safety or compliance on its own.

  • Enforly evaluates the call you send; it doesn’t execute tools or roll them back.
  • It isn’t a sandbox or a permission system. Keep least-privilege credentials for your agent.
  • A check adds one network call per tool call. Skip it for tools that are always safe, like reading the time.

Questions

Does this protect against prompt injection?

It doesn’t detect injected text. It evaluates the resulting tool call against your policy, so an injected “delete everything” is still judged as a deletion without confirmation.

Which agent frameworks does it work with?

Any. It’s an HTTP call or an SDK call before execution. See the AI SDK guide and the n8n guide.

Should I check every tool call?

Check the ones with side effects. Read-only tools that can’t leak data usually don’t need it.

Related

Try this policy on your own data.

Copy the policy above, add your real examples, and see the decision. 200 free checks per month, no credit card.