# Policy reference (CEL)

flowifai adjudicates every tool call your broker forwards against your org's policy
rules. This page is the exact contract: what a rule is, how matching works, which
effect wins, and how the engine fails.

## What a rule is

A policy rule has:

| Field | Meaning |
| --- | --- |
| Tool pattern | The exact tool name to match, or `*` for any tool. |
| Argument predicate | Optional [CEL](https://cel.dev) expression over the call's arguments. Empty means "always matches". |
| Effect | `allow`, `needs_approval`, or `deny`. |
| Enabled | Disabled rules never match. |
| Approval requirement | For `needs_approval` rules only: how many approvals (n-of-m), which approver groups are eligible, and whether approvals must span at least two groups. |

Rules are org-level. Tool names come from the schemas your registered MCP servers
expose — the console's policy view shows which registered server provides each tool,
so you can scope a rule to a server's tools by naming them exactly.

## Matching

A rule matches a call when **both** hold:

1. its tool pattern matches the called tool (exact name, or `*`), and
2. its argument predicate — if present — evaluates to `true`.

Predicates are CEL expressions evaluated with two variables:

- `args` — the call's arguments as a map (`map<string, dyn>`), decoded from the tool
  call's JSON arguments object;
- `tool` — the called tool's name as a string.

Examples:

```text
has(args.query) && type(args.query)==string &&
  (args.query.contains("DROP")||args.query.contains("DELETE")||args.query.contains("TRUNCATE"))
```

```text
has(args.amount) && type(args.amount)==double && args.amount > 0.0
```

```text
has(args.path) && has(args.recursive) && args.recursive == true
```

Predicates are compiled and validated when you save the rule; a rule with a
predicate that does not compile is rejected.

## Most restrictive wins, default deny

All enabled rules are evaluated against the call. Among the rules that match, the most
restrictive effect wins:

```text
deny  >  needs_approval  >  allow
```

If **no** rule matches, the decision is **deny**. There is no implicit allow anywhere:
you must write an `allow` rule for the tools you want to pass ungated.

When the winning decision is `needs_approval` and several `needs_approval` rules
matched, the approval requirement is the strictest merge across them: the maximum
approvals-required count, the union of eligible approver groups, and cross-group
required if any matching rule requires it. The matched rules are surfaced to approvers
as the "why gated" provenance on the ticket.

## Fail-closed behavior

The engine treats every abnormal condition as a denial:

- **Unparseable arguments** → deny.
- **Predicate evaluation error** (for example, a predicate that touches a wrong-typed
  field) → the whole call is denied, even if another rule would have allowed it.
- **Unknown effect value** → treated as deny.

Because an erroring predicate denies the whole call, write broad (`*`-pattern)
predicates **type-guarded** so they can only ever evaluate to true or false:

```text
has(args.command) && type(args.command)==string && args.command.contains("deploy")
```

The `has(...)` and `type(...)` guards ensure the predicate never errors regardless of
the argument shape a tool sends.

## Policy templates

The console ships a curated catalog of commit-point templates (destructive SQL,
payment/charge, outbound send, recursive filesystem delete, deploy/migrate/apply,
git force-push). Every template uses tool pattern `*` with a type-guarded predicate
and effect `needs_approval`. Templates key on *guessed* argument field names — real
MCP tools use arbitrary schemas — so treat an applied template as a starting point and
verify it matches your tool's actual fields (for example, your DB tool may call the
query field `sql` or `statement` instead of `query`).

## Schema drift and stale rules

Enrollment registers your MCP server's tool schema. If the live schema drifts from the
registered one, the broker blocks all calls to that server until you re-run the
app-generated `flowifai connect` command. Additionally, argument-level rules that
reference a changed schema are **disabled** (fail closed) until re-confirmed — a
renamed or re-typed field can never quietly turn a gate into a pass.

## Who can edit policy

Policy create/delete requires a policy-admin user, which is a separate capability from
approval eligibility. A single approver therefore cannot rewrite policy to bypass
n-of-m or separation-of-duty requirements.

## Managing rules

Manage rules in the console under **Policies**, or from the CLI:

```sh
flowifai policy list
flowifai policy add --name "gate deploys" --tool "*" \
  --predicate 'has(args.command) && type(args.command)==string && args.command.contains("deploy")' \
  --effect needs_approval
flowifai policy delete <id>
```

Rule changes are recorded in the audit log like every other security-relevant event.