Skip to content

Policy evaluation flow

Policy evaluation in Arbitex is a two-phase process. Phase 1 detects sensitive entities in the prompt or response text. Phase 2 evaluates the detected entities against a policy chain to determine what action to take. These phases are independent by design: all entity detection completes before any action is determined. The engine never short-circuits detection to act faster — you always get a complete picture of what was found before a decision is made.

This guide is aimed at engineers integrating with the policy engine and security architects designing policy chains. It covers the internal mechanics of both phases, the combining algorithms, rule condition semantics, action severity ordering, and audit trail generation.

Source: backend/app/services/policy_engine.py


The two phases of policy evaluation are deliberately decoupled:

  • Phase 1 — DLP Scan: Run all four detection tiers exhaustively against the request text. Collect every entity match. Deduplicate overlapping spans. Apply confidence threshold filtering. Produce a DLPScanResult.
  • Phase 2 — Policy Chain Evaluation: Build a PolicyContext from the DLP results and request metadata. Evaluate the applicable policy chains in order. Apply the combining algorithm to determine the final PolicyDecision.

No rule can influence what gets detected. No detection result can skip rule evaluation. The boundary between Phase 1 and Phase 2 is a clean data handoff.

┌─────────────────────────────────────────────────────────────────┐
│ PHASE 1: DLP SCAN │
│ │
│ Input text │
│ │ │
│ ├──► Tier 1: Regex (76 patterns, deterministic) │
│ ├──► Tier 2: NER (spaCy en_core_web_sm, probabilistic) │
│ ├──► Tier 3: DeBERTa NLI (contextual validation) │
│ └──► Tier 4: CredInt (bloom filter, concurrent) │
│ │
│ All tiers run. All matches collected. │
│ Deduplication + confidence threshold applied. │
│ Output: DLPScanResult { matches: [...] } │
└──────────────────────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ PHASE 2: POLICY CHAIN EVALUATION │
│ │
│ DLPScanResult + request metadata → PolicyContext │
│ │
│ 1. Evaluate user chain (scope="user") │
│ 2. Evaluate org chain (scope="org") │
│ │
│ Combining algorithm: first_applicable or deny_overrides │
│ │
│ Output: PolicyDecision { action, matched_rule, ... } │
└──────────────────────────────────┬──────────────────────────────┘
AuditLog entry written

The DLP scan runs four detection tiers. All four tiers run exhaustively against the full text — detection does not stop when one tier produces a match.

76 compiled regex patterns covering structured PII and credential formats: SSNs, credit card numbers, IBANs, passport numbers, driver’s license formats by jurisdiction, API key prefixes, JWT patterns, and more. Regex detection is deterministic — a match either occurs or it does not. Confidence is always 1.0 for regex matches. Regex patterns are jurisdiction-aware: the US SSN pattern is distinct from the UK NI number pattern.

spaCy en_core_web_sm NER model identifies contextually-recognized entities: PERSON names, ORG names, email addresses, phone numbers, dates, locations, and custom entity labels registered via the spaCy pipeline. NER is probabilistic — the model produces a confidence score for each entity. Unlike regex, NER understands context: “John Smith” in a medical record context scores higher than in a list of fictional characters.

DeBERTa Natural Language Inference validates and potentially demotes Tier 2 NER findings by scoring whether the surrounding context entails the entity label. A detected PERSON entity might be demoted if DeBERTa determines the surrounding text is discussing a historical figure in an academic context rather than transmitting live PII.

This tier requires explicit opt-in:

Terminal window
DLP_DEBERTA_ENABLED=true
DEBERTA_MODEL_PATH=/path/to/deberta-v3-base-mnli

When disabled, Tier 2 NER findings pass through unmodified.

CredInt operates concurrently with Tiers 1–3. The Outpost runs a bloom filter check against a locally-synced index of known-compromised credentials. Only the first 8 hex characters of the SHA-1 hash of a candidate credential are transmitted for lookup — the full credential is never sent to any external service. CredInt produces a match record when a candidate credential matches the compromised index with a confidence score derived from the frequency bucket of that credential in known breach datasets.

Candidate credentials for CredInt lookup are sourced from Tier 1 regex matches whose entity type is in the CredInt-eligible set (e.g., API_KEY, OAUTH_TOKEN, PASSWORD_PATTERN). CredInt does not independently identify credential candidates — it validates regex candidates against the known-compromise index. This design means CredInt adds zero false-positive risk for entity types that are not first detected by Tier 1.

