Back to Blog
Engineering

Block Unsafe AG2 Tool Calls Before They Run

governed_functions wraps the functions you pass to register_function. A refused call never runs.

Block unsafe AG2 tool calls before they run. governed_functions checks each call: add is approved, an undeclared divide is blocked.

October 28, 20269 min read
AG2governed_functionsConversableAgentpre-check
An undeclared divide tool call from an AG2 agent reaches the governed_functions gate and is blocked; add is approved
One AG2 tool call: divide is blocked and add is approved
TL;DR
  • Install agentrust-py[ag2] and wrap the tool functions once.
  • Declare add and multiply. Leave divide off the manifest.
  • Run --without-agentrust, then --local, then --console. Add --scripted for no model and no key.
  • Register the wrapped functions with register_function.
Keep reading for the problem, how the check works, and the full walkthrough →

The governed_functions helper checks every AG2 tool call with AgentTrust before the tool runs. Wrap your agent's tools once, then run a small calculator agent with and without AgentTrust and watch a call get refused.

The agent asks. The gateway answers. Only then does the function run.
THE PROBLEM

The problem: an AG2 agent can call any function it is registered with

The model chooses which tool to call and with what arguments. In this guide's calculator agent, add and multiply are the intended tools, yet the model can also call divide. Run the agent with no check and divide goes through: nothing stopped it. For a calculator that is harmless. Swap divide for issue_refund or delete_user and it would run just the same.

Without a check divide runs though it is not declared. With governed_functions add is approved, divide is blocked and the run still finishes
The same agent, without and with the check.
THE FIX

The fix: governed_functions checks each call before the tool runs

AG2 decides which tools to call, and nothing stops it from calling one it should not. The AgentTrust helper governed_functions wraps each tool so the call goes to AgentTrust first. By the end of this guide:

  • Every tool call is checked before it runs, and its result after.
  • A refused call never runs. The agent gets a plain Blocked by AgentTrust result as the tool's output and the run still finishes.
  • Every decision, allowed or refused, shows under Events in your console.

Prerequisites. Python 3.10 or later. An AG2 agent (the ag2 package, 0.x, which keeps the ConversableAgent API, developed in the AG2 repository) whose agents use tools. AG2 1.x is a different API and is not covered. The walkthrough needs an OpenAI key unless you add --scripted. The local gateway needs no account; the console route does. You can also run the whole walkthrough with --scripted, which replays a fixed conversation and needs no model and no key.

Package name. Install agentrust-py and import agentrust_sdk. The PyPI package agentrust-sdk belongs to another company.

HOW IT WORKS

Why the check sits where AG2 functions are registered

Five steps of one AG2 tool call: the executor gets the call, the wrapper asks the gateway, the gateway compares and records, then the function runs or is refused
One tool call from request to decision.

In an AG2 agent, a tool call crosses two places. register_function tells the calling agent which functions exist, and the executor agent runs the one the model picks. The wrapper goes on the function before it is registered, so the check runs on every call the executor makes.

What happens on one tool call

  1. Intercept. The wrapped function receives the call. Your tool's code has not run.
  2. Ask. The wrapper sends the tool name and its arguments, with the agent id, to the AgentTrust gateway. This is the pre-check.
  3. Compare. The gateway checks the call against the agent's declared tools. A tool that is not declared, or an amount above its declared limit, is refused by the tool-trust hard gate.
  4. Record. The gateway writes an event for the decision, allowed or refused.
  5. Run or refuse. An allowed call runs, and its result goes back to the gateway for a post-check, a second event. A refused call never runs. The agent receives Blocked by AgentTrust as the tool's result.

What the wrapper must cover

The check covers only the tools you wrap. A tool you register unwrapped is not checked, so wrap every tool the agent has. Give each agent its own agent_id so the audit trail says who acted. The manifest matters too: an agent with no declared tools is not restricted, so every tool is allowed.

SET UP

Add governed_functions to your AG2 agent

The helper works with any AG2 ConversableAgent that calls registered functions. These steps connect to your console. With no account, use the local gateway below instead.

Install the adapter

pip install "agentrust-py[ag2]"

