> ## Documentation Index
> Fetch the complete documentation index at: /llms.txt
> Use this file to discover all available pages before exploring further.

# Attribute-Based Controls

> Apply rules only for some values of a request attribute, such as the country. Author them as one control per value or as one composite control.

Some rules apply only when a request attribute has a given value. The attribute can be the user's country, tenant, user tier or channel. Agent Control expresses this as a condition on the attribute, combined with the rule itself.

This example uses the country (jurisdiction) as the attribute. An assistant must not return a German IBAN to a user in Germany, or a US Social Security number to a user in the US. To use another attribute, replace the jurisdiction field and its values.

## What This Demonstrates

* Attribute-based rules: each rule applies only when the request's jurisdiction matches.
* Two ways to author the same policy:
  * One control per attribute value, here one per country.
  * One composite control with a branch for each value.
* Baseline controls that apply to every country, next to the country controls.
* Two ways to send the jurisdiction with each check:
  * In `step.context`, with `evaluate_controls()` or the HTTP API.
  * As a function argument, with the `@control()` decorator.

No LLM or API key is needed. The scripts use fixed text in place of a model answer.

## Quick Start

### Prerequisites

1. Start the Agent Control server: `make server-run` from the repo root.

### Run the Demo

```bash theme={null}
# From the agent-control repo root
uv run python examples/attribute_controls/attribute_controls_context.py
uv run python examples/attribute_controls/attribute_controls_decorator.py
```

Each script creates its agent and controls, then runs the scenarios. It prints PASS or FAIL for each check, and exits non-zero if any check fails. The scripts are safe to re-run.

## Send the Jurisdiction

The caller must send the jurisdiction with each check. Use an ISO 3166-1 alpha-2 code, such as `DE` or `US`.

If the country code arrives in a request header, such as `x-ai-jurisdiction: DE`, the caller copies it into `step.context.jurisdiction`. Agent Control does not map request headers into the step.

### In `step.context` (recommended)

`context` holds request metadata, separate from the data that the controls check. Controls select the value with the path `context.jurisdiction`.

```python theme={null}
result = await agent_control.evaluate_controls(
    "support-answer",
    input="What is on file for me?",
    output=answer_text,
    context={"jurisdiction": "DE"},
    step_type="llm",
    stage="post",
    agent_name=AGENT_NAME,
)
if not result.is_safe:
    ...  # blocked
```

Gateways and non-Python clients send the same field in the body of `POST /api/v1/evaluation`:

```json theme={null}
{
  "agent_name": "my-agent",
  "stage": "post",
  "step": {
    "type": "llm",
    "name": "support-answer",
    "input": "What is on file for me?",
    "output": "Your refund goes to DE89 3704 0044 0532 0130 00.",
    "context": {"jurisdiction": "DE"}
  }
}
```

### As a decorator argument

`@control()` builds the step from the function's arguments, and it cannot set `step.context`. To use the decorator, pass the jurisdiction as a function argument and select it with `input.jurisdiction`.

```python theme={null}
@control(step_name="support-answer", step_type="tool")
async def support_answer(*, text: str, jurisdiction: str) -> str:
    return text
```

> [!NOTE]
>
> This works only for `tool` steps and custom step types, where `input` is the dict of all arguments. For an `llm` step, `input` is a single string, so the path `input.jurisdiction` does not resolve.

> [!WARNING]
>
> The controls below use the `context` path. For the decorator path, make two changes in each control, or the controls never match and every request is allowed:
>
> * Set `"step_types"` to `["tool"]`.
> * Set the jurisdiction selector `path` to `"input.jurisdiction"`.

## Author the Controls

Each country rule is an `and` of two conditions:

* The jurisdiction is in a list, checked with the `list` evaluator.
* The rule matches, here a `regex` on the output.

### Option 1: One control per country

