> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visiqlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rules

> Define retrieval policies for your AI agents with trust tiers, surfaces, and per-branch masking.

## How Rules Work

Rules define what context your agents are permitted to see. Every document or tool result captured by the in-process SDK is evaluated against your rule set. Rules compile into a bundle that SDKs download and cache locally — evaluation happens in-process, with no network round-trip on the retrieval hot path.

There is **one rule engine** for action and retrieval governance: a rule is a single object tagged with the operations it targets (`operations[]` — `action`, `retrieval`, or both), and the same rule source evaluates identically on the server and inside the SDK. Rules created through the retrieval endpoints are tagged `retrieval` automatically; the dashboard rule editor can tag one rule with both operations so a single policy governs a hybrid read-write tool.

Evaluation is a **priority-descending, first-match-wins cascade**: rules are sorted by `priority` (highest number first) and the first rule whose checks and conditions match decides the outcome.

***

## Rule Structure

A rule has the following fields:

| Field                  | Type             | Description                                                                                                                                                            |
| ---------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                 | `string`         | Human-readable name for the rule                                                                                                                                       |
| `description`          | `string`         | Optional longer description                                                                                                                                            |
| `rego_source`          | `string`         | Policy source code (required — see [Policy Format](#policy-format))                                                                                                    |
| `natural_language`     | `string`         | Plain-English statement of the rule, used for AI compilation                                                                                                           |
| `priority`             | `number`         | Rules with a higher number are evaluated first. Default: `0`                                                                                                           |
| `enabled`              | `boolean`        | Whether the rule is active. Disabled rules are excluded from the bundle                                                                                                |
| `trust_tier`           | `string`         | Minimum agent trust tier for this rule (e.g. `tier2`) — lower-trust agents get an immediate deny from this rule                                                        |
| `surface`              | `string`         | Restrict the rule to one communication surface (e.g. `PUBLIC_CHANNEL`)                                                                                                 |
| `principal_exclusions` | `string[]`       | Agent IDs this rule **denies outright** — an agent on the list gets a `PRINCIPAL_EXCLUDED` deny when the cascade reaches this rule                                     |
| `redaction_spec`       | `object`         | Masking directives applied by `redact` decisions (authored in the dashboard rule editor — see [Masking](#masking-redaction_spec))                                      |
| `hitl_fallback`        | `deny` \| `mask` | Fallback for `escalate` branches: with `mask`, escalated content is returned masked immediately (using the rule's masking directives) while the escalation is reviewed |

### Example Rule (JSON)

```json theme={null}
{
  "name": "Deny tier3 from confidential data",
  "description": "Prevent tier3 agents from accessing internal, confidential, or restricted documents",
  "rego_source": "package recall.rules.tier3_guard\n\ndefault decision = \"deny\"\n\ndecision = \"deny\" if {\n  input.trust_tier == \"tier3\"\n  input.resource_metadata.classification in [\"internal\", \"confidential\", \"restricted\"]\n}",
  "natural_language": "Deny tier3 agents from accessing internal, confidential, or restricted data",
  "priority": 50,
  "enabled": true,
  "trust_tier": null,
  "surface": null,
  "principal_exclusions": null
}
```

***

## Trust Tiers and Need-to-Know

Each agent is assigned a **trust tier** by an operator, and (separately) a **business function** classified automatically from what the agent does — you can pin it by hand. Both are server-authoritative: rules read them as `input.agent.trust_tier` and `input.agent.business_function`, and a caller cannot spoof them.

| Trust Tier | Meaning                                                        |
| ---------- | -------------------------------------------------------------- |
| `tier1`    | Highest trust — full access where the agent has a need to know |
| `tier2`    | Standard trust                                                 |
| `tier3`    | Restricted — least-trusted agents                              |

The default protection every tenant ships with is a **curated catalog of 35 default rules** built on a need-to-know matrix: for each protected data category, the outcome combines whether the agent's business function *needs* that category with its trust tier —

* **Need + `tier1`** → full value
* **Need + `tier2`** → escalate for human review (redact for the highest-volume categories)
* **Need + `tier3`** → masked
* **No need-to-know** → masked, or denied for the most sensitive classes
* **Missing/unknown attributes** → falls into a protective branch (fail-closed)

### Document Classifications

Rules gate on the metadata your documents carry — most commonly `resource_metadata.classification` (a sensitivity level: `public`, `internal`, `confidential`, `restricted`) and `resource_metadata.data_categories` (content tags such as `pii`, `pci`, `financial`, `credentials`).

<Warning>Retrieval is fail-closed: when **no rule matches** a document, the decision is `deny` with reason code `DEFAULT_DENY`. In `monitor` mode this is observed but not enforced; in `enforce` mode an uncovered document is suppressed. Roll out with monitor mode and confirm your coverage before enforcing.</Warning>

***

## Surfaces

Surfaces represent the communication channel where the agent's output will be delivered. Rules can restrict context based on the destination surface — a document might be allowed in a private group but denied in a public channel.

| Surface            | Description                          |
| ------------------ | ------------------------------------ |
| `DIRECT_MESSAGE`   | One-to-one private message           |
| `PRIVATE_GROUP`    | Private group or channel             |
| `INTERNAL_CHANNEL` | Company-internal channel             |
| `PUBLIC_CHANNEL`   | Publicly visible channel             |
| `EXTERNAL_CHANNEL` | Channel shared with external parties |

A rule with `surface: "PUBLIC_CHANNEL"` applies only when the agent's output surface is a public channel; on other surfaces the cascade skips it. Rules without a surface restriction match all surfaces.

***

## Policy Format

Every rule's `rego_source` is a policy in a supported subset of Rego, interpreted by the same evaluator on the server and in the SDK — a rule decided one way on the server is decided identically in-process.

```
package recall.rules.<name>

# Default: deny for gating rules (fail-closed); use "allow" only for
# transform-only rules such as redaction.
default decision = "deny"

# One block per outcome. Conditions inside a block are AND-ed;
# add more blocks for OR.
decision = "allow" if {
  # conditions...
}
```

### Input Document

The evaluation input has these fields:

| Field                     | Type     | Description                                                                                                                                                                        |
| ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `input.agent_id`          | `string` | Agent identifier                                                                                                                                                                   |
| `input.trust_tier`        | `string` | `tier1`, `tier2`, or `tier3`                                                                                                                                                       |
| `input.operation`         | `string` | `retrieve`, `tool_call`, or `prompt_render`                                                                                                                                        |
| `input.resource_type`     | `string` | Resource kind, e.g. `document`                                                                                                                                                     |
| `input.resource_metadata` | `object` | Tags on the candidate resource: `data_categories`, `classification`, `owner`, etc.                                                                                                 |
| `input.surface`           | `string` | Communication surface (e.g. `PUBLIC_CHANNEL`)                                                                                                                                      |
| `input.query`             | `string` | Retrieval query string (optional)                                                                                                                                                  |
| `input.normalized`        | `object` | The canonical normalized event (unified schema). A canonical path like `read/data_categories` is referenced as `input.normalized.read.data_categories`                             |
| `input.agent`             | `object` | Server-authoritative agent attributes: `input.agent.trust_tier`, `input.agent.business_function`, `input.agent.categories` — injected from the agent record, never from the caller |

### Supported Conditions

| Form                                 | Meaning                                                 |
| ------------------------------------ | ------------------------------------------------------- |
| `input.path == "value"`              | Equality (also numbers, `true`/`false`)                 |
| `input.path != "value"`              | Inequality                                              |
| `input.path >= 100`                  | Numeric comparison (`>` `>=` `<` `<=`)                  |
| `input.path in ["a", "b"]`           | Scalar field is one of the listed values                |
| `"a" in input.path`                  | Array field contains the value (e.g. `data_categories`) |
| `not <condition>`                    | Negation                                                |
| `startswith(input.path, "v")`        | String prefix (also `endswith`, `contains`)             |
| `regex.match("pattern", input.path)` | Regular-expression test over a string field             |
| `count(input.path) > 0`              | Array/string length comparison                          |
| `input.path`                         | Truthy check                                            |

Constructs outside the subset (`some` iterators, comprehensions, custom functions, cross-field comparisons) are not supported. A condition the engine cannot parse becomes always-false — the rule can never match (fail-closed) — and both the dashboard editor and the AI compiler reject non-evaluable conditions before saving.

<Tip>Prefer fail-closed negation: `not input.agent.trust_tier == "tier1"` matches when the attribute is *absent*, while `input.agent.trust_tier != "tier1"` does not. The curated default catalog uses `not … ==` forms exclusively so a missing attribute protects rather than leaks.</Tip>

### Decisions

| Value      | Effect                                                                                                                                                    |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allow`    | Content passes through unchanged                                                                                                                          |
| `deny`     | Content is withheld entirely                                                                                                                              |
| `redact`   | Content passes through with the rule's masking directives applied                                                                                         |
| `escalate` | The access is recorded for human review. With `hitl_fallback: mask` the content returns masked in the meantime; otherwise it passes through while flagged |

The decision response also carries a machine `reason_code` (`TIER_MISMATCH`, `PRINCIPAL_EXCLUDED`, `POLICY_DENY`, `AUDIENCE_EXPANSION`, `SURFACE_RESTRICTION`, `POLICY_ALLOW`, `DEFAULT_DENY`, `EMERGENCY_BYPASS`, and `MONITOR_MODE` — recorded when a monitor-mode agent's would-be decision is overridden to allow) derived by the engine — you do not author reason codes in the policy.

### Example: Surface-Based Restriction

```
package recall.rules.audience_expansion_guard

default decision = "deny"

# Deny confidential content in public or external channels
decision = "deny" if {
  input.resource_metadata.classification == "confidential"
  input.surface in ["PUBLIC_CHANNEL", "EXTERNAL_CHANNEL"]
}

# Allow confidential content in private contexts
decision = "allow" if {
  input.resource_metadata.classification == "confidential"
  input.surface in ["DIRECT_MESSAGE", "PRIVATE_GROUP", "INTERNAL_CHANNEL"]
}
```

***

## Masking (`redaction_spec`)

A `redact` decision applies the rule's masking directives. Each directive names a structured `field` to mask wherever it appears and/or a regex `pattern` applied to string content, plus a masking mode — surfaced in the rule editor as:

| Editor mode   | Behavior                                                                                                                       |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Replace**   | The whole match is replaced with a fixed string (default `[REDACTED]`)                                                         |
| **Transform** | Format-preserving: keep the first/last N characters, mask the rest (an email variant keeps the first character and the domain) |
| **Custom**    | A keep-pattern regex decides what survives; everything else is masked                                                          |

Masking is authored **per decision branch**: each `redact` branch of a rule carries its own set of directives, so one rule can mask account numbers on its medium-trust branch and everything on its low-trust branch. A rule supports up to 50 directives and 50 masking branches.

Masking is fail-closed end to end: a redact decision whose directives cannot be resolved is denied when the rule opts into fail-closed behavior, and if applying a mask throws at runtime, the SDK excludes the document rather than leaking it unredacted.

***

## Natural Language Rules

You can describe rules in plain English. Retrieval governance compiles them to policy using AI (the `POST /recall/rules/compile` endpoint, rate-limited to 10 requests per minute per organization). No policy-language expertise required.

**Example prompt:** "Deny tier3 agents from accessing any document classified as internal, confidential, or restricted."

The compiler reads your existing rules for context, validates that every condition is evaluable by the engine, and can save the rule directly. The dashboard rule editor offers the same NL-first authoring plus a visual condition builder and a **Simulate** panel that replays your rule against recent real traffic before you save.

Rules authored in the dashboard also pass a **simulated-interference gate**: a rule that would deny or escalate more than 5% of your recent real traffic is rejected (`INTERFERENCE_THRESHOLD_EXCEEDED`) until you make it more specific. Masking never counts as interference — a masked request still proceeds.

<Tip>The compile endpoint supports streaming via `?stream=true` or `Accept: text/event-stream` for real-time feedback during rule compilation.</Tip>

***

## Rule Evaluation Flow

When a retrieved document is evaluated, the engine walks rules from highest priority down. For each rule:

1. **Emergency bypass** — a rule under an active [emergency bypass](/rules/retrieval/emergency-bypass) returns `allow` (`EMERGENCY_BYPASS`) immediately.
2. **Principal exclusions** — an agent listed in the rule's `principal_exclusions` is denied (`PRINCIPAL_EXCLUDED`).
3. **Surface restriction** — a surface-scoped rule applies only when the input surface matches; otherwise the cascade skips it.
4. **Trust tier** — a rule with `trust_tier` set denies agents below that tier (`TIER_MISMATCH`).
5. **Policy conditions** — the rule's `rego_source` is evaluated; if a decision block matches, its decision is final. For rules without a surface restriction, a non-matching body falls through to the next rule — but a surface-scoped rule whose surface matches is always decisive: a non-allow body resolves to `SURFACE_RESTRICTION` rather than falling through.

A rule whose body doesn't match contributes nothing — its `default decision` declaration never decides the event, so per-rule defaults are inert and the cascade continues. When no rule matches at all, the decision is `deny` (`DEFAULT_DENY`, fail-closed).

The SDK evaluates the same cascade in-process against its cached bundle (`GET /rules/bundle`, a SHA-256-versioned bundle with `ETag`/`304` caching, refreshed in the background roughly every 5 seconds). With a `VISIQ_API_KEY` the harness reaches SaaS (`https://api.visiqlabs.com` by default) and confirms this bundle automatically — monitor-until-confirmed is only the brief pre-first-bundle window. On that cold start with no bundle loaded, an agent **confirmed in `enforce`** stays fail-closed, while a **never-confirmed** agent runs `monitor`; the no-match `DEFAULT_DENY` floor still applies wherever enforcement is active.

***

## Managing Rules

Rules can be created and managed via:

* **Dashboard**: Navigate to **Harness → Rules** at [app.visiqlabs.com](https://app.visiqlabs.com). The unified editor covers both action and retrieval rules — create manually, describe in natural language, or edit the visual condition graph.
* **REST API**: Use the rules endpoints documented in the [API Reference](/rules/retrieval/api-reference) for programmatic rule management, CI/CD pipelines, and bulk imports.

### Rule Priority Guidelines

| Priority range | Suggested use                                             |
| -------------- | --------------------------------------------------------- |
| `100+`         | Override rules — emergency denials that should always win |
| `50–99`        | Standard rules for known patterns                         |
| `10–49`        | Broad trust-tier and category rules                       |
| `1–9`          | Catch-all rules                                           |
| `0`            | Default / lowest precedence                               |
