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

# Policy file reference

> Every field in a Zespan policy file, its type, whether it is required, and the exact subset of YAML that policy files support.

A policy file describes one policy. Files live in `policies/` in your
repository, at any depth, with a `.yaml` or `.yml` extension.

## A complete example

```yaml policies/hipaa-phi-egress.yaml theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
apiVersion: zespan.dev/v1
kind: Policy
metadata:
  id: hipaa-phi-egress
  name: HIPAA PHI egress control
  description: Blocks PHI from leaving the intake agents.
  labels:
    framework: hipaa
    owner: security
spec:
  enforcement: deny
  phase: post
  failOpen: false
  appliesTo:
    environments: [prod, staging]
    agents: ["intake-*", "triage-*"]
  rules:
    - type: pii
      action: block
      detect: [ssn, mrn]
    - type: regex
      action: redact
      patterns: ["\\d{3}-\\d{2}-\\d{4}"]
  exemptions:
    - reason: "Clinical reviewer operating under BAA-2201"
      agents: ["clinical-reviewer"]
      expiresAt: "2027-01-01"
```

## Top level

| Field        | Type   | Required | Notes                           |
| ------------ | ------ | -------- | ------------------------------- |
| `apiVersion` | string | Yes      | Must be exactly `zespan.dev/v1` |
| `kind`       | string | Yes      | Must be exactly `Policy`        |
| `metadata`   | object | Yes      | See below                       |
| `spec`       | object | Yes      | See below                       |

## `metadata`

| Field         | Type                   | Required | Notes                                                                                                                                                                                           |
| ------------- | ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`          | string                 | Yes      | Stable identifier. Lowercase letters, digits and hyphens; must start and end with a letter or digit; max 64 characters. **Changing it is treated as deleting one policy and creating another.** |
| `name`        | string                 | Yes      | Human-readable name, shown in the dashboard                                                                                                                                                     |
| `description` | string                 | No       | Free text                                                                                                                                                                                       |
| `labels`      | map of string → string | No       | Your own metadata; not interpreted by Zespan                                                                                                                                                    |

<Warning>
  `metadata.id` is the identity of the policy. Renaming it in place means the
  old policy is removed and a new one created — which `apply` will refuse
  without `--allow-remove`.
</Warning>

Each `id` must be unique across your whole file set. Two files claiming the same
id are rejected, because which one owned the resulting rows would otherwise
depend on directory ordering.

## `spec`

| Field         | Type                         | Required | Notes                                                                                                                             |
| ------------- | ---------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `enforcement` | `dryrun` \| `warn` \| `deny` | Yes      | The posture for every rule in this policy — see [the enforcement ladder](/policies/as-code#the-enforcement-ladder)                |
| `phase`       | `pre` \| `post` \| `both`    | Yes      | Whether rules run before the model call, after it, or both                                                                        |
| `failOpen`    | boolean                      | Yes      | Whether traffic is allowed through when the policy cannot be evaluated. Required deliberately — there is no safe default to guess |
| `appliesTo`   | object                       | No       | Scope. Absent means the whole project                                                                                             |
| `rules`       | list                         | Yes      | At least one. A policy with no rules enforces nothing                                                                             |
| `exemptions`  | list                         | No       | See below                                                                                                                         |

### `spec.appliesTo`

| Field          | Type           | Required | Notes                                                                                                                                       |
| -------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `environments` | list of string | No       | Environment slugs. A **guard on `apply`**, not a router — see [scoping to environments](/policies/as-code#scoping-a-policy-to-environments) |
| `agents`       | list of string | No       | Agent names; `*` wildcards are supported                                                                                                    |
| `models`       | list of string | No       | Model identifiers; `*` globs are supported. Checked on every request                                                                        |
| `schedule`     | object         | No       | `days`, `hours` (`"09:00-18:00"`), `timezone`                                                                                               |

`environments` and `models` are enforced at different moments, and the
difference matters:

* **`environments` is checked once, when you apply.** `apply` refuses to write
  a policy into an environment the policy does not list. It never fans one
  apply out into several.
* **`models` is checked on every request.** A policy scoped to `["gpt-4*"]`
  does not fire for a request reporting `claude-opus-4`, and a request that
  reports **no** model at all is out of scope rather than blocked — the same
  convention `model_governance` uses.

### `spec.rules[]`

| Field             | Type                                   | Required | Notes                                                                                                                                               |
| ----------------- | -------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`            | string                                 | Yes      | One of the rule types below                                                                                                                         |
| `action`          | `block` \| `warn` \| `redact` \| `log` | Yes      | What to do on a match. Softened by `enforcement`                                                                                                    |
| `slug`            | string                                 | No       | Pins this rule to a specific guardrail row. Normally omitted — `zespan policy pull` writes it so a generated file maps onto rows that already exist |
| *(anything else)* | any                                    | No       | Passed through to the rule engine unchanged                                                                                                         |

Rule order is significant and is preserved exactly as written.

**Valid `type` values:** `pii`, `toxicity`, `topic_boundary`, `format`,
`cost_ceiling`, `custom_llm`, `regex`, `agent_rate_limit`, `tool_misuse`,
`loop_detection`, `agent_misuse`, `scope_enforcement`, `delegation_control`,
`model_governance`, `secret_egress`, `schema_contract`.

### `model_governance`