It needs AG2 0.x (ag2>=0.9,<1.0), which provides the autogen module. Microsoft's own AutoGen 0.4+ is a different package: see Block Unsafe AutoGen Tool Calls Before They Run.

Register the agent

In your console, select New agent and copy its API Key and Agent ID. The key starts with at_live_ and is shown once. Select the agent, then under Manifest & fingerprint select Declare and add the tools it may use, with an optional limit for a tool that moves money, such as issue_refund up to 100.

Do not skip the manifest.

An agent with no declared tools is not restricted: every tool is allowed.

Point your app at the console

export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech
export AGENTRUST_KEY=at_live_...
export AGENTRUST_AGENT_ID=support-agent

Wrap the tools

Where: in the one place your agent's tool functions are registered. Wrap the functions first, then pass the wrapped ones to register_function. ConversableAgent itself does not change.

+ from agentrust_sdk.adapters.autogen import governed_functions
+ tools = governed_functions(
+     tools, agent_id=os.environ["AGENTRUST_AGENT_ID"]
+ )

Your functions do not change. The wrapped function keeps its name, docstring and signature, so AG2 builds the same tool schema for the model. Async functions stay async.

When the agent calls a tool the manifest does not allow, or one over its limit, the tool does not run. The agent receives this as the tool's result and can explain or try something else:

Blocked by AgentTrust: Tool-trust hard gate: tool_trust: 'issue_refund' amount 500.0 exceeds declared max_amount 100.0 (envelope …)

Stop the whole run instead. To raise an error on a block instead of letting the agent read the refusal, pass on_block="raise". AgentTrust also offers AgentTrustReplyHook to check each message an agent sends, and AgentTrustGroupChatMonitor for group chats; they are separate from the tool wrapper.

No account? Connect to a local gateway

Skip steps 2 and 3 and start a gateway inside your program before the agent's first tool call:

pip install "agentrust-py[ag2,embedded]"
from agentrust_sdk.embedded import embed_gateway

gateway = embed_gateway()
gateway.register_manifest("support-agent", {
    "declared_tools": [
        {"name": "lookup_order"},
        {"name": "issue_refund", "max_amount": 100},
    ],
})

Set AGENTRUST_AGENT_ID=support-agent (or pass the ID straight to governed_functions) and go to step 4. Decisions last only while the program runs, so use this to try AgentTrust, in tests and in CI.

SEE IT BLOCKED

Run it and watch an AG2 tool call get blocked

The same four steps, applied to a small calculator agent. You run it three ways and watch a tool call get refused. About 10 minutes. Each step has one thing to do and one thing you should see.

Before you start. Python 3.10 or later. Steps 1 to 4 need nothing else when you add --scripted to each command; without it they need an OpenAI key (OPENAI_API_KEY). Steps 5 to 7 use an AgentTrust console account.

Create the project

Make a folder with its own Python environment and install what the program needs:

mkdir agentrust-quickstart && cd agentrust-quickstart
python -m venv .venv && source .venv/bin/activate     # on Windows: .venv\Scripts\activate
pip install "agentrust-py[ag2,embedded]"

Save the program below as ag2_quickstart.py. It is the example examples/ag2_quickstart.py from the AgentTrust repository.

