Skip to main content
Pure-function evaluator for Glide’s AgentPolicyEnvelope. Given an envelope and a PolicyRequest, evaluate() checks 14 independent policy axes and returns a typed verdict with per-axis reason codes. No DB access, no network calls, no side effects — the caller is responsible for fetching velocity aggregates and for deciding whether the verdict enforces on Privy programmable signing policy (EVM) or in a Redis state layer (Solana stateful axes). The evaluator has default-deny semantics: any configured axis that cannot be checked because required input is missing yields a deny reason rather than silently passing. Axes that are undefined or carry empty arrays on the envelope are skipped — “not configured” is not the same as “deny all” for most axes (see the empty-allowlist table below).

Install

npmjs.com/package/@glideco/policy-engine

Why pure functions?

A stateful evaluator couples tests to DB fixtures or Redis state. A pure evaluator is a unit-testable function: every policy scenario is a data fixture in a describe block. The caller fetches velocity aggregates from wherever they live (Redis, Privy state, a Postgres materialized view) and passes them in as VelocityContext. The engine doesn’t care which source they came from. This also means the engine is chain-agnostic. On EVM, Privy programmable signing enforces per_tx_max, counterparty_allowlist, and chain_allowlist natively. On Solana, stateful axes (daily_cap, velocity_max_txs_per_day) enforce in the router’s Redis layer. The same evaluate() call drives both paths.

The 14 policy axes

chain_allowlist: [] + a counterparty present → deny. This is the one axis that defaults to deny rather than open when empty, because chain restriction is a required posture for any agent that can transact.

Basic evaluation

Handling deny

Any deny reason wins over a step-up trigger. The reasons array contains one entry per failing axis:

Missing velocity context

If the envelope configures a stateful axis (daily cap, velocity limits) but the caller omits velocity_context, the evaluator returns deny with reason nil_input_velocity_context rather than silently passing:

Narrowing detection for grant refresh

The package also exports isNarrowingOrUnchanged, used by the agent.grant.refresh MCP tool to determine whether a policy update is safe to apply without a principal step-up:
Narrowing is checked per-axis:
  • Amount caps: tighter = smaller number, or newly defined where previously absent.
  • Allowlists: tighter = subset of the old list (every new entry must be in the old set).
  • Blocklist: tighter = superset of the old list (removing a blocked MCC is broadening).
  • Any single broadening on any axis → narrowed: false.

Reading list