Skip to main content

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.

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. For where this fits in a tick, see The 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:
FieldTypeSet by
risk_bandu8 (1–10)Owner
protocol_whitelistSet<(protocol, chain)>Owner
chain_whitelistSet<chain>Owner
per_route_cap_usdcu128Owner
daily_cap_usdcu128Owner
Every field is owner-only. The agent’s session key has no permission to modify any of them. See 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: policy engine diagram
  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.
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.

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:
{
  "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.
  • 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

Session keys

The on-chain layer that turns rule violations into reverts.

Audit trail

Where PolicyRejections are recorded and indexed.

The deterministic loop

The five steps of an EvaluatorThought, with check_policy in context.

Set your rules

The user-facing controls that populate the policy.