"""
ag2_quickstart.py — an AG2 agent whose tool calls AgentTrust checks.

The agent is a calculator: one AG2 (AutoGen 0.2 API) agent with add, multiply
and divide tools, asked to do two sums.

AgentTrust is one line (marked "AgentTrust" below): the tools are wrapped with
governed_functions before they are registered. Every call is checked before it
runs, and its result after. A tool the agent may not use is refused before its
code runs; the agent is told why as the tool's result and the chat still
finishes. The agent may use add and multiply, not divide.

RUN IT (in the folder that holds this file)
    pip install "agentrust-py[ag2,embedded]"
    export OPENAI_API_KEY=...                             # or use --scripted, which needs no key

    python ag2_quickstart.py --without-agentrust          # AgentTrust off: every tool runs
    python ag2_quickstart.py --local                      # a gateway starts inside this program; no account
    python ag2_quickstart.py --console                    # your AgentTrust console keeps every decision
    python ag2_quickstart.py --scripted                   # replay a fixed conversation instead of calling a model

  --console needs three settings, from the agent you create in the console:
      export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech
      export AGENTRUST_KEY=at_live_...
      export AGENTRUST_AGENT_ID=...

ANOTHER MODEL
    AG2_QUICKSTART_MODEL=gpt-4o (export OPENAI_API_KEY). Any model AG2 supports
    through llm_config works; the model must support tool calling.
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import tempfile
from types import SimpleNamespace

from autogen import ConversableAgent, register_function

from agentrust_sdk.adapters.autogen import governed_functions

AGENT_ID = os.environ.get("AGENTRUST_AGENT_ID", "calculator-agent")
MODEL = os.environ.get("AG2_QUICKSTART_MODEL", "gpt-4o-mini")

# The tools this agent may call. With --local they are set here; with --console
# you declare the same list on the agent in the console. divide is left out on purpose.
MANIFEST = {"declared_tools": [{"name": "add"}, {"name": "multiply"}]}


def multiply(a: int, b: int) -> int:
    """Multiply `a` and `b`."""
    return a * b


def add(a: int, b: int) -> int:
    """Add `a` and `b`."""
    return a + b


def divide(a: int, b: int) -> float:
    """Divide `a` by `b`."""
    return a / b


# AgentTrust: every call to these tools is checked before it runs and after it
# returns. Register the result with the agent, not the plain functions.
tools = governed_functions([add, multiply, divide], agent_id=AGENT_ID)

REQUESTS = ["Add 3 and 4.", "Divide 10 by 2."]


def build_agents(llm_config) -> tuple[ConversableAgent, ConversableAgent]:
    """The calculator that asks for tools and the executor that runs them."""
    calculator = ConversableAgent(
        "calculator",
        system_message=(
            "You do the arithmetic you are asked for, using your tools, one tool call at a time. "
            "If a tool is refused, say the operation could not be done. "
            "Reply with the answer and the word DONE."
        ),
        llm_config=llm_config,
        human_input_mode="NEVER",
    )
    executor = ConversableAgent(
        "executor",
        llm_config=False,
        human_input_mode="NEVER",
        is_termination_msg=lambda m: "DONE" in (m.get("content") or ""),
    )
    for tool in tools:
        register_function(
            tool, caller=calculator, executor=executor, name=tool.__name__, description=tool.__doc__,
        )
    return calculator, executor


def llm_config(scripted: bool = False) -> dict:
    # AG2 builds its OpenAI client when the agent is created, so even the scripted
    # run needs some key to be set; it is never sent anywhere.
    key = "scripted" if scripted else os.environ.get("OPENAI_API_KEY", "")
    return {"config_list": [{"model": MODEL, "api_key": key}]}


def ask(request: str, config) -> str:
    calculator, executor = build_agents(config)
    result = executor.initiate_chat(calculator, message=request, max_turns=6, silent=True)
    return (result.summary or "").replace("DONE", "").strip()


# --scripted: replay a fixed conversation, so the quickstart runs with no model and no key.
# A replay does not read tool results, so its last line is the same with AgentTrust on or off;
# what differs is what each tool returned.
# Each reply is either a tool call (name, arguments) or the final text.
SCRIPT = [
    ("add", {"a": 3, "b": 4}), "3 + 4 = 7. DONE",
    ("divide", {"a": 10, "b": 2}), "That is everything you asked for. DONE",
]


def use_scripted_model(replies=SCRIPT) -> None:
    from autogen.oai.client import OpenAIWrapper

    queue = list(replies)

    def create(self, **kwargs):
        reply = queue.pop(0)
        if isinstance(reply, str):
            message = {"role": "assistant", "content": reply}
            calls = None
        else:
            name, args = reply
            calls = [{"id": "call-1", "type": "function",
                      "function": {"name": name, "arguments": json.dumps(args)}}]
            message = {"role": "assistant", "content": None, "tool_calls": calls}
        msg = SimpleNamespace(content=message["content"], tool_calls=calls, function_call=None,
                              role="assistant", model_dump=lambda: message)
        return SimpleNamespace(
            choices=[SimpleNamespace(message=msg, finish_reason="stop")], model="scripted", cost=0, id="r",
            usage=SimpleNamespace(prompt_tokens=0, completion_tokens=0, total_tokens=0),
        )

    OpenAIWrapper.create = create
    OpenAIWrapper.extract_text_or_completion_object = classmethod(
        lambda cls, r: [r.choices[0].message.model_dump()] if r.choices[0].message.tool_calls
        else [r.choices[0].message.content]
    )


def ask_team(where: str, scripted: bool) -> bool:
    """Ask the agent to add, then to divide. False if the model can't be reached."""
    print(f"AgentTrust: {where} · agent {AGENT_ID} · model {'scripted' if scripted else MODEL}")
    if scripted:
        use_scripted_model()
    try:
        for request in REQUESTS:
            print(f"\n=== {request} ===")
            print(f"\nResult: {ask(request, llm_config(scripted))}")
    except Exception as exc:  # noqa: BLE001 - any provider error ends the run
        print(f"\nThe model call failed ({MODEL}): {exc}\n"
              "Check OPENAI_API_KEY is set, or run with --scripted.", file=sys.stderr)
        return False
    return True


