Evaluator interprets a policy bundle against a tool-call request and returns allow / deny synchronously. It runs in your process, with no network round-trip on the hot path.
Constructor
evaluate(request)
EvaluationRequest is an arbitrary object with tool_name (required) and an optional agent_id. Add any other fields your policies reference as dot-paths (input.command, kwargs.amount, etc.).
Returns an EvaluationResult:
Algorithm
- Frozen-agent kill-switch. If
bundle.frozenAgentIdscontainsrequest.agent_id(case-insensitive), returndeny / AGENT_FROZENbefore any rule fires. - Empty-bundle fail-closed. If no bundle is loaded or the bundle has no policies, return
deny / NO_POLICIES. - Per-policy compile check. If any policy contains a
matchesrule whose regex failed to compile, returndeny / POLICY_COMPILE_ERRORwhen that policy is reached. - Rule scan. For every
(policy, rule)pair, AND all conditions. On match:- Record the rule as the current “matched” rule.
- If the rule’s effect is
deny, return immediately. - If
allow, keep scanning — a later deny wins.
- Fall-through. If no rule matched, return the first policy’s
spec.defaultEffect.
Operators
Field resolution
Dot-paths walk the request object:undefined, which compares unequal to anything a policy would eq against. __proto__, constructor, and prototype parts are rejected at the schema layer to prevent prototype-walking; resolveField also uses Object.hasOwn so inherited properties don’t resolve.
Wall-clock budget
Eachevaluate() call is bounded to 50 ms of total work. If a pathologically large bundle would exceed that, evaluation bails out with deny / EVAL_TIMEOUT rather than blocking the event loop. The check fires at rule boundaries, so the actual upper bound is “current rule’s evaluation time + 50 ms.”
For a healthy 1000-rule bundle on a modern laptop, evaluation typically completes in 1–2 ms — well under the budget.