SDK

Harness Decorator

Put @harness on each tool. Every call is checked before it runs.

Harness Decorator

Guide · Python · @harness

Put one decorator, @harness, on each function your agent uses as a tool, and every call is checked before it runs. Then run a small support assistant with and without AgentTrust and watch two calls get refused.

What you will build

An agent decides which tools to call, and nothing stops it from calling one it should not. @harness is a decorator from the AgentTrust Python SDK. Before the function runs, it asks AgentTrust whether this call is allowed. By the end of this guide:

  • Every tool call is checked before it runs, and its result after.

  • A refused call never runs. Your code gets a BlockedError with the reason, and can tell the model or the user.

  • Every decision, allowed or refused, shows under Events in your console.

Prerequisites. Python 3.10 or later and an agent whose actions are Python functions. The "See it in action" walkthrough needs no model and no API key. The local gateway needs no account; the console route needs an AgentTrust console account. Package name. Install agentrust-py and import agentrust_sdk. The PyPI package agentrust-sdk belongs to another company.

Connect AgentTrust in 4 steps

These steps connect to your console. With no account, use the local gateway instead.

Install the SDK

Terminal: run this

pip install agentrust-py

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

Terminal: run this

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

Decorate the tools

Where: on each function the agent calls as a tool, in the one place those functions are defined. How: one import and one decorator per function, with the agent's ID.

support tools

+ from agentrust_sdk import BlockedError, harness
 
+ @harness(agent_id="support-assistant")
  def issue_refund(order_id, amount, user, input):
      return {...}

Three things to know.

  1. The tool's name is its function name. It must match a tool declared on the agent, or the call is refused.

  2. Pass user and input when you call a tool. They are who is asking and what they asked, and AgentTrust reads both. A call without input is refused. The function must accept them.

  3. Connect before the decorator runs. @harness reads the gateway address when the function is defined, so set the variables (or start the local gateway) before the def.

Catch BlockedError where your agent calls the tool, and say why:

Python

try:
    result = issue_refund(order_id="O-1001", amount=5000, user=user, input=message)
except BlockedError as blocked:
    result = f"Refused: {blocked.reason}"

When the agent calls issue_refund with an amount over 100, the function does not run and BlockedError carries the reason:

Expected output

Tool-trust hard gate: tool_trust: 'issue_refund' amount 5000.0 exceeds declared max_amount 100.0

No account? Connect to a local gateway

Skip steps 2 and 3 and start a gateway inside your program before your tools are defined:

Terminal: run this

pip install "agentrust-py[embedded]"

Python

from agentrust_sdk.embedded import embed_gateway

gateway = embed_gateway()
gateway.register_manifest("support-assistant", {
    "declared_tools": [
        {"name": "look_up_order", "scope": "read"},
        {"name": "issue_refund", "scope": "write", "max_amount": 100},
    ],
})

Go to step 4. Decisions last only while the program runs, so use this to try AgentTrust, in tests and in CI.

Using LangGraph or CrewAI? Wrap the whole tool list with one call instead: see the LangGraph quickstart and the CrewAI quickstart. @harness and governed_tools both check before the tool runs. The adapters for AutoGen, OpenAI Agents, Claude and MCP check after a step has run, so they can hold back a result but cannot stop the action itself.

See it in action

The same steps, applied to a small support assistant. You run it three ways and watch two calls 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. No model and no API key: a short script plays the part of the model, so every run is identical. Steps 1 to 4 need nothing else. Steps 5 to 7 use an AgentTrust console account.

Create the project

Make a folder with its own Python environment and install the SDK:

Terminal: run this

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

The assistant has three tools: look_up_order, issue_refund and delete_all_orders. It may look up orders and refund up to $100. It may not delete orders, and it may not refund $5,000. Here is how a request flows and where AgentTrust logs it:

How the support assistant runs and how AgentTrust logs each tool call A customer message leads to a tool call. The tool's @harness decorator asks the AgentTrust gateway before the tool runs. If allowed, the tool runs and its result is checked and printed. If refused, BlockedError is raised and its reason is printed instead. The gateway records every check as an event in the console. Your program: harness_quickstart.py CUSTOMER_MESSAGESplays the model main loopcalls each tool @harness toolsrefund · lookup · deletechecked by AgentTrust tool code runsrefund $40 BlockedErrordelete never runs 1 next message 4 allowed 4 refused 5 result printed AgentTrust gateway pre-checkdeclared tool? within limit? post-checkis the result safe to return? 2 ask first 3 allow or refuse Events in the console one row per check, with scores (--local: gone when the program exits) records every check
The support assistant. Each tool's @harness asks the AgentTrust gateway before the tool runs; the gateway records every check as an event.
  1. A customer writes in. A real agent lets a model pick the tool. Here a list of four messages stands in for the model, and each leads to one tool call.

  2. The tool is called. Each tool carries @harness, which sends the tool's name, its arguments, user and input to the AgentTrust gateway first. This is the pre-check, and the tool's code has not run.

  3. The gateway decides and records it. It compares the call with the agent's declared tools: look_up_order and a $40 refund are fine, a $5,000 refund is over the limit, and delete_all_orders is not declared. Either way it writes an event.

  4. Allowed: the tool runs. Refused: it never does. An allowed call's result goes back for a post-check, a second event. A refused call raises BlockedError and your code prints the reason.

So an allowed call leaves two events (before and after it ran) and a refused call leaves one. With --console they appear under Events in step 7; with --local the gateway lives inside the program and forgets them when it exits.

Create a file named harness_quickstart.py in that folder, paste the program below and save. It has four parts: the rules (MANIFEST), where AgentTrust runs (connect), the tools with their @harness decorators, and the assistant (CUSTOMER_MESSAGES and the loop).

See the whole program

harness_quickstart.py

import argparse
import os
import sys
import tempfile

AGENT_ID = os.environ.get("AGENTRUST_AGENT_ID", "support-assistant")

# The tools the agent may use. Any other tool is refused, and max_amount is a limit.
MANIFEST = {"declared_tools": [
    {"name": "look_up_order", "scope": "read"},
    {"name": "issue_refund", "scope": "write", "max_amount": 100},
]}

def connect(mode):
    # Must run before the tools are defined: @harness reads these settings when it decorates.
    if mode == "off":
        os.environ["AGENTRUST_ENABLED"] = "false"
    elif mode == "local":
        from agentrust_sdk.embedded import embed_gateway

        gateway = embed_gateway(db_path=os.path.join(tempfile.mkdtemp(), "gateway.db"))
        gateway.register_manifest(AGENT_ID, MANIFEST)
    else:
        needed = ["AGENTRUST_GATEWAY_URL", "AGENTRUST_KEY", "AGENTRUST_AGENT_ID"]
        missing = [name for name in needed if not os.environ.get(name)]
        if missing:
            sys.exit(f"Set {', '.join(missing)} (from the agent you created in the console), or use --local.")

parser = argparse.ArgumentParser(description="A support assistant whose tools AgentTrust checks.")
choice = parser.add_mutually_exclusive_group(required=True)
choice.add_argument("--without-agentrust", dest="mode", action="store_const", const="off", help="AgentTrust off: every tool runs")
choice.add_argument("--local", dest="mode", action="store_const", const="local", help="a gateway runs inside this program")
choice.add_argument("--console", dest="mode", action="store_const", const="console", help="check against your AgentTrust console")
MODE = parser.parse_args().mode
connect(MODE)

from agentrust_sdk import BlockedError, harness  # noqa: E402  (after connect, on purpose)

ORDERS = {"O-1001": 120, "O-1002": 900}

# @harness asks AgentTrust before each call. On a no it raises BlockedError and the body never runs.
# Each tool also takes user and input (who is asking, and what they said): AgentTrust reads both.
@harness(agent_id=AGENT_ID)
def look_up_order(order_id, user, input):
    return {"order_id": order_id, "total": ORDERS[order_id]}