# --without-agentrust: every tool runs. For comparison.
def run_without_agentrust(scripted: bool) -> int:
    os.environ["AGENTRUST_ENABLED"] = "false"   # the SDK's off switch: wrapped tools pass every call through
    return 0 if ask_team("off (every tool runs)", scripted) else 1


# --local: a gateway runs inside this program; no account needed.
def run_local(scripted: bool) -> int:
    from agentrust_sdk.embedded import embed_gateway

    # SQLite in a temp dir, gone after the run. embed_gateway sets
    # AGENTRUST_GATEWAY_URL and AGENTRUST_KEY, which governed_functions reads.
    gateway = embed_gateway(db_path=os.path.join(tempfile.mkdtemp(), "gateway.db"))
    gateway.register_manifest(AGENT_ID, MANIFEST)
    try:
        ok = ask_team(f"local gateway {gateway.url} (no account, nothing kept)", scripted)
    finally:
        gateway.stop()
    return 0 if ok else 1


# --console: the SDK sends every check to your console, which keeps the decisions.
def run_with_console(scripted: bool) -> int:
    missing = [v for v in ("AGENTRUST_GATEWAY_URL", "AGENTRUST_KEY", "AGENTRUST_AGENT_ID") if not os.environ.get(v)]
    if missing:
        print(f"Set {', '.join(missing)} (from the agent you created in the console), or use --local.",
              file=sys.stderr)
        return 2
    console = os.environ["AGENTRUST_GATEWAY_URL"]
    ok = ask_team(f"console {console}", scripted)
    print(f"\nOpen Events in the console ({console}) and filter by agent ID {AGENT_ID}.")
    return 0 if ok else 1


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="The AG2 calculator agent, governed by AgentTrust.")
    where = parser.add_mutually_exclusive_group()
    where.add_argument("--local", action="store_true", help="run a gateway inside this program, no account (the default)")
    where.add_argument("--console", action="store_true", help="send every check to your AgentTrust console")
    where.add_argument("--without-agentrust", action="store_true", help="for comparison: AgentTrust off, every tool runs")
    parser.add_argument("--scripted", action="store_true", help="replay a fixed conversation: no model, no API key")
    args = parser.parse_args(argv)
    if args.without_agentrust:
        return run_without_agentrust(args.scripted)
    return run_with_console(args.scripted) if args.console else run_local(args.scripted)


if __name__ == "__main__":
    sys.exit(main())

Run it without AgentTrust

First, see what the agent does on its own:

python ag2_quickstart.py --without-agentrust --scripted

The agent is asked to add, then to divide. The scripted agent replays a fixed conversation and does not read tool results, so its last line is the same with AgentTrust on or off. What differs is what each tool returned.

AgentTrust: off (every tool runs) · agent calculator-agent · model scripted

