# Approval policies and impact limits

## Approval policies

A policy decides, per environment and on every call, whether a person must approve. Create with `ops.approvalPolicies.create(…)` (Python `ops.approval_policies.create(…)`, snake_case fields). Needs `policies:write`; reading needs `policies:read`. The environment is the key's.

| Field | Values |
| --- | --- |
| `name` | 1 to 80 characters |
| `matchKind` | `action` (with `matchActionName`: one action; without: every action), `custom` (approvals requested with `request_approval`), `any` |
| `matchActionName` | An existing action in the environment |
| `condition` | Optional CEL expression over `input`, `agent.id`, `user.id`, `tags`, `action.name`, `kind`. Checked against the action's input schema when saved. An evaluation error requires approval, so guard optional fields with `has(input.x)` |
| `minAmount`, `matchAgentId`, `matchUserId`, `matchTags` | Optional matchers |
| `effect` | `require_approval`, `auto_approve`, `auto_deny` |
| `requiredApprovals` | 1 to 20 distinct approvers (N-of-M). Any single deny finalizes |
| `decidedBy` | `team` (default: console, Slack, a key with `approvals:decide`) or `end_user` (the user the call was made for, in your app; one decision). A call with no end user is always decided by the team |
| `priority` | 0 to 10000, lower runs earlier. The earliest match wins |
| `enabled` | Policies from packs install disabled |
| `acknowledgeBroadScope` | Required for an `auto_deny` that targets every action or every request |

Rules that hold whatever the policies say:
- An `auto_approve` clears an action's own `requiresApproval` when it names that action with `matchActionName`, and not otherwise.
- The key or app that requested an approval cannot decide it (`403 AUTHORIZATION_ERROR`). A person cannot decide what their own agent requested (`403 SELF_APPROVAL_NOT_ALLOWED`) unless nobody else can decide in that environment and they decide directly in the console or Slack.
- An approved call sends the request snapshot taken when it was requested; editing the action meanwhile changes nothing.
- An approval of a governed action is bound to the exact request. Anlyon recomputes its digest before dispatch and refuses the call on any difference.
- Halting the environment stops even approved calls.

Test a policy without running anything: `ops.approvalPolicies.evaluate({ … })`. Policy files (`approval-policies/files/plan|apply|test`) keep policies in the repository; see https://docs.anlyon.com/trust-control/policy-as-code.md.

Examples:

```typescript
// The user approves each new issue in their own GitHub, in your app
await ops.approvalPolicies.create({ name: 'User approves new issues', matchKind: 'action', matchActionName: 'github-create-issue', effect: 'require_approval', decidedBy: 'end_user' });

// Refunds over $100 need two approvers
await ops.approvalPolicies.create({ name: 'Big refunds', matchKind: 'action', matchActionName: 'refund-order', condition: 'input.amount > 10000', effect: 'require_approval', requiredApprovals: 2 });

// Small refunds go through without a person
await ops.approvalPolicies.create({ name: 'Small refunds', matchKind: 'action', matchActionName: 'refund-order', condition: 'input.amount <= 2000', effect: 'auto_approve', priority: 10 });
```

## Impact limits

A limit caps what governed actions change. `ops.impactLimits.create(…)`, needs `impact-limits:write` (no key holds it by default, so the agent it constrains cannot raise it). Reading needs `impact-limits:read`, which Standard includes.

| Field | Values |
| --- | --- |
| `name` | 1 to 120 characters |
| `dimension` | `money`, `resource_mutations`, or a declared unit such as `emails` |
| `currency` | Required for `money` (`usd`) |
| `period` | `day` (UTC day), `month`, `total` |
| `amount` | Integer. Minor units for money |
| `actionName` | Optional: count this action alone |
| `resourcePrefix` | Optional: count effects on resources with this prefix, for example `github:acme/fixtures@` |
| `scope` | `environment` (default, one shared allowance) or `end_user` (each end user gets the whole amount; calls with no user count on the shared one). Fixed once created |

How it behaves:
- Reserved just before dispatch, after any approval. All applicable limits are reserved together, or none and nothing is sent.
- A call that does not fit is refused whole: a send to four recipients with two left sends none.
- `unknown` and pending outcomes keep their capacity held until settled.
- `ops.impactLimits.list()` returns `limit`, `consumed`, `reserved`, `unknownExposure`, `available` for each.
- `update(id, { amount })` raises or lowers; `archive(id)` stops it applying to new calls.
- A limit counts governed actions called through Anlyon. Calls made around Anlyon are not counted.