| Field             | Type         | Notes                                                                                                                                                                         |
| ----------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allowModels`     | list of glob | If non-empty, only matching models are permitted                                                                                                                              |
| `denyModels`      | list of glob | Matching models are refused                                                                                                                                                   |
| `requireProvider` | string       | `openai`, `anthropic`, `google`, … Inferred from the model when the caller does not report one; a model whose provider cannot be inferred **fails** rather than being guessed |
| `allowRegions`    | list of glob | If non-empty and the caller reports no region, the rule **fails** rather than passing unchecked                                                                               |
| `denyRegions`     | list of glob | Matching regions are refused                                                                                                                                                  |

Globs use `*` only; everything else is literal. A caller that reports no model at
all is out of scope for this rule rather than blocked by it.

### `secret_egress`

| Field                | Type           | Notes                                                                      |
| -------------------- | -------------- | -------------------------------------------------------------------------- |
| `disablePatterns`    | list of string | Turn off a curated pattern by its label, if it false-positives for you     |
| `additionalPatterns` | list of regex  | Your own patterns, checked with the same ReDoS-safe matcher used elsewhere |

The curated set covers AWS access keys, GitHub tokens (including fine-grained
PATs), Slack, Stripe, OpenAI, Anthropic, Google, SendGrid, Twilio, npm tokens,
PEM private keys, JWTs and bearer credentials. With `action: redact` the matched
material is replaced with `[REDACTED]` rather than the response being blocked.

### `schema_contract`

| Field    | Type        | Notes                                                                                       |
| -------- | ----------- | ------------------------------------------------------------------------------------------- |
| `schema` | JSON Schema | Validated with full JSON Schema, so types, `required`, `enum`, ranges and nesting all apply |

Output that is not valid JSON is reported as such rather than as a schema
mismatch. A rule with no `schema` passes rather than failing every response —
the misconfiguration is caught by `zespan policy validate`.

### `spec.exemptions[]`

| Field       | Type                  | Required | Notes                                                                                                                |
| ----------- | --------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `reason`    | string                | Yes      | Why this exemption exists. Recorded in the audit log on apply, and reported as the reason the control was skipped    |
| `agents`    | list of string        | No       | Which agents it covers, matched case-insensitively. **Omit it and every agent in the policy's scope is exempt**      |
| `expiresAt` | string (`YYYY-MM-DD`) | **Yes**  | The last day it applies. Enforced at evaluation time, so an exemption that expires stops applying without a redeploy |

An exemption suppresses the policy's rules for the scope it names. A suppressed
rule produces **no** guardrail event at all — not one recorded as allowed,
which would read on the dashboard as "the control ran and found nothing".

<Warning>
  `expiresAt` is required, and a policy whose exemption has already expired
  **fails validation** — naming the exemption's reason so you know which one to
  renew. An exemption with no expiry is a permanent hole in a control that
  nobody ever revisits.
</Warning>

<Note>
  The expiry is checked **on every request**, not when you apply. The day named
  in `expiresAt` is itself still exempt (compared as a UTC date, so the result
  does not depend on your server's timezone); from the day after, the control
  enforces again on its own — you do not have to re-run `apply` for it to take
  effect. Renewing an exemption, or changing `appliesTo.models`, *is* a real
  change to the policy, so `zespan policy plan` reports it as an update.
</Note>

## The supported YAML subset

Policy files are parsed as **YAML 1.2 core**. Almost everything you would
normally write works, including block scalars, flow collections, comments, and
any consistent indentation.

A small set of features is rejected — each with the line number and a message
saying what to write instead. These are all things that make a security policy
harder to read or review, not parser limitations:

| Rejected                                   | Why                                                                   |
| ------------------------------------------ | --------------------------------------------------------------------- |
| Anchors (`&name`) and aliases (`*name`)    | A policy must be readable top to bottom, without resolving references |
| Merge keys (`<<`)                          | Same reason — write the merged fields out                             |
| Explicit tags (`!!str`, `!custom`)         | The value's own syntax determines its type                            |
| Multi-document files (`---` separators)    | One policy per file                                                   |
| Duplicate keys in one mapping              | Silently taking the last one hides a real mistake                     |
| Collections or non-strings as mapping keys | Policy field names are always strings                                 |
| A top-level document that is not a mapping | A policy file is a mapping of fields                                  |

<Note>
  Because parsing follows YAML 1.2 core rather than 1.1, `on`, `off`, `yes` and
  `no` stay **strings** rather than becoming booleans. This is the well-known
  "Norway problem" (`no` → `false`), and avoiding it matters here because `on`
  is a plausible value in a policy file.
</Note>

## Validation

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
zespan policy validate
```

Runs offline. Every problem in every file is reported at once:

```text theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
policies/bad.yaml:1  spec.enforcement is "maybe"; must be one of "dryrun", "warn", "deny".
                     Start at "dryrun" to see what a policy would have caught without affecting traffic.
policies/bad.yaml:1  spec.rules[0].type is "telepathy", which is not a known rule type.
                     Valid types are: "pii", "toxicity", ...

1 of 2 file(s) failed validation.
```

## Next steps

<CardGroup cols={2}>
  <Card title="Policy as code" icon="file-code" href="/policies/as-code">
    The authoring loop and the ownership model.
  </Card>

  <Card title="zespan policy" icon="terminal" href="/cli/policy">
    Every verb and flag.
  </Card>
</CardGroup>