=== Add 3 and 4. ===
>>>>>>>> EXECUTED FUNCTION add...
Output:
7
Result: 3 + 4 = 7.

=== Divide 10 by 2. ===
>>>>>>>> EXECUTED FUNCTION divide...
Output:
5.0
Result: That is everything you asked for.

The agent used divide and nothing stopped it. For a calculator that is harmless. Swap divide for issue_refund or delete_user and it would run just the same.

Where governed_functions sits in the AG2 file

This is the only change AgentTrust needs, and it is already in your file, so there is nothing to edit. Find the governed_functions line in the program, near the top. It sits right after the tools are defined and before the agent is built. On your own agent it looks like this:

+ from agentrust_sdk.adapters.autogen import governed_functions
- tools = [add, multiply, divide]
+ tools = governed_functions([add, multiply, divide], agent_id=AGENT_ID)

Names, descriptions and argument types stay the same, so the agent sees what it saw before. A tool you register unwrapped is not checked. Every tool call now goes through AgentTrust first. Allowed calls run and their results are checked. Refused calls never run.

Run it locally

Now run the same agent with AgentTrust on:

python ag2_quickstart.py --local --scripted

The program starts a small AgentTrust gateway inside itself and allows only the tools in MANIFEST: add and multiply. divide is not on the list. No account needed.

AgentTrust: local gateway http://127.0.0.1:8765 (no account, nothing kept) · agent calculator-agent · model scripted

=== Add 3 and 4. ===
>>>>>>>> EXECUTED FUNCTION add...
Output:
7
Result: 3 + 4 = 7.

=== Divide 10 by 2. ===
>>>>>>>> EXECUTED FUNCTION divide...
Output:
Blocked by AgentTrust: Tool-trust hard gate: action 'divide' is
not in this agent's declared_tools (envelope
ca333621-ea2b-4602-983d-8a8e942a1e7b)
Result: That is everything you asked for.

add ran. divide was refused before its code ran, and the agent was told why, so the run still finished. Nothing is kept after the program exits.

Create the agent in your console

To keep decisions and manage the agent from the console, register it there. In your AgentTrust console:

  1. Open Agents, select New agent, name it ag2-quickstart, and select Create.
  2. Copy the API Key and the Agent ID before you select Done. The key starts with at_live_ and is shown only once.
  3. Select the agent in the list. In the panel that opens, under Manifest & fingerprint, select Declare. Under Declared tools, select + Add tool twice, type add and multiply, and select Save manifest.

Step 3 is the same list MANIFEST holds in the program, now kept in the console.

Do not skip the manifest.

An agent with no declared tools is not restricted. Every tool is allowed, so divide would run.

Run it against the console

Give the program the three values from step 5, then run it with --console:

export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech   # your console's address
export AGENTRUST_KEY=at_live_...                                # the API key
export AGENTRUST_AGENT_ID=ag2-quickstart                     # the Agent ID

python ag2_quickstart.py --console --scripted

The first line names the console, and the run should match step 4: add returns 7 and divide is refused with the same Blocked by AgentTrust text. The last line tells you where to look. Drop --scripted to use a real model.

See the decisions in the console

Open Events in the console and type your Agent ID into Filter by Agent ID. You will see one row per decision: add approved before and after it ran, and divide blocked. Select a row to see the scores behind the decision.

AgentTrust Events filtered to langgraph-quickstart, with one Block row and two Approve rows
Events. This capture is from the LangGraph guide, filtered to agent langgraph-quickstart. The Events table looks the same for AG2: one Block row and two Approve rows.

You are done. Your agent's tool calls are now checked, refused when they should not run, and recorded.

Wrap the functions first. A function you register unwrapped is not checked.
LIMITS

What the AG2 check covers and what it doesn't

The same agent, without and with the one change: wrapping its tools.

Your agent can…WithoutWith
Be limited to the tools you allow (you ran this)✗✓
Have a call stopped before it runs (you ran this)✗✓
Tell the agent why a call was refused, and keep running (you ran this)✗✓
Leave a record of every decision, with scores✗✓
Have a tool's result checked before the agent sees it✗✓
Cap risky amounts, such as a refund over 100✗✓
Hold a risky call until a person approves it✗✓
Be stopped instantly with a kill switch✗✓
Refuse every call if AgentTrust can't be reached (optional)✗✓

