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

# Policy

> What an agent may touch, what needs a human, what is masked, and what the engine must guarantee before the connection will start.

One policy governs one connection. The language is the same for every engine — what differs between engines is the *level* at which each guarantee is met, not the rules you write.

```yaml theme={null}
version: 1
connection: shop
mode: policy

require:
  scoping: enforced
  masking: enforced
  statement_timeout: native
  attribution: native

approval:
  read: auto
  write: pr_required
  delete: pr_required
  ddl: denied
  unbounded_mutation: pr_required

resources:
  allow:
    - { kind: table, path: [shop, orders] }
    - { kind: table, path: [shop, customers], columns: [id, tenant_id, email, tier] }
    - { kind: view,  path: [shop, order_summary] }

scope:
  enabled: true
  bind:
    orders.tenant_id: "{tenant_id}"
    customers.tenant_id: "{tenant_id}"

limits:
  max_rows: 100
  statement_timeout_ms: 5000
  approval_timeout_ms: 60000

masking:
  customers.email: tokenize
```

## require — what the engine must provide

The gateway refuses to start a connection that cannot meet every line. Each guarantee takes a minimum level from [the capability ladder](https://warden.buildlab.in/concepts/capability-ladder.md): `native`, `enforced`, or `advisory`.

```yaml theme={null}
require:
  scoping: enforced      # Warden rewriting the statement is enough
  masking: enforced
  statement_timeout: native   # the server must enforce this itself
```

Asking for `native` scoping means the *database* must enforce it — row-level security — so that it holds even for a connection that bypasses Warden. Asking for `enforced` accepts Warden's governed relations. Both are defensible; the point is that you choose, and you find out at startup rather than during an incident.

## approval — what needs a human

| Value | Meaning |
| - | - |
| `auto` | Runs without asking |
| `pr_required` | Becomes a proposal with a preview, and waits for a reviewer |
| `denied` | Refused outright; retrying will not help |

Classes are derived from the *structure* of the operation, never its leading keyword: a `WITH … DELETE … RETURNING` wrapped in a `SELECT` is a delete. `unbounded_mutation` is a class of its own, because "change every record" deserves a different answer from "change this one".

## resources — what exists for this agent

Anything not listed is not merely denied: it is invisible. `warden_describe_schema` returns only permitted resources, because a schema listing is reconnaissance.

```yaml theme={null}
- { kind: table, path: [shop, customers], columns: [id, tenant_id, email, tier] }
```

`columns` restricts the projection, enforced inside the statement rather than filtered from results.

> [!TIP]
>
> A table you cannot scope is more dangerous than one you deny. In the example policy, `shop.order_lines` is deliberately excluded: it has no tenant column, so predicate injection cannot scope it, and an agent granted it would read every tenant's line items. `warden policy lint` flags that combination.

## scope — restricting rows to this agent

```yaml theme={null}
scope:
  enabled: true
  bind:
    orders.tenant_id: "{tenant_id}"
```

`{tenant_id}` comes from the agent's `scope` values in `warden.yaml`. For PostgreSQL, each scoped table is replaced by a subquery already filtered, column-projected, and masked — so no alias, join, or expression in the agent's own SQL can get around it.

> [!WARNING]
>
> **Scope is not a bound.** "Every order belonging to this tenant" is still unbounded for a reviewer, and Warden counts it that way. Scope limits *whose* records are affected, not *how many*. See [blast radius](https://warden.buildlab.in/concepts/blast-radius.md).

## limits

| Field | Effect |
| - | - |
| `max_rows` | Caps rows returned; the response reports `truncated` so the agent knows it did not see everything |
| `statement_timeout_ms` | Passed to the engine where it is native |
| `approval_timeout_ms` | How long a blocking approval waits before expiring. Nothing runs on expiry |

## masking

```yaml theme={null}
masking:
  customers.email: tokenize
```

| Mode | Behaviour |
| - | - |
| `redact` | Replaced with a fixed marker |
| `hash` | Deterministic digest |
| `tokenize` | Deterministic token that round-trips, so the agent stays useful |
| `partial` | Keeps a recognisable fragment |

Masking is applied inside the statement where the engine allows it, which is what stops `SELECT upper(email)` or a join from unmasking a value. See [masking](https://warden.buildlab.in/concepts/masking.md).

## Linting

```bash theme={null}
warden policy lint ./policies/shop.yaml
```

It checks shape and internal consistency — resources, scope bindings, masking rules, approval classes, and combinations that are valid individually but dangerous together, such as a granted table with no column to scope it by.

The `require:` block is checked at startup instead, against the real connection: `warden serve` probes the engine and refuses to start when a guarantee falls short of the level the policy demands.


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