After all tiers complete, the engine deduplicates matches:

  • Same entity type, same span: only the match with the highest confidence score is kept.
  • Different entity types at the same span: both matches are kept. A string that is simultaneously an EMAIL and a PERSON (e.g., a named email alias) produces two distinct match records.
  • Overlapping but not identical spans: both kept. Downstream rule conditions can match on either.

After deduplication, matches with confidence below the threshold are dropped. The default threshold is 0.5. This threshold is configurable per-deployment via DLP_CONFIDENCE_THRESHOLD.

@dataclass
class DLPScanResult:
matches: list[DLPMatch]
@dataclass
class DLPMatch:
detector_name: str # e.g. "regex_ssn_us", "ner_person", "deberta_nli"
entity_type: str # e.g. "SSN", "PERSON", "API_KEY", "CREDIT_CARD"
matched_text: str # the actual matched string
start: int # character offset, inclusive
end: int # character offset, exclusive
confidence: float # 0.0 to 1.0

Policy chain evaluation begins by assembling a PolicyContext from the DLP scan result and request metadata.

PolicyContext is the complete state available to every rule condition. Rules can only observe what is in PolicyContext — they cannot query external systems at evaluation time.

Field Type Description
user_id UUID Identity of the requesting user
tenant_id UUID Tenant (organization) identifier
provider str AI provider name, e.g. "anthropic", "openai"
model str Model name, e.g. "claude-opus-4-6"
prompt_text str Full text of the request (input) or response (output)
detected_entities list[dict] Entities from DLP scan: {entity_type, text, confidence, start, end}
user_groups list[str] SCIM/directory group memberships for the user
user_risk_score float Risk score from CredInt and GeoIP enrichment (0.0–1.0)
direction str "input" or "output"
intent_complexity str or None "simple", "medium", "complex", or None
channel str "interactive" or "api" (default "api")
model_risk_tier str or None From model registry; None indicates unclassified model
content_categories list[str] L1 keyword classifier category labels

Policy chains are evaluated in a fixed order that mirrors the Palo Alto firewall policy model — most specific to least specific:

  1. User chain (scope="user", scope_id=user_id) — personal policy overrides for this specific user.
  2. Org chain (scope="org", scope_id=tenant_id) — the primary organizational policy chain applied to all users in the tenant.

If the user chain produces a terminal decision, the org chain is not evaluated. If the user chain produces no match (or only non-terminal REDACT accumulations), evaluation continues to the org chain.

How accumulated REDACTs carry across chains

Section titled “How accumulated REDACTs carry across chains”

If the user chain accumulates one or more REDACT transformations but does not produce a terminal action, those accumulated redactions are preserved and passed into the org chain evaluation. The org chain then continues accumulating or terminates with its own decision. The final PolicyDecision.accumulated_redacts is the union of all REDACT transformations collected across both chains.

This means a user-chain REDACT rule and an org-chain REDACT rule can both fire in sequence, producing a decision that applies both transformations before forwarding the request.

User chain (first_applicable):
R1 (seq=1): entity_types=["EMAIL"] → REDACT (non-terminal, accumulated)
(no more matching rules — chain exhausted without terminal action)
Pass accumulated_redacts=[EMAIL redaction] to org chain.
Org chain (first_applicable):
R1 (seq=1): entity_types=["SSN"] → REDACT (non-terminal, accumulated)
R2 (seq=2): catch-all → ALLOW (terminal)
Final: PolicyDecision(
action="ALLOW",
accumulated_redacts=[EMAIL redaction, SSN redaction]
)
→ Request forwarded with both EMAIL and SSN replaced.

Each policy chain specifies a combining algorithm that governs how multiple rules within the chain interact. Two algorithms are supported.

Rules are evaluated in priority order. Lower sequence number = higher priority. The first rule whose conditions all match AND whose action is terminal determines the result. Evaluation stops immediately at that rule.

REDACT is non-terminal: if a rule matches and its action is REDACT, the transformation is accumulated but evaluation continues to the next rule.

