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

# Policy engine

> How user rules become checks, where they're enforced, and what a rejection looks like.

> The policy engine is the gate every plan passes through. Rules are stored on-chain on the user's smart account; checks run twice — once in the off-chain loop as an optimization, and once on-chain at the contract level as the actual guarantee.

This page covers the rule model and how it's enforced. For how to configure
rules as a user, see [Set your rules](/quickstart/set-your-rules). For where
this fits in a tick, see [The deterministic loop](/architecture/deterministic-loop).

## What "policy" means in Compass

A policy is the **full set of constraints attached to one user's smart
account**. It has five fields:

| Field                | Type                     | Set by |
| :------------------- | :----------------------- | :----- |
| `risk_band`          | `u8` (1–10)              | Owner  |
| `protocol_whitelist` | `Set<(protocol, chain)>` | Owner  |
| `chain_whitelist`    | `Set<chain>`             | Owner  |
| `per_route_cap_usdc` | `u128`                   | Owner  |
| `daily_cap_usdc`     | `u128`                   | Owner  |

Every field is owner-only. The agent's session key has no permission to
modify any of them. See [Session keys](/contracts/session-keys).

The policy lives on the Diamond account as facet storage. It is the single
source of truth — both the off-chain engine and the on-chain enforcement
read from the same on-chain state.

## Two layers of enforcement

Policy is checked **twice** for every route:

<img src="https://mintcdn.com/compassai/Qq7UDvjWweIau4hP/images/policy-engine-diagram.svg?fit=max&auto=format&n=Qq7UDvjWweIau4hP&q=85&s=9886dbbdbbb630d5eb39cfbcb7e76151" alt="policy engine diagram" className="rounded-lg" width="1500" height="800" data-path="images/policy-engine-diagram.svg" />

1. **Off-chain, in the deterministic loop.** As step 4 of an
   `EvaluatorThought`. This is an optimization — it stops out-of-policy
   plans before they consume gas and lets the dashboard show users *why*
   something was rejected.
2. **On-chain, in the smart account.** The session key facet validates
   every call against the policy stored on the same account. Any call that
   violates a rule reverts before USDC moves.

The on-chain check is what makes the trust model the contract rather than
the operator. The off-chain check could be bypassed by a compromised agent
backend; the on-chain check could not — the session key physically cannot
sign a call the facet would reject. Both layers read the **same** policy
state — the one stored on the Diamond. There is no separate off-chain
policy; the cache is just a copy.

<Info>
  If the off-chain check ever lets something through that the on-chain check
  would reject, the result is a reverted transaction, not a successful
  exploit. The on-chain layer is the safety net.
</Info>

## The check sequence

For a single candidate plan, the off-chain engine runs the following checks
in order. The first failure short-circuits the rest and produces a rejection.

1. **Chain whitelist.** Is every chain in the plan (source and target) in
   `chain_whitelist`?
2. **Protocol whitelist.** Is the `(protocol, target_chain)` pair in
   `protocol_whitelist`?
3. **Risk band.** Is the target protocol's assigned risk score `≤ risk_band`?
4. **Per-route cap.** Is the route amount `≤ per_route_cap_usdc`?
5. **Daily cap.** Would executing this route push today's total above
   `daily_cap_usdc`?

The on-chain check enforces the same five rules but operates on the actual
call data being signed, not on a high-level plan. Each rule maps to a
specific selector check inside the session key facet.

## What a rejection looks like

A rejected plan does not silently disappear. The engine emits a structured
`PolicyRejection` and the deterministic loop writes it into the
`EvaluatorThought` for that tick:

```json theme={null}
{
  "rule": "protocol_whitelist",
  "reason": "(protocol, chain) pair not in whitelist",
  "candidate": {
    "source_chain": "arc_testnet",
    "target_chain": "<l2_chain>",
    "target_protocol": "<lending_protocol>",
    "amount_usdc": "5000000"
  },
  "policy_snapshot_hash": "0x7f3a..."
}
```

Three things matter about this structure:

* **`rule` is the failed rule name**, not a free-text reason. This lets the
  UI and audit tooling categorize failures without parsing strings.
* **`policy_snapshot_hash`** lets a reader verify *which version* of the
  policy was in force when the check ran. Policy changes are on-chain
  transactions; rejections can be tied back to the exact policy state.
* **The full `candidate` is preserved**, so the rejection can be replayed
  against a different policy (e.g. "would this plan have passed if my risk
  band were 7?").

## What happens when a plan is rejected

The deterministic loop treats rejection as a normal outcome:

* **No transaction is broadcast.** USDC does not move. No gas is spent.
* **The `EvaluatorThought` is still written.** This includes the
  `PolicyRejection` plus the full state snapshot from steps 1–3. See
  [Audit trail](/architecture/audit-trail).
* **The scheduler does not immediately retry the same plan.** Re-running
  the evaluator on the same inputs produces the same rejection. The
  scheduler waits for a relevant state change before re-evaluating.
* **The user can see the rejection.** Both the chat panel (if the plan came
  from chat) and the dashboard surface the failed rule name.

This is also the path for rejections initiated by the chat agent. A user
prompt that produces an out-of-policy plan results in a `PolicyRejection`
with the same structure, surfaced in the chat panel rather than as a tick
log.

## Policy updates and in-flight plans

Policy is mutable — owners change rules from the dashboard at any time. The
engine handles concurrent policy updates as follows:

* **A policy update is a normal on-chain transaction.** It commits a new
  policy state with a new snapshot hash.
* **In-flight plans are re-checked against the current policy before
  signing.** A plan generated under the old policy can be rejected by the
  new one. This is intentional — if a user tightens their rules, the next
  tick respects the new rules immediately.
* **In-flight Gateway intents are not rolled back.** If USDC is mid-flight
  between chains and the user tightens their chain whitelist mid-route, the
  intent still settles (it has already been signed). The new policy applies
  to the next tick.

## Why a separate engine

A reasonable question: why not let the smart account be the only enforcer?

Two reasons:

* **Reasoning over candidates without signing them.** The off-chain engine
  can evaluate hundreds of candidate plans per tick (different venues,
  different amounts) and pick the best one before signing anything. Pushing
  every candidate to the chain for a revert-or-pass check would be
  gas-prohibitive.
* **Structured failure information.** A reverted transaction tells you
  *that* something failed, not *why*. The off-chain engine produces typed
  rejections that the audit trail and UI can act on.

The on-chain check is the safety guarantee. The off-chain engine is the
thing that makes the system usable.

## Next steps

<CardGroup cols={2}>
  <Card title="Session keys" icon="key" href="/contracts/session-keys">
    The on-chain layer that turns rule violations into reverts.
  </Card>

  <Card title="Audit trail" icon="scroll" href="/architecture/audit-trail">
    Where PolicyRejections are recorded and indexed.
  </Card>

  <Card title="The deterministic loop" icon="repeat" href="/architecture/deterministic-loop">
    The five steps of an EvaluatorThought, with check\_policy in context.
  </Card>

  <Card title="Set your rules" icon="sliders" href="/quickstart/set-your-rules">
    The user-facing controls that populate the policy.
  </Card>
</CardGroup>