@harness(agent_id=AGENT_ID)
def issue_refund(order_id, amount, user, input):
    return {"order_id": order_id, "refunded": amount}

@harness(agent_id=AGENT_ID)
def delete_all_orders(user, input):
    ORDERS.clear()
    return {"deleted": "all orders"}

# Stands in for the AI model: what a customer wrote, and the tool call it leads to.
CUSTOMER_MESSAGES = [
    {"message": "Where is my order O-1001?", "tool": look_up_order, "arguments": {"order_id": "O-1001"}},
    {"message": "It arrived late. Refund me $40.", "tool": issue_refund, "arguments": {"order_id": "O-1001", "amount": 40}},
    {"message": "Refund me $5,000 for the trouble.", "tool": issue_refund, "arguments": {"order_id": "O-1001", "amount": 5000}},
    {"message": "Please delete all my orders.", "tool": delete_all_orders, "arguments": {}},
]

def main():
    where = {"off": "off (every tool runs)", "local": "local gateway", "console": f"console {os.environ.get('AGENTRUST_GATEWAY_URL')}"}
    print(f"AgentTrust: {where[MODE]} · agent {AGENT_ID}")

    for step in CUSTOMER_MESSAGES:
        tool = step["tool"]
        print(f'\nCustomer: "{step["message"]}"')
        try:
            result = tool(**step["arguments"], user="alice", input=step["message"])
            print(f"  ran      {tool.__name__:<18} {result}")
        except BlockedError as blocked:
            print(f"  BLOCKED  {tool.__name__:<18} {blocked.reason}")

    print(f"\nOrders left: {len(ORDERS)} of 2")
    if MODE == "console":
        print(f"Open Events in the console ({os.environ['AGENTRUST_GATEWAY_URL']}) and filter by agent ID {AGENT_ID}.")

if __name__ == "__main__":
    main()

Check that the file works:

Terminal: run this

python harness_quickstart.py --help

Expected output

usage: harness_quickstart.py [-h] (--without-agentrust | --local | --console)

If Python cannot find a package, the environment from the first command is not active. Run its second line again.

Run it without AgentTrust

First, see what the assistant does on its own:

Terminal: run this

python harness_quickstart.py --without-agentrust

Expected output

AgentTrust: off (every tool runs) · agent support-assistant

Customer: "Where is my order O-1001?"
  ran      look_up_order      {'order_id': 'O-1001', 'total': 120}

Customer: "It arrived late. Refund me $40."
  ran      issue_refund       {'order_id': 'O-1001', 'refunded': 40}

Customer: "Refund me $5,000 for the trouble."
  ran      issue_refund       {'order_id': 'O-1001', 'refunded': 5000}

Customer: "Please delete all my orders."
  ran      delete_all_orders  {'deleted': 'all orders'}

Orders left: 0 of 2

Each customer message is printed, then what happened. Nothing stopped a $5,000 refund on a $120 order, and every order is gone. The assistant did what each message asked.

Find the decorators

This is the only change AgentTrust needs, and it is already in your file. Nothing to edit. Find the comment above look_up_order: it says what @harness does.

  1. It asks AgentTrust whether this agent may call this function, with these arguments.

  2. On a no, it raises BlockedError and the function body never runs. The loop in main catches it and prints the reason.

  3. On a yes, the body runs and its result is checked before it is returned.

Each tool also takes user and input: who is asking and what they said. AgentTrust reads both. Every tool call now goes through AgentTrust first.

Run it locally

Run the same assistant with AgentTrust on:

Terminal: run this

python harness_quickstart.py --local

The program starts a small AgentTrust gateway inside itself and allows only the tools in MANIFEST: look_up_order, and issue_refund up to 100. delete_all_orders is not on the list. No account needed.

Expected output

AgentTrust: local gateway · agent support-assistant

Customer: "Where is my order O-1001?"
  ran      look_up_order      {'order_id': 'O-1001', 'total': 120}