Rule Set (first_applicable):
R1 (seq=1): entity_types=["SSN"] → REDACT (non-terminal)
R2 (seq=2): user_groups=["finance"] → ALLOW (terminal)
R3 (seq=3): catch-all → BLOCK (terminal)
Evaluation for user NOT in finance, SSN detected:
R1: SSN detected? YES → REDACT accumulated, continue
R2: user in finance? NO → skip
R3: catch-all → BLOCK (terminal)
Result: BLOCK (with accumulated REDACT recorded in decision)
Evaluation for user IN finance, SSN detected:
R1: SSN detected? YES → REDACT accumulated, continue
R2: user in finance? YES → ALLOW (terminal)
Result: ALLOW (R3 never reached)

All rules in the chain are evaluated regardless of earlier matches. Deny actions win over everything.

  • BLOCK or CANCEL matched by any rule: immediately terminal — deny wins, stop evaluating.
  • ALLOW or ROUTE_TO: collected but evaluation continues.
  • REDACT: transformation accumulated, evaluation continues.
  • If no deny found after all rules: return the most severe collected non-deny decision by severity score.
Rule Set (deny_overrides):
R1 (seq=1): entity_types=["API_KEY"] → BLOCK (deny)
R2 (seq=2): entity_types=["SSN"] → REDACT
R3 (seq=3): catch-all → ALLOW
Evaluation for request with both SSN and API_KEY detected:
R1: API_KEY detected? YES → BLOCK matched → immediately terminal
Result: BLOCK (R2 and R3 never evaluated)
Evaluation for request with SSN only:
R1: API_KEY detected? NO → skip
R2: SSN detected? YES → REDACT accumulated
R3: catch-all → ALLOW collected
No deny found. Most severe non-deny: REDACT (severity 3) > ALLOW (severity 0).
Result: REDACT

Each rule has a set of conditions. All conditions within a single rule use AND logic — every condition in the rule must match for the rule to apply. If any condition fails, the rule is skipped.

Before conditions are evaluated, the rule’s applies_to field is checked against context.direction:

applies_to value Applies when
"input" context.direction == "input" only
"output" context.direction == "output" only
"both" Always (default)

If applies_to does not match the current direction, the rule is skipped entirely — conditions are not evaluated.

Condition key Match logic
user_groups User is a member of ANY group in the listed values
entity_types At least one detected entity has a type in the list AND confidence >= entity_confidence_min (default 0.5)
content_regex re.search(pattern, context.prompt_text) returns a match
providers context.provider is in the listed values
models context.model is in the listed values
user_risk_score_min context.user_risk_score >= threshold
intent_complexity Exact string match against context.intent_complexity
channel context.channel is in the listed values
model_risk_tier Tier value (or "unclassified" when context.model_risk_tier is None) is in the listed values
content_categories Any listed category is a prefix of any value in context.content_categories

A rule with no conditions defined always matches. This is used intentionally as the last rule in a chain to define a default action (commonly ALLOW or BLOCK) for any request that no earlier rule matched.

The entity_types condition supports an optional entity_confidence_min field per rule (distinct from the global DLP confidence threshold). This allows a rule to require higher confidence for a match:

conditions:
entity_types:
- SSN
- CREDIT_CARD
entity_confidence_min: 0.8 # only match if confidence >= 0.8

The global DLP threshold (default 0.5) filters matches before they reach the policy engine. entity_confidence_min applies a second, rule-specific filter on top of that.

The following shows how a multi-condition rule is evaluated. All conditions use AND logic:

Rule:
applies_to: "input"
conditions:
providers: ["anthropic", "openai"]
entity_types: ["SSN"]
entity_confidence_min: 0.75
user_groups: ["contractors"]
user_risk_score_min: 0.4
Evaluation:
1. applies_to="input" vs direction="input" → PASS
2. providers: context.provider="anthropic" in ["anthropic", "openai"] → PASS
3. entity_types: SSN detected at confidence 0.82 >= 0.75 → PASS
4. user_groups: user is in "contractors" → PASS
5. user_risk_score_min: user_risk_score=0.61 >= 0.4 → PASS
All 5 conditions passed → rule MATCHES → action applied.

If any single condition fails (e.g., the user is not in contractors), the entire rule is skipped regardless of how many conditions passed.


Actions have a numeric severity score used by the deny_overrides algorithm to determine which action wins when no deny is found.