```python theme={null}
de_iban = {
    "execution": "server",
    "scope": {"step_types": ["llm"], "stages": ["post"]},
    "condition": {
        "and": [
            {
                "selector": {"path": "context.jurisdiction"},
                "evaluator": {"name": "list", "config": {"values": ["DE"]}},
            },
            {
                "selector": {"path": "output"},
                "evaluator": {
                    "name": "regex",
                    "config": {"pattern": r"\bDE\d{2}(?: ?\d{4}){4} ?\d{2}\b"},
                },
            },
        ]
    },
    "action": {"decision": "deny"},
}
```

Write a similar control for each country, for example `US` with a Social Security number pattern.

### Option 2: One composite control

Put each country's `and` branch inside one `or`:

```python theme={null}
countries = {
    "execution": "server",
    "scope": {"step_types": ["llm"], "stages": ["post"]},
    "condition": {
        "or": [
            {"and": [jurisdiction_in(["DE"]), output_matches(DE_IBAN)]},
            {"and": [jurisdiction_in(["US"]), output_matches(US_SSN)]},
        ]
    },
    "action": {"decision": "deny"},
}
```

`jurisdiction_in()` and `output_matches()` are small helpers in the example scripts. They return the leaf conditions shown in Option 1.

### Which option to use

| | One control per country | One composite control |
| - | - | - |
| Number of controls | One per country | One |
| Match result | Names the country's control | Names the composite control |
| Change one country | Edit or disable only that control | Edit the shared condition |

## Baseline and Country Controls

Most policies combine two kinds of rules:

* **Baseline controls** apply to every country. They have no jurisdiction condition.
* **Country controls** apply only to some countries. They include the jurisdiction condition.

Keep them as separate controls. A baseline control is a plain control:

```python theme={null}
baseline_email = {
    "execution": "server",
    "scope": {"step_types": ["llm"], "stages": ["post"]},
    "condition": {
        "selector": {"path": "output"},
        "evaluator": {
            "name": "regex",
            "config": {"pattern": r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b"},
        },
    },
    "action": {"decision": "deny"},
}
```

The baseline and the country controls run together on each check. A baseline control also applies when the request has no jurisdiction.

> [!NOTE]
>
> Agent Control evaluates the controls in parallel and stops the remaining evaluations after the first `deny` match. If a baseline control and a country control both match, `result.matches` usually names only one of them.

To tell the two kinds apart, use a naming convention or `tags`, for example `["baseline"]` and `["country", "DE"]`.

## Scenarios

Both authoring options give the same decisions. The example runs them side by side, with the baseline control on both:

| Scenario | Expected | Matched by |
| - | - | - |
| DE + IBAN | Blocked | DE country rule |
| US + IBAN (no US rule) | Allowed | - |
| US + SSN | Blocked | US country rule |
| DE + SSN (no DE rule) | Allowed | - |
| US + clean text | Allowed | - |
| DE + email | Blocked | Baseline |
| No jurisdiction + IBAN | Allowed | - (country rules are skipped) |
| No jurisdiction + email | Blocked | Baseline |

## Missing Jurisdiction

> [!WARNING]
>
> If a request has no jurisdiction, no country rule matches. Only baseline controls apply, so the request is allowed unless a baseline control matches. Agent Control has no built-in default jurisdiction.

If you need a strict default, add a control that matches when the jurisdiction is missing or not covered:

```python theme={null}
strict_default = {
    "execution": "server",
    "scope": {"step_types": ["llm"], "stages": ["post"]},
    "condition": {
        "and": [
            {"not": jurisdiction_in(["DE", "US"])},
            {"or": [output_matches(DE_IBAN), output_matches(US_SSN)]},
        ]
    },
    "action": {"decision": "deny"},
}
```

Keep this control in sync with the country controls:

* The `not` list must name every country that has a rule.
* The `or` must repeat every country rule.

For example, if you add an `FR` control but do not add its rule to the `or`, a request with no jurisdiction that matches the `FR` rule is allowed.

## Files

* `attribute_controls_context.py` - Sends the jurisdiction in `step.context` with `evaluate_controls()`
* `attribute_controls_decorator.py` - Sends the jurisdiction as a `@control()` function argument

## Source Code

View the complete example with all scripts and setup instructions:

[`Attribute-Based Controls Example`](https://github.com/agentcontrol/agent-control/tree/main/examples/attribute_controls)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.