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

# Request context

> Per-request caller identity, role selection, attributes, biscuit attenuation.

Real backends serve many users, and different users get different capabilities.
Hexgate splits that into two pieces:

* **`HexgateContext`** — the per-request scope. Marks "this invocation acts on
  behalf of alice, with roles X, and these attributes." Async context manager;
  pushes a fact-bearing Biscuit through the agent runtime.
* **Role policies** — one `policy.yaml` per role, optionally inheriting from a
  base mixin. The runtime picks the right one at call time based on the active
  context's primary role.

The two are deliberately decoupled: tokens carry **identity** (who is calling),
policy files carry **rules** (what they can do).

## Minimal example

```python theme={null}
from hexgate import HexgateContext, load_hexgate_agent, stream_agent

agent, handler = load_hexgate_agent("support_bot")          # client + roles attached at load

async with HexgateContext(user_id="alice", user_roles=["billing"], ttl_seconds=300):
    async for event in stream_agent(agent, handler, "refund customer 30"):
        ...
```

No manual `attenuate_for_user`, `extract_facts`, or `ToolUseContext` plumbing at
the call site. The runtime mints the per-request token, picks the `billing`
role's policy file, and evaluates its constraints against each tool call.

## Filtering on attributes (`ctx.*`)

Beyond the role, a context can carry an open **attributes** bag that policy
constraints read through the `ctx.*` namespace — turning role-based access into
attribute-based (ABAC) filtering:

```python theme={null}
async with HexgateContext(
    user_id="alice",
    user_roles=["billing"],
    attributes={"department": "finance", "region": "EU", "clearance_level": 3},
):
    async for event in stream_agent(agent, handler, "refund customer 30"):
        ...
```

```yaml theme={null}
# the billing policy can now gate on caller attributes, not just role
tools:
  refund_order:
    mode: allow
    constraints:
      - args.amount <= 500
      - 'ctx.department == "finance"'
      - "ctx.clearance_level >= 3"
```

A missing `ctx.<key>` fails closed (deny), like any absent reference.

<Warning>
  **Attributes are exactly as trustworthy as the code that sets them.** `ctx.*`
  may gate `deny` decisions, but it carries the **same trust contract as
  `user_roles`**: populate `attributes` from trusted server-side data (your
  auth/session/IdP), never from raw client input. Hexgate does not independently
  verify attribute values.
</Warning>

## FastAPI pattern

The scope must enclose the **streaming iteration itself**, because the role is
resolved lazily — read on each tool call as the agent runs (see [Notes](#notes)).
The robust shape is to open the scope *inside the generator* that produces the
response:

```python theme={null}
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from hexgate import HexgateContext, load_hexgate_agent, stream_agent

app = FastAPI()
agent, handler = load_hexgate_agent("support_bot")          # at startup

@app.post("/chat")
async def chat(request: Request, req: ChatRequest):
    auth = await authenticate(request)                       # your auth

    async def scoped_events():
        # scope wraps the iteration, so every tool call sees the role
        async with HexgateContext(
            user_id=auth.id,
            user_roles=auth.roles,                            # e.g. ["billing"]
            session_id=request.state.session_id,
            attributes={"department": auth.department},       # trusted server data
            ttl_seconds=300,
        ):
            async for event in stream_agent(agent, handler, req.message):
                yield event

    return StreamingResponse(scoped_events())
```

<Warning>
  **Don't scope auth in `@app.middleware("http")`.** Starlette's
  `BaseHTTPMiddleware` returns the response *before* the `StreamingResponse` body is
  iterated, so a `async with HexgateContext(...): return await call_next(request)`
  exits the scope before the first event is produced. Because hexgate resolves the
  role at tool-call time, every call during streaming then reads no role and
  silently falls back to the `default` policy — a silent authorization downgrade,
  no error raised. Scope inside the generator (above), or use a pure **ASGI**
  middleware that wraps the `send` channel — not `BaseHTTPMiddleware`.
</Warning>

For a non-streaming endpoint, `async with HexgateContext(...): return await
invoke_agent(...)` is fine — the agent runs to completion inside the scope.

## `HexgateContext` fields

| Field         | Type                                         | Required | Effect                                                                                                  |
| ------------- | -------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `user_id`     | `str`                                        | ✅        | Becomes `user("alice")` in the attenuated Biscuit.                                                      |
| `user_roles`  | `list[str]`                                  | optional | The first role selects which role policy file applies at tool-call time. Fall-back: the `default` role. |
| `session_id`  | `str?`                                       | optional | Trace tagging — surfaces on Langfuse spans.                                                             |
| `attributes`  | `dict[str, str \| int \| bool \| list[str]]` | optional | Caller attributes exposed to `ctx.*` constraints. Populate from trusted server data.                    |
| `ttl_seconds` | `int?`                                       | optional | Embeds a `check if time($t), $t < now+ttl` predicate so the token can't outlive the request.            |

Only the **first** role in `user_roles` reaches policy selection today (read via
`primary_role`); the rest are carried but inert until multi-role selection lands.

## Role policies — one file per role

Agents that need per-role behaviour ship a `policies/` directory instead of a
single `policy.yaml`:

```text theme={null}
agent/
├── agent.yaml
├── system.md
└── policies/
    ├── default.yaml          # fallback when no role matches
    ├── read_only.yaml        # mixin — is_mixin: true
    ├── support.yaml          # inherits: [read_only]
    └── billing.yaml          # inherits: [read_only, support]
```

Each role file is a complete `AgentPolicy`. Inheritance is left-to-right, child
wins on conflicts:

```yaml theme={null}
# policies/read_only.yaml  (mixin — safe base)
version: 1
is_mixin: true
default_policy:
  mode: deny
tools:
  view_orders:  { mode: allow }
  list_tickets: { mode: allow }
```

```yaml theme={null}
# policies/billing.yaml
version: 1
inherits: [read_only]
tools:
  refund_order:
    mode: allow
    constraints:
      - args.amount <= 500
      - args.currency == "USD"
  wire_transfer:
    mode: approval_required
    constraints:
      - args.amount <= 100000
```

See [constraints](/policy/constraints) for the expression grammar (including
`ctx.*`) and a role-scoped end-to-end walkthrough.

## Notes

* **Single-file policies still work.** A legacy `policy.yaml` is treated as the
  `default` role — no migration needed.
* **Lazy attenuation.** `HexgateContext.__aenter__` only pushes a contextvar —
  the cryptographic work happens inside `stream_agent` / `invoke_agent` the first
  time the agent runs. Errors surface at first agent call, not at scope entry.
* **Local agents skip attenuation.** A context scope around a `load_local_agent`
  agent logs a warning and runs with no facts. The `default` policy still applies
  — use `load_hexgate_agent` for the full signed chain.
* **Explicit override.** Passing `tool_use_context=` explicitly to `stream_agent`
  / `invoke_agent` wins over an active context scope. Useful for tests or one-off
  bypass.
* **Sync callers.** `HexgateContext` exposes both `async with ctx:` and
  `ctx.sync_scope()`. The async form is the primary API; the sync mirror exists
  for CLI loops and `Runner.run_sync`-style callers.