ACTION_SEVERITY = {
"ALLOW": 0,
"PROMPT": 1,
"ROUTE_TO": 2,
"REDACT": 3,
"CANCEL": 4,
"BLOCK": 5,
}

Higher severity wins. In deny_overrides, if no BLOCK or CANCEL is found and multiple non-deny actions were collected, the action with the highest severity score is returned as the final decision.

Action Meaning
ALLOW Request proceeds unmodified
ALLOW_WITH_OVERRIDE Request proceeds; user acknowledged a policy warning
PROMPT User must acknowledge a policy warning before proceeding
ROUTE_TO Request is transparently rerouted to a different model
REDACT Matched entities are replaced with redaction placeholders before forwarding
CANCEL Request is cancelled (client receives error); not a hard security block
BLOCK Request is blocked and logged as a policy violation

ROUTE_TO redirects the request to an alternate model without informing the user. The PolicyDecision carries either route_to_model (specific model name) or route_to_tier (model tier, resolved to the tenant’s configured default model for that tier at dispatch time). If both are set, route_to_model takes precedence.

A common pattern is routing high-risk-score users to a monitored model with reduced capability:

conditions:
user_risk_score_min: 0.7
action: ROUTE_TO
route_to_tier: "monitored"

PROMPT presents the user with an admin-configured message and requires them to click through before the request proceeds. This is the soft-block pattern: the request is not denied, but the user must acknowledge the policy concern.

ALLOW_WITH_OVERRIDE is used when a user has already acknowledged a PROMPT — the second evaluation (after acknowledgment) produces ALLOW_WITH_OVERRIDE rather than PROMPT to indicate the override was recorded. Both the original PROMPT and the subsequent ALLOW_WITH_OVERRIDE appear in the audit log, forming a consent trail.

prompt_message in PolicyDecision contains the admin-configured message text displayed to the user.


The terminal/non-terminal distinction matters only in first_applicable mode.

All of the following actions are terminal in first_applicable:

  • ALLOW
  • ALLOW_WITH_OVERRIDE
  • BLOCK
  • CANCEL
  • ROUTE_TO
  • PROMPT

When a rule matching one of these actions is reached, evaluation stops. The chain does not continue to lower-priority rules.

Non-terminal actions (accumulate and continue)

Section titled “Non-terminal actions (accumulate and continue)”

REDACT is the only non-terminal action. When a rule matches with REDACT:

  1. The redaction transformation is appended to accumulated_redacts in the building decision.
  2. Evaluation continues to the next rule in priority order.
  3. Multiple REDACT rules can accumulate multiple transformations in a single pass.
  4. The final terminal rule determines the action field in PolicyDecision. If the chain reaches a terminal ALLOW, the accumulated redactions are still applied (the request is allowed but with redacted content).

Example 1: Single entity (SSN), first_applicable, user not in finance group

Section titled “Example 1: Single entity (SSN), first_applicable, user not in finance group”

Rule chain (first_applicable):

Seq Conditions Action
1 entity_types: ["SSN"] REDACT
2 entity_types: ["SSN"], user_groups: ["finance"] ALLOW
3 (catch-all) BLOCK

Request: User is not a member of the finance group. SSN detected by Tier 1 regex, confidence 0.92.

Evaluation trace:

R1 (seq=1): entity_types=["SSN"] → SSN present at confidence 0.92 >= 0.5 ✓
Action: REDACT (non-terminal) → accumulated_redacts += SSN redaction
Continue to next rule.
R2 (seq=2): entity_types=["SSN"] → SSN present ✓
user_groups=["finance"] → user NOT in finance ✗
Condition failed → skip.
R3 (seq=3): (no conditions) → catch-all matches ✓
Action: BLOCK (terminal) → stop.
Final: PolicyDecision(action="BLOCK", accumulated_redacts=[{entity_type: "SSN", ...}])

The REDACT from R1 is recorded in the decision but the request is blocked — the redaction was accumulated but not applied since BLOCK is the terminal outcome.


Example 2: Multi-entity (SSN + API_KEY), deny_overrides

Section titled “Example 2: Multi-entity (SSN + API_KEY), deny_overrides”

Rule chain (deny_overrides):

Seq Conditions Action
1 entity_types: ["API_KEY"] BLOCK
2 entity_types: ["SSN"] REDACT
3 (catch-all) ALLOW

