# Agent guard

> Check your AI agent’s tool calls, their results and its replies against rules you write in plain words.

Canonical: https://platform.uprelic.com/docs/agent-guard

---

Agent guard checks what your AI agent does against rules you write in plain words. Before the agent calls a tool, you send the call to `POST /v1/guard/check`. Strom answers, for each rule, the probability that the call breaks it, and tells you whether to go ahead. You can try it without code in the console’s [Agent guard](https://platform.uprelic.com/console/harnesses/agent-guard) page.

You can check three stages of your agent’s work:

- `before_tool`: a tool call, before it runs. A block means the tool doesn’t run.
- `after_tool`: what a tool returned, before the agent sees it. A block means the agent doesn’t get the result. The tool has already run, so this can’t undo what it did.
- `before_reply`: the agent’s reply, before the user sees it. A block means the reply isn’t sent.

## Writing a rule

A rule has an `id` that you choose (lowercase letters, digits, `-` and `_`) and five settings:

- `text`: the rule in plain words, for example “Block refunds above 500 unless the user’s role is manager.”
- `stage`: `before_tool` (the default), `after_tool` or `before_reply`. A rule checks one stage; to check a tool both before and after it runs, write two rules.
- `tools`: the tool names the rule is checked for, for example `["refund"]`, or `["*"]` for every tool. Required for `before_tool` and `after_tool`. Leave it out for `before_reply`, which checks every reply.
- `mode`: `observe` or `enforce`. A rule in `observe` mode never blocks anything. It only reports `would_block`, so you can see what it would catch before you enforce it.
- `threshold`: how sure Strom must be that the call breaks the rule before it counts as a match, from 0 to 1. The default is 0.9.

Create and edit rules with the API:

```bash Create a rule
curl https://api.uprelic.com/v1/guard/rules \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "refund-limit",
    "text": "Block refunds above 500 unless the user’s role is manager.",
    "stage": "before_tool",
    "tools": ["refund"],
    "mode": "enforce",
    "threshold": 0.9
  }'
```

The id must be new: if a rule already has it, you get a `409` and nothing changes. To change a rule, send its settings to `PUT /v1/guard/rules/{id}`, without the id in the body. Each change saves a new version, which checks report in `rules[].version`. `GET /v1/guard/rules` lists your rules and `DELETE /v1/guard/rules/{id}` deletes one. An account can have up to 256 rules.

## Checking a tool call

Send the tool’s name as `action` and its arguments as `params`. Put facts from your own application, like the signed-in user’s role, in `context`. Take them from your own systems, not from the model’s output, since the model could make them up.

```bash Request
curl https://api.uprelic.com/v1/guard/check \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "refund",
    "params": {"order_id": "A1", "amount": 800},
    "context": {"role": "support_agent"}
  }'
```

```json Response · illustrative
{
  "decision": "block",
  "rules": [
    {
      "id": "refund-limit", "version": 1,
      "text": "Block refunds above 500 unless the user’s role is manager.", "probability": 0.97,
      "threshold": 0.9, "mode": "enforce", "matched": true
    }
  ],
  "usage": {"rules_evaluated": 1, "input_tokens": 96, "cost": 0.000004, "currency": "usd"},
  "metadata": {"server_processing_time_ms": 92}
}
```

`decision` is one of:

- `block`: a rule in `enforce` mode matched. Don’t run the tool.
- `would_block`: only rules in `observe` mode matched. Run the tool.
- `allow`: no rule matched, or no rule applies to this tool. Run the tool.

`rules` lists every rule that applied, the most likely to be broken first, with its `text`, its `probability` and whether it `matched`. When a call is blocked, give the agent the text of each matched rule as the tool’s result, so it can tell the user why or take another route instead of trying again.

## Sessions

A session is one task of your agent, such as answering one customer email. Open one with `POST /v1/guard/sessions` when the task starts, and send its `id` as `session_id` with each check of that task. A check without `session_id` counts as a session of its own.

```bash Request
curl -X POST https://api.uprelic.com/v1/guard/sessions \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json Response
{"id": "5f0c2a1e-8d3b-4c7a-9e21-3b6f0d9a4c11", "cost": 0.0, "currency": "usd"}
```

`cost` is the session fee: 0 while your plan's sessions for the month last. A `session_id` that isn't one of your sessions gets `404`.

## Checking a tool’s result

After the tool has run, send the same `action` and `params` with what the tool returned as `result`. The `after_tool` rules for that tool are checked. When the decision is `block`, don’t give the result to the agent: give it the text of each matched rule instead.

```bash Request
curl https://api.uprelic.com/v1/guard/check \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "read_file",
    "params": {"path": "config/database.yml"},
    "result": "pool: 5\npassword: hunter2"
  }'
```

## Checking a reply

To check a reply before the agent sends it, send `response` (and optionally the user’s `request`) instead of `action`. The `before_reply` rules are checked. When the decision is `block`, don’t send the reply.

```bash Request
curl https://api.uprelic.com/v1/guard/check \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "response": "Sure, your API key is upr_k3J9xQ2mP7.",
    "request": "What is my API key?"
  }'
```

## Using the Python SDK

Install it with `pip install uprelic`. Decorate each tool with `client.guard()`: every call is checked first, with the function’s arguments as `params`. With `check_result=True`, what the function returns is checked too.

```python Python
import uprelic

client = uprelic.Uprelic()  # reads UPRELIC_API_KEY

client.create_guard_rule(
    "refund-limit",
    text="Block refunds above 500 unless the user’s role is manager.",
    tools=["refund"],
    mode="enforce",
    threshold=0.9,
)

@client.guard("refund", context=lambda: {"role": current_user.role})
def refund(order_id, amount):
    ...

@client.guard("read_file", check_result=True)
def read_file(path):
    ...

try:
    refund("A1", amount=800)
except uprelic.Blocked as blocked:
    tool_result = str(blocked)  # "refund blocked by refund-limit: Block refunds above 500 …"
```

`uprelic.Blocked` is raised when the decision is `block`, and also when the check itself fails, so an unchecked tool never runs. Its message names the matched rules; return it to the agent as the tool’s result. To check a reply, call `client.check(response=..., request=...)`.

## Trying a rule

Every check uses the rules saved on your account. To try a new rule, save it in `observe` mode. It never blocks anything, so your agent keeps working while you see what the rule would catch.

Before you switch the rule to `enforce`, look at what it would have blocked. `GET /v1/guard/checks` lists your checks, newest first. Filter with `decision` (`allow`, `would_block` or `block`), and page with `page` and `page_size` (up to 100).

```bash Request
curl "https://api.uprelic.com/v1/guard/checks?decision=would_block" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json Response · illustrative
{
  "checks": [
    {
      "id": "3f6c1d2e-8a4b-4c1e-9d2f-1a2b3c4d5e6f",
      "created_at": "2026-10-27T09:30:00+00:00",
      "stage": "before_tool",
      "decision": "would_block",
      "action": "refund",
      "params": {"order_id": "A1", "amount": 800},
      "context": {"role": "support_agent"},
      "rules": [{"id": "refund-limit", "version": 1, "probability": 0.97, "matched": true, ...}],
      "cost": 0.00001, "currency": "usd"
    }
  ],
  "total": 1, "page": 1, "page_size": 25
}
```

In Python: `client.guard_checks(decision="would_block")`.

## Stored data

What you send in a check, including tool results, is stored so you can list it, for as long as your [plan](https://platform.uprelic.com/docs/pricing#plans) keeps request contents (7 days on Free), then deleted. Checks that no rule applies to aren’t stored. Agent guard has no zero data retention option.

## Cost

- Input tokens cost what they cost on `POST /v1/systemone` with `strom-fast`: the tool call, result or reply once, plus each rule's text. `usage.input_tokens` is that count.
- Each session past your plan's monthly sessions costs $0.50 per 1,000: Free includes none, Starter 50,000 and Growth 500,000. The fee is charged when you open the session, or with the check when it has no `session_id`.
- A check that no rule applies to is free.
- The cost of each check is in `usage.cost`.

## Errors

- `401`: the API key is missing or wrong.
- `402`: your balance is too low.
- `404`: no rule has this id. `409`: a rule already has this id, or the account already has 256 rules.
- `413`: the check has more than 32,000 input tokens, for example a very large tool result.
- `422`: the request is invalid, for example both `action` and `response` are sent, or a `before_tool` rule has no `tools`.
- `429`: too many requests. Checks share the rate limits of your other requests.
- `502`: the model couldn’t be reached. Treat the action as not checked.