✗ means AG2 does not provide it, so you would build it yourself. The first three rows are what you just ran. The others are covered by the SDK's tests (the AG2 tests run real ConversableAgent agents against a real gateway) and the adapter guide (docs/framework-adapters/autogen.md), not run here.

Related checks

MechanismWhat it checks
governed_functionsEach tool call before it runs, and its result after
AgentTrustReplyHookEach message an agent sends. Attach it with AgentTrustReplyHook(agent_id=...).attach(agent).
AgentTrustGroupChatMonitorGroup chats. Separate from the tool wrapper.
NEXT STEPS

Next steps after the AG2 tool check

Hold risky calls for a person. Set the agent's mode to Human Review. Rejected or timed-out calls do not run. See docs/framework-adapters/autogen.md.

Check every message too. Add AgentTrustReplyHook(agent_id=...).attach(agent). It checks each message an agent sends. See docs/framework-adapters/autogen.md.

Put @harness on plain Python tools in Stop Risky AI Agent Tool Calls in Python with @harness, or build a full governed bot in Guide: Stop Your AI Support Bot from Issuing a Bad Refund.

FREQUENTLY ASKED QUESTIONS

Common questions

AG2 0.x (ag2>=0.9,<1.0), which keeps the ConversableAgent API. AG2 1.x is a different API and is not covered.

No. AG2 is the fork that keeps the old ConversableAgent API. Microsoft's autogen-agentchat 0.4+ is a different package and uses governed_tools. See the AutoGen guide.

It is not checked. Wrap every tool the agent has before you pass it to register_function, and give each agent its own agent_id so the audit trail says who acted.

A plain Blocked by AgentTrust result as the tool's output, and the run still finishes. Pass on_block="raise" to raise an error instead.

No. Add --scripted to replay a fixed conversation with no model and no key, and use the local gateway, which needs no account. The console route needs an AgentTrust console account.

READY TO GOVERN YOUR AGENTS?

No AI agent enters production without AgentTrust

Check every tool call before it runs, and keep a record of every decision.

See Pricing & Start Free →

More from the blog

EngineeringOctober 27, 2026EngineeringOctober 27, 2026EngineeringOctober 27, 2026EngineeringOctober 27, 2026EngineeringOctober 28, 2026AI ComplianceJuly 22, 2026AI ComplianceJuly 22, 2026AI GovernanceJuly 22, 2026AI GovernanceJuly 22, 2026AI ArchitectureJuly 22, 2026AI SecurityJuly 16, 2026MLOpsJuly 10, 2026AI ImplementationJuly 8, 2026AI Agent ArchitectureJuly 5, 2026AI Agent ArchitectureJune 30, 2026EngineeringJune 23, 2026AI Agent ArchitectureJuly 2, 2026AI Agent ArchitectureJuly 1, 2026IntegrationsJuly 1, 2026IntegrationsJuly 1, 2026IntegrationsJuly 2, 2026AI SecurityJuly 3, 2026AI SecurityJuly 1, 2026AI ComplianceJuly 2, 2026AI ComplianceJuly 3, 2026AI StrategyJuly 2, 2026AI StrategyJuly 3, 2026AI StrategyJuly 3, 2026AI StrategyJuly 3, 2026AI GovernanceJuly 28, 2026Healthcare AIJuly 28, 2026ArchitectureJuly 29, 2026ArchitectureJuly 29, 2026AI StrategyJuly 29, 2026AI StrategyJuly 30, 2026AI SecurityJuly 30, 2026AI ComplianceJuly 30, 2026EngineeringJuly 30, 2026AI GovernanceAugust 4, 2026EngineeringAugust 4, 2026EngineeringAugust 4, 2026Agent SecurityAugust 19, 2026AI ComplianceSeptember 4, 2026EngineeringSeptember 4, 2026AI StrategySeptember 4, 2026AI GovernanceSeptember 4, 2026