Request: Both SSN (confidence 0.87) and API_KEY (confidence 1.0) detected.

Evaluation trace:

R1 (seq=1): entity_types=["API_KEY"] → API_KEY present at confidence 1.0 ✓
Action: BLOCK → deny action → immediately terminal, stop.
R2 and R3 never evaluated.
Final: PolicyDecision(action="BLOCK", matched_rule_id=<R1 id>, match_reason="API_KEY detected")

In deny_overrides, a single BLOCK from any rule wins immediately. Even though SSN was also detected and R2 would have produced a REDACT, the BLOCK from R1 takes precedence and stops evaluation.


Example 3: Entity detected, no rule matches

Section titled “Example 3: Entity detected, no rule matches”

Rule chain (first_applicable):

Seq Conditions Action
1 entity_types: ["SSN"] BLOCK
2 entity_types: ["CREDIT_CARD"] REDACT

Request: EMAIL address detected (confidence 0.78). No SSN, no CREDIT_CARD.

Evaluation trace:

R1 (seq=1): entity_types=["SSN"] → no SSN in detected_entities ✗ → skip.
R2 (seq=2): entity_types=["CREDIT_CARD"] → no CREDIT_CARD ✗ → skip.
No rules matched. Chain exhausted.
Final: PolicyDecision(
action="ALLOW",
matched_rule_id=None,
match_reason="no policy match -- default ALLOW"
)

The EMAIL entity is recorded in the audit trail with its confidence score. The request passes. This is expected behavior: if your policy chain does not address a particular entity type, the default outcome is ALLOW. If you want a default-deny posture, add a catch-all BLOCK rule as the last rule in the chain.


PolicyDecision is the dataclass returned by the policy engine to the request handler. All fields are recorded in the audit log.

@dataclass
class PolicyDecision:
action: str # ALLOW, BLOCK, CANCEL, REDACT,
# ROUTE_TO, PROMPT, ALLOW_WITH_OVERRIDE
matched_pack_id: UUID | None # policy pack that contained the matching rule
matched_rule_id: UUID | None # specific rule that determined the outcome
matched_sequence: int | None # priority number of the matched rule
match_reason: str # human-readable explanation
redact_replacement: str | None # replacement string for single-rule REDACT
route_to_model: str | None # target model for ROUTE_TO
route_to_tier: str | None # target model tier for ROUTE_TO
message: str | None # message returned to client on BLOCK/CANCEL
eval_duration_ms: float # wall-clock evaluation time in milliseconds
accumulated_redacts: list[dict] # all REDACT transformations accumulated
prompt_message: str | None # admin-configured message for PROMPT /
# ALLOW_WITH_OVERRIDE actions
{
"action": "BLOCK",
"matched_pack_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"matched_rule_id": "11223344-5566-7788-99aa-bbccddeeff00",
"matched_sequence": 1,
"match_reason": "entity type API_KEY detected at confidence 1.00",
"redact_replacement": null,
"route_to_model": null,
"route_to_tier": null,
"message": "Your request was blocked because it contained a credential. Remove credentials from prompts and retry.",
"eval_duration_ms": 4.2,
"accumulated_redacts": [],
"prompt_message": null
}
{
"action": "REDACT",
"matched_pack_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"matched_rule_id": "aabbccdd-eeff-0011-2233-445566778899",
"matched_sequence": 2,
"match_reason": "entity type SSN detected at confidence 0.92",
"redact_replacement": "[REDACTED-SSN]",
"route_to_model": null,
"route_to_tier": null,
"message": null,
"eval_duration_ms": 6.8,
"accumulated_redacts": [
{
"entity_type": "SSN",
"matched_text": "078-05-1120",
"start": 42,
"end": 53,
"replacement": "[REDACTED-SSN]",
"rule_id": "aabbccdd-eeff-0011-2233-445566778899"
}
],
"prompt_message": null
}

Every request evaluated by the policy engine generates an AuditLog entry. The audit log is the authoritative record for compliance reporting, incident investigation, and chain-of-custody verification.

Entries are written synchronously as part of the request lifecycle — the request handler does not return a response to the client until the audit entry has been committed. This ensures there is no window in which a request can be processed but not logged.

