Policy Engine
Rule-based governance with 12 field operators, scope globs, and time windows.
The policy engine evaluates every governed LLM call against a set of rules before the call is forwarded to the provider. Rules are defined in YAML or JSON and support field matching, scope globs, time windows, and priority ordering.
Enforcement Levels
Each rule specifies an enforcement level that determines what happens when the rule matches:
| Enforcement | Effect |
|---|---|
| hard | Deny the request. Throws PolicyDeniedError. |
| soft | Allow the request. A soft-only match is invisible to the caller: the receipt carries no rule or reason, the llm_call audit event records nothing about it, and every call site reads the evaluation result only inside its decision === "deny" branch. hasWarnings and softViolations live only on the internal PolicyResult, which is not exported. A soft rule's [WARN] reason surfaces in exactly one place: the PolicyDeniedError message, when a hard rule denies the same call. |
Enforcement also decides what happens when a condition cannot be evaluated — the field is absent from the context, or has the wrong type for a numeric or string operator (every operator except exists / not_exists reports an absent field as indeterminate; eq, neq, in and not_in compare strictly, so a value of another type is a determinate comparison, false for eq and in and true for neq and not_in). A hard rule treats an indeterminate condition as satisfied and still fires (fail closed); a soft rule treats it as unmet and stays silent. So a hard rule on model denies a governed action call, which carries no model; lead such a rule with an exists condition on the same field when it must apply only where the field is populated.
PolicyResult
Every policy evaluation returns a structured result. This type and evaluatePolicy() are internal to the package — neither is exported from usertrust or usertrust/headless — so a governed call observes a policy decision in-band only as a PolicyDeniedError, whose reason joins one entry per matched violation with ; : the rule's [id] (or [name]) label followed by its description (or, for a rule that has an id but no description, its name), with soft matches prefixed [WARN]. Only the denial's audit event is filtered to the hard rules:
interface PolicyResult {
decision: "allow" | "deny";
hasWarnings: boolean;
matched: RuleMatch[];
hardViolations: RuleMatch[];
softViolations: RuleMatch[];
reasons: string[];
evaluatedAt: string;
}Rule Structure
A policy rule specifies conditions, scope, enforcement, and optional time windows:
# usertrust.config.json — operator-declared host scope (required for the rule below)
# { "budget": 100000, "scope": ["production/api"] }
# .usertrust/policies/default.yml
rules:
- name: block-expensive-models
description: Deny requests to opus-class models in production
effect: deny
enforcement: hard
priority: 10
scopePatterns: ["production/*"]
conditions:
- field: model
operator: contains
value: "opus"
timeWindows:
- daysOfWeek: [1, 2, 3, 4, 5] # Mon-Fri
startHour: 9
endHour: 17Operators
The policy engine supports 12 field operators for matching conditions. Each operator compares a field value (resolved via dot-notation) against the rule's expected value.
| Operator | Description |
|---|---|
exists | Field is present |
not_exists | Field is absent |
eq | Equal (strict) |
neq | Not equal |
gt | Greater than |
gte | Greater than or equal |
lt | Less than |
lte | Less than or equal |
in | Value is in array |
not_in | Value is not in array |
contains | String contains substring |
regex | Matches regular expression |
See Policy Operators for detailed usage and examples of each operator.
Scope Matching
scopePatterns match the operator-declared scope on the controller or config — never request content. Set it the same way you set budget or parentUserId:
await trust(client, { scope: ["production/api"] });
// or in usertrust.config.json: { "scope": ["production/api"] }The patterns themselves are minimatch globs:
scopePatterns: ["production/*"] # all production sub-scopes
scopePatterns: ["development/**"] # development and all nested scopes
scopePatterns: ["*/billing"] # billing scope in any environmentIf the operator sets no scope, a rule with scopePatterns never matches — adding a pattern does not "narrow a live rule"; it stays inert until the host declares a matching scope. A request body / params.params.scope cannot volunteer one.
If a rule has no scopePatterns field, it matches all calls.
Time Windows
Rules can be restricted to specific days and hours:
timeWindows:
- daysOfWeek: [1, 2, 3, 4, 5] # 0=Sunday, 6=Saturday
startHour: 9 # 24-hour format
endHour: 17A rule with a time window only matches during the specified period. Outside the window, the rule is skipped entirely. Hours are compared in the controller process's local time (getDay / getHours): startHour (0–23) is inclusive and endHour (0–24) is exclusive, and a window whose startHour is greater than its endHour wraps midnight on the same local day. Either bound may be omitted.
Priority
Rules are evaluated in priority order. Lower numbers are evaluated first (higher priority):
| Priority | Convention |
|---|---|
| 1-10 | Critical safety rules |
| 10-50 | Cost and budget rules |
| 50-100 | Operational rules |
| 100 | Default priority |
If multiple rules match, all hard violations result in denial. Soft violations accumulate as warnings.
Default Rules
usertrust ships with three built-in rules:
- block-budget-overshoot — Deny when
budget_remaining_afterlt0 (hard) - block-budget-exhausted — Deny when
budget_remaininglte0 (hard) - warn-high-cost — Warn when
estimated_costgt1000 (soft)
The built-ins carry priorities 1, 2 and 50 and are always enforced: a custom policy file is concatenated on top of them (mergePolicies), so user rules can add deny or warn rules but cannot remove, disable, or override a built-in — enabled: false or a rule reusing a built-in's id simply appends another rule while the original stays active. The default.yml that usertrust init writes contains a copy of these three rules; deleting them from the file does not turn them off.
Further Reading
- Policy Rules -- Full rule definition reference
- Policy Operators -- Detailed operator documentation with examples
- PII Detection -- Built-in PII detection and redaction