usertrust

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:

EnforcementEffect
hardDeny the request. Throws PolicyDeniedError.
softAllow the request with warnings. The governance receipt includes the matched rule and reason.

PolicyResult

Every policy evaluation returns a structured result:

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: 17

Operators

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.

OperatorDescription
existsField is present
not_existsField is absent
eqEqual (strict)
neqNot equal
gtGreater than
gteGreater than or equal
ltLess than
lteLess than or equal
inValue is in array
not_inValue is not in array
containsString contains substring
regexMatches regular expression

See Policy Operators for detailed usage and examples of each operator.

Scope Matching

scopePatterns match the operator-declared scope on the governor 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 environment

If 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: 17

A rule with a time window only matches during the specified period. Outside the window, the rule is skipped entirely.

Priority

Rules are evaluated in priority order. Lower numbers are evaluated first (higher priority):

PriorityConvention
1-10Critical safety rules
10-50Cost and budget rules
50-100Operational rules
100Default 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:

  1. block-budget-overshoot -- Deny when budget_remaining_after lt 0 (hard)
  2. block-budget-exhausted -- Deny when budget_remaining lte 0 (hard)
  3. warn-high-cost -- Warn when estimated_cost gt 1000 (soft)

Further Reading