Field Type Description
action str Event type, e.g. "prompt_sent", "response_blocked"
model_id UUID Model registry identifier
provider str AI provider name
user_id UUID Requesting user
tenant_id UUID Tenant (organization)
metadata JSONB Detected entities, confidences, matched rule details
hmac str HMAC-SHA256 digest of this entry
previous_hmac str Digest of the preceding entry (chain linkage)
hmac_key_id str Key identifier for HMAC rotation
created_at timestamptz Entry creation timestamp with timezone
Field Type Description
credint_enabled bool Whether CredInt was active for this request
credint_hit bool Whether a compromised credential was detected
frequency_bucket str Breach frequency tier of the matched credential
context_type str Context classification used by CredInt
sha1_prefix str First 8 hex characters of the SHA-1 hash only — never the full hash
credint_confidence float CredInt match confidence score
Field Description In HMAC chain
src_ip Source IP address (inet) Yes
dst_ip Destination IP address (inet) Yes
country GeoIP-resolved country No
region GeoIP-resolved region No
city GeoIP-resolved city No
isp ISP name No
asn Autonomous system number No

Each audit entry contains an HMAC-SHA256 digest computed over a canonical serialization of the entry’s tamper-evident fields. That digest is stored in the entry’s hmac field and also written to the next entry’s previous_hmac field, forming a hash chain analogous to a blockchain ledger.

Chain properties:

  • Tamper detection: Any modification to a past entry invalidates its HMAC, which breaks the chain from that point forward. Verification tools traverse the chain from oldest to newest and report the first break.
  • Key rotation: hmac_key_id records which key was active when the entry was signed. Key rotation does not break chain continuity — entries signed with old keys are verified with the old key, new entries with the new key.
  • What is in the chain: user_id, tenant_id, action, provider, model_id, src_ip, dst_ip, metadata (including detected entities and matched rule), created_at, and previous_hmac. GeoIP enrichment fields are excluded.
Entry N-1 Entry N Entry N+1
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ...fields... │ │ ...fields... │ │ ...fields... │
│ hmac: H(N-1) │──────► │ previous_hmac: │ │ previous_hmac: │
│ │ │ H(N-1) │──────► │ H(N) │
│ │ │ hmac: H(N) │ │ hmac: H(N+1) │
└─────────────────┘ └─────────────────┘ └─────────────────┘

To verify chain integrity for a tenant, iterate all entries in created_at order and re-compute HMAC for each entry using the key referenced by hmac_key_id. Compare against the stored hmac. Verify the previous_hmac of entry N matches the hmac of entry N-1.

Querying the audit log for policy decisions

Section titled “Querying the audit log for policy decisions”

The metadata JSONB field is indexed with GIN for efficient querying. Common audit queries:

-- All BLOCK decisions for a tenant in the last 7 days
SELECT created_at, user_id, metadata
FROM audit_logs
WHERE tenant_id = $1
AND action = 'prompt_blocked'
AND created_at > now() - interval '7 days'
ORDER BY created_at DESC;
-- All requests that triggered SSN detection
SELECT created_at, user_id, action, metadata
FROM audit_logs
WHERE tenant_id = $1
AND metadata @> '{"detected_entities": [{"entity_type": "SSN"}]}'
ORDER BY created_at DESC;
-- CredInt hits in the last 30 days
SELECT created_at, user_id, credint_confidence, frequency_bucket, sha1_prefix
FROM audit_logs
WHERE tenant_id = $1
AND credint_hit = true
AND created_at > now() - interval '30 days'
ORDER BY credint_confidence DESC;

The user receives an error response. The block message is configurable per rule:

{
"error": "Request blocked by policy",
"message": "This request contains content that is not permitted under your organization's policy."
}

The specific message text is set by the admin in the rule’s action configuration.

The request is silently cancelled. The user receives a generic error (similar to a network timeout). No block message is shown. Use CANCEL when you want to suppress a request without revealing that a policy matched.

The user’s prompt text reaches the model with matched content replaced by the configured redaction string (e.g., [REDACTED]). The user is not notified that redaction occurred unless the admin has configured a notification. The model responds based on the redacted text.

The request is forwarded to a different model than the user requested — either a specific model (route_to_model) or a tier (haiku, sonnet, opus). The user receives a response, but from the routed model. No notification is shown to the user.

The user is presented with a governance challenge dialog before the request proceeds:

[Admin-configured challenge message]
[ Acknowledge and continue ] [ Cancel ]

If the user acknowledges, the request is allowed. If the user cancels, the request is not sent. PROMPT only fires for channel=interactive requests. API channel requests are not affected by PROMPT rules and receive HTTP 449 directly.

Similar to PROMPT, but the user sees an override acknowledgment message and explicitly consents before the request proceeds:

[Admin-configured override message]
[ I understand, continue ] [ Cancel ]

ROUTE_TO redirects the request to a different model or model tier when a rule matches. Use it for cost-based routing, capability-based routing, or risk-based routing.

{
"type": "ROUTE_TO",
"route_to_model": "claude-haiku-4-5-20251001"
}
{
"type": "ROUTE_TO",
"route_to_tier": "haiku"
}

Valid tier values: haiku, sonnet, opus. The engine resolves the tier to the configured model for that tier at evaluation time. If both route_to_model and route_to_tier are set, route_to_model takes precedence.

Route simple requests to a lower-cost model:

Pack: "Cost Optimization"
Rule 1: intent_complexity=simple → ROUTE_TO tier=haiku
Rule 2: intent_complexity=complex → ROUTE_TO tier=opus
(no rule for medium → falls through to default model)

ROUTE_TO example: risk-score-based routing

Section titled “ROUTE_TO example: risk-score-based routing”

Route elevated-risk users to a restricted model:

Pack: "Risk-Based Routing"
Rule: user_risk_score_min=0.7, providers=[openai] → ROUTE_TO model=gpt-4o-mini

13. User chain vs org chain — practical distinction

Section titled “13. User chain vs org chain — practical distinction”

The org chain (scope="org") is the primary policy chain for your organization. All users in the org are subject to it. Rules in the org chain can use user_groups conditions to target specific groups without requiring separate per-user chains.

Org chain (first_applicable):
Pack: "Compliance Baseline"
Rule 1: entity_types=[SSN, CREDIT_CARD] → BLOCK (applies to all users)
Rule 2: user_groups=["finance"], entity_types=[BANK_ACCOUNT] → ALLOW (finance team exempt)
Rule 3: entity_types=[BANK_ACCOUNT] → BLOCK (all other users)

The user chain (scope="user") contains overrides for a specific user. It is evaluated first — before the org chain. A terminal action in the user chain prevents the org chain from being evaluated at all.

Use user chains sparingly. The recommended pattern is to express group-level overrides via user_groups conditions in the org chain rather than creating individual user chains.


Pattern 1: DLP baseline with group override

Section titled “Pattern 1: DLP baseline with group override”

Block PII for all users, but allow the security-audit group with an acknowledged override:

Org chain (first_applicable):
Pack: "Security Audit Override" (sequence 1)
Rule: user_groups=["security-audit"] → ALLOW_WITH_OVERRIDE
Pack: "DLP Baseline" (sequence 2)
Rule: entity_types=[SSN, CREDIT_CARD, PASSPORT] → BLOCK

Pattern 2: Cost-tiered routing with compliance block

Section titled “Pattern 2: Cost-tiered routing with compliance block”

Implement cost optimization routing but ensure compliance blocks always fire using deny_overrides:

Org chain (deny_overrides):
Pack: "Cost Routing" (sequence 1)
Rule: intent_complexity=simple → ROUTE_TO tier=haiku
Rule: intent_complexity=complex → ROUTE_TO tier=opus
Pack: "Compliance Block" (sequence 2)
Rule: content_regex="export controlled|ITAR|EAR" → BLOCK

With deny_overrides, the compliance block in Pack 2 fires even if Pack 1 has already produced a routing decision.

Pattern 3: Channel-gated PROMPT governance

Section titled “Pattern 3: Channel-gated PROMPT governance”

Present a governance challenge only for interactive users, not API callers:

Org chain (first_applicable):
Pack: "Interactive Governance"
Rule: channel=["interactive"], content_regex="generate.*code" → PROMPT
prompt_message="Code generation requires confirmation. Proceed?"

Route high-risk users to a restricted model regardless of their request:

Org chain (first_applicable):
Pack: "Risk Escalation"
Rule: user_risk_score_min=0.8 → ROUTE_TO model=gpt-4o-mini
Pack: "Default Policy"
Rule: (no conditions — catch-all) → ALLOW