Customer: "It arrived late. Refund me $40."
  ran      issue_refund       {'order_id': 'O-1001', 'refunded': 40}

Customer: "Refund me $5,000 for the trouble."
  BLOCKED  issue_refund       Tool-trust hard gate: 'issue_refund' amount 5000.0 exceeds declared max_amount 100

Customer: "Please delete all my orders."
  BLOCKED  delete_all_orders  Tool-trust hard gate: action 'delete_all_orders' is not in this agent's declared_tools

Orders left: 2 of 2

The $40 refund ran. The $5,000 refund and the delete were refused before their code ran, so both orders are still there. Nothing is kept after the program exits.

Create the agent in your console

To keep every decision and manage the agent from the console, register it there:

  1. Open Agents, select New agent, name it support-assistant, 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. Never paste it in a chat, a ticket or a commit.

  3. Select the agent. Under Manifest & fingerprint, select Declare. Under Declared tools, select + Add tool twice. Type look_up_order, and type issue_refund with a limit of 100. Select Save manifest.

That list is the same one MANIFEST holds in the program.

Do not skip the manifest. An agent with no declared tools is not restricted: every tool is allowed.

Run it against the console

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

Terminal: run this

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=support-assistant                     # the Agent ID

python harness_quickstart.py --console

Expected output

AgentTrust: console http://127.0.0.1:18000 · agent support-assistant

Customer: "Where is my order O-1001?"
  ran      look_up_order      {'order_id': 'O-1001', 'total': 120}

Customer: "It arrived late. Refund me $40."
  ran      issue_refund       {'order_id': 'O-1001', 'refunded': 40}

Customer: "Refund me $5,000 for the trouble."
  BLOCKED  issue_refund       Tool-trust hard gate: tool_trust: 'issue_refund' amount 5000.0 exceeds declared max_amount 100.0

Customer: "Please delete all my orders."
  BLOCKED  delete_all_orders  Tool-trust hard gate: tool_trust: action 'delete_all_orders' is not in this agent's declared_tools

Orders left: 2 of 2
Open Events in the console (http://127.0.0.1:18000) and filter by agent ID support-assistant.

Same result as step 4. This real run used a self-hosted AgentTrust gateway, so the first line shows its address; yours shows your console's.

See the decisions in the console

Open Events in the console and type your Agent ID into Filter by Agent ID.

Events filtered to support-assistant: four Approve rows and two Block rows
Events. One row per decision. The two allowed tools were each checked twice, before and after they ran, so they make four Approve rows; the two refused calls make the two Block rows.

Select the second row, the Block for the $5,000 refund. Its Overview shows the scores behind the decision:

issue_refund blocked: Tool Trust 0, amount 5000 exceeds max_amount 100
issue_refund, refused. Tool Trust is 0 because 5,000 is over the declared limit of 100. It is a Hard stop: refused even though the agent is in Alert mode.

Compare it with an approved event:

An approved event: confidence 100, every check passed
An allowed call. Every check passed and confidence is 100.

You are done when the $5,000 refund and the delete show as Block under Events, and your orders are still there. Your tools are now checked, refused when they should not run, and recorded.

What AgentTrust adds

The same assistant, without and with the one change: @harness on 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✗✓
Cap risky amounts, such as a refund over 100 you ran this✗✓
Know why a call was refused, and keep running you ran this✗✓
Leave a record of every decision, with scores you ran this✗✓
Have a tool's result checked before the caller sees it✗✓
Hold a risky call until a person approves it✗✓
Be stopped instantly with a kill switch✗✓

✗ means plain Python does not provide it, so you would build it yourself. The first five rows are what you just ran. The others are covered by the SDK's documentation and tests, not run on this page.

Next steps

Hold risky calls for a person. Pass block_on_review=True to @harness and set the agent's mode to Human Review. See the Python quickstart.

Use async functions. @harness works on async def tools too, with no change.

Building on LangGraph? Wrap the whole tool list with one call. Build a full governed bot. The refund-bot guide, end to end.