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

# Policies

> YAML in the dashboard, versioned, diffable, dry-runnable.

A policy is a YAML document with rules that match a tool call and assign an effect (`allow` or `deny`). Policies are authored in the dashboard or pushed via the API, versioned on each save, and published as a signed bundle that the SDK pulls every 30 seconds.

## Anatomy

```yaml theme={null}
apiVersion: agent-governance.io/v1
kind: Policy
metadata:
  name: deny-destructive-fs
  description: Block any agent from removing files.
  labels:
    owner: platform
spec:
  defaultEffect: allow
  rules:
    - id: no-delete
      description: Deny delete_file calls
      effect: deny
      conditions:
        - field: tool_name
          operator: eq
          value: delete_file
    - id: no-rm-shell
      effect: deny
      conditions:
        - field: tool_name
          operator: eq
          value: shell.exec
        - field: input.command
          operator: contains
          value: "rm -rf"
```

### Fields

| Field                  | Required | Notes                                           |
| ---------------------- | -------- | ----------------------------------------------- |
| `apiVersion`           | Yes      | Always `agent-governance.io/v1`.                |
| `kind`                 | Yes      | Always `Policy`.                                |
| `metadata.name`        | Yes      | Human label. Doesn't have to be unique.         |
| `metadata.description` | No       | Surfaces in the dashboard.                      |
| `metadata.labels`      | No       | `Record<string, string>` for ownership tagging. |
| `spec.defaultEffect`   | Yes      | `allow` or `deny`. Applied if no rule matches.  |
| `spec.rules`           | Yes (≥1) | Evaluated top-to-bottom; first match wins.      |

### Rule conditions

A rule matches when **every** condition in its list matches (logical AND). Condition shape:

```yaml theme={null}
- field: <dotted.path.into.request>
  operator: <eq | neq | in | nin | contains | starts_with | ends_with | gt | gte | lt | lte | regex>
  value: <string | number | bool | string[] | number[]>
```

#### Common fields

| Field          | Type                | Source                                                               |
| -------------- | ------------------- | -------------------------------------------------------------------- |
| `tool_name`    | string              | First positional arg to `evaluate()`.                                |
| `agent_id`     | string              | Bound to the JWT's `sub` claim.                                      |
| `input.<key>`  | any                 | From `metadata.input` passed to `evaluate()`. Supports nested paths. |
| `args.<index>` | any                 | Positional args (LangChain adapter).                                 |
| `kwargs.<key>` | any                 | Keyword args (LangChain adapter).                                    |
| `dlp_detected` | bool                | Set by the inline DLP scanner if enabled.                            |
| `dlp_severity` | `low\|medium\|high` | Highest severity detected.                                           |
| `dlp_types`    | string\[]           | Types that fired (`EMAIL`, `SSN`, `CREDIT_CARD`, …).                 |

## Versioning

Every save creates a new version. Versions can be:

* **draft** — saved but not published. Visible in the editor, not in the bundle.
* **active** — currently published. There is at most one active version per policy.
* **archived** — was active, has been replaced. Kept for audit.

When you click **Publish** in the dashboard, the new version becomes active and the bundle version increments. SDKs pulling the bundle see the new policies on their next poll (\~30s).

## Dry-run

Before publishing, click **Dry-run** in the editor. Rubric replays the last 1000 audit events through the candidate policy and shows you:

* How many events would have been allowed vs denied.
* Which agents are most affected.
* A diff against the currently-published version.

Always dry-run a deny rule before shipping it. The audit replay catches over-broad matchers cheaply.

## Scope: which agents does this policy enforce against?

Scope is **prescriptive and operator-controlled**. Every policy carries an explicit list of agents it enforces against, stored separately from the YAML. The list is editable on the policy detail page and required at publish time.

| State       | Meaning                                   | SDK behavior                                                                      |
| ----------- | ----------------------------------------- | --------------------------------------------------------------------------------- |
| Empty scope | Policy applies to **no agents**.          | Bundle build excludes the policy from every per-agent slice. The policy is inert. |
| ≥ 1 agent   | Policy applies only to the listed agents. | Each scoped agent's bundle includes the policy. Unscoped agents don't see it.     |

This is **explicit opt-in by design**. A newly-enrolled agent is unpoliced until an operator scopes a policy onto it — the cost of accidentally leaving a policy off one agent was judged lower than the cost of an over-broad policy quietly enforcing somewhere unintended.

### How it ships

* Scope is **not** part of the policy YAML — it's a separate per-policy agent list. Forking a template never carries scope across orgs; the fork prompts the operator for fresh agent selection.
* Each agent gets its own bundle slice. Editing scope on a published policy fans out fresh bundles to the affected agents only — no version bump, no SDK restart.
* Every scope add/remove is recorded in the audit ledger right alongside tool-call decisions, on the same tamper-evident chain. Config tampering is as detectable as decision tampering.

### Required at publish

The dashboard disables the publish button until you pick at least one agent. Drafts are allowed to have empty scope — they're not in the bundle anyway.

### Filtering rule logic by agent metadata

Conditions like `agent_environment eq prod` still work for narrowing **which calls inside a scoped agent** match a rule. They're orthogonal to scope: scope picks which agents see the policy; conditions pick which calls within those agents trigger which effect.

## Default-allow vs default-deny

The default `spec.defaultEffect: allow` is the right starting point — it lets you ship an integration with zero policies and add denies incrementally.

For high-trust environments, flip to `defaultEffect: deny` and add explicit allow rules. The SDK supports both; nothing in the framework prefers one model.
