# Probity Vocabulary Registry, v1

This document is the versioned public registry of the closed enumerations used by the Probity
Predicate Standard. The enumerations are `layer`, `violation_type`, and `sink`, used by the
[catch-record](catch-record.md) component (and, transitively, by the verdicts that reference catch
records), plus the `auth_downgrade_cause` enumeration used by the
[a2a-mesh-scorecard](../v2/a2a-mesh-scorecard.md). These vocabularies are **not free text**. A conforming
producer emits only registered values; a conforming verifier rejects any value not in the registry
version it knows.

The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as in
[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).

## Why a registry, and why fail-closed

A security verdict's evidence is only meaningful if the vocabulary it uses is fixed and agreed. If a
verifier silently accepted an unknown `layer` or `violation_type`, a producer could emit a value the
verifier cannot interpret and the verifier would have no honest basis to trust or distrust it. So the
registry is **fail-closed at a version**:

- A verifier MUST reject any `layer`, `violation_type`, or `sink` value that is not present in the
  registry version it implements. There is no warn-and-continue mode, and none is planned.
- This is the one place where the standard's general "ignore unknown" rule does NOT apply. A verifier
  ignores an unknown *field* (forward-compatibility), but MUST reject an unknown *enum value* for a
  known field. An unknown field is additive data the verifier can safely skip; an unknown enum value
  is a value the verifier cannot interpret and therefore cannot trust.

## Registry versioning and governance

- **The registry version is tied to the predicate version.** The v1 registry below is the vocabulary
  for the v1 predicates. A registry revision moves in lockstep with the predicate's compatibility
  rules.

- **Additions are registry revisions, not silent changes.** Adding a value to any of these
  vocabularies is a published registry revision that ships a new registry list. It is never a silent
  change a deployed verifier is expected to tolerate; producers and verifiers adopt the new registry
  version together. A renamed or removed value is a breaking change and is a new major version.

- **Governance / contribution process.** Probity maintains this registry initially. Proposals to add
  or change a value go through a published contribution process: a `CONTRIBUTING` document describes
  the request format, and changes are submitted as pull requests and reviewed in the open before a new
  registry version is cut. The intent is to graduate registry maintenance to a neutral, multi-party
  process as adoption grows. Until then, all proposed changes are reviewed publicly.

## v1 registry contents

### `layer`

The substrate-qualified layer that fired a catch. A value names a public abstraction of an isolation
or policy layer; it does not expose any internal implementation detail. The v1 registered values are:

```
substrate.vfs_hook
policy.idna
policy.scrub_encodings
policy.authorize_context
policy.network_hook
policy.engine
netconfig.dns_pin
netconfig.dns_ad_quorum
sandbox.firecracker_isolation
sandbox.overlay_limits
vet.idna_recheck
vet.ide_settings_recheck
policy.egress_sinkhole
netconfig.nftables_moat
vmi.attribution
quarantine.coverage
```

`substrate.vfs_hook` names the guest filesystem hook engine: the substrate-level interception point
that resolves every stat, readdir, open, create, mkdir, chmod, remove, and remove_all request a
guest issues against a HookAction (an ActionFunc result or a static rule) before it reaches the real
filesystem. A catch at this layer is a filesystem-syscall-shaped decision, which is what separates
it from every other substrate/policy layer: `policy.network_hook` decides whether a network request
or response may proceed, not a filesystem operation; `policy.egress_sinkhole` captures a stream that
was already admitted past the network layer; `vet.idna_recheck` is a static-analysis finding emitted
by a linter pass, not a runtime block at all. Only `substrate.vfs_hook` covers the guest's
file-operation surface, so a reader can trust that a `substrate.vfs_hook` catch names a filesystem
decision and nothing else.

`policy.idna` names the runtime hostname-canonicalization gate `NormalizeHost` applies to every
policy pattern and every SNI/host header value the engine evaluates: IP-literal pre-reject, UTS #46
+ IDNA 2008 encoding, the CVE-2024-12224 pre-encode integrity check, RFC 1035 label-geometry
validation, and the post-IDNA round-trip check. Every `IDNA_*` violation type in this registry fires
under this one layer. It is distinct from the similarly-named `vet.idna_recheck`, which is a
build-time static-analysis finding about caller code that skips a DIFFERENT IDNA hazard (missing a
post-`ToASCII` `net.ParseIP` recheck) and never fires at runtime; and from `netconfig.dns_pin`,
which enforces a DNS-response pin match strictly after a hostname has already survived `policy.idna`
normalization. Collapsing any of the three would blur a build-time linter finding, a runtime
normalization rejection, and a post-resolution pin mismatch into one undifferentiated bucket.

`policy.scrub_encodings` names the secret-scrubbing pass `SecretScrubber.ReplaceAll` /
`ReplaceAllString` runs over an HTTP response body or SSE data line before it leaves the
interceptor; the layer fires only when the pass actually replaced bytes (output differed from
input), and it never records the raw secret material, only that a replacement happened. It is
distinct from `policy.network_hook`, which governs whether the surrounding request or response was
allowed to proceed at all — a scrub can fire on a request `policy.network_hook` already approved,
because approval and content-rewriting are separate decisions; and from `policy.egress_sinkhole`,
which captures and drops a stream wholesale rather than rewriting bytes within one that is still
being forwarded. A scrub event is evidence of an exfiltration ATTEMPT inside allowed traffic, not
evidence that the traffic itself was blocked.

`policy.authorize_context` names the dialer-family host-admission gate: the phase-ordered
`AuthorizeHostname` -> `FilterPinned` -> `VerifyDialedAddr` sequence that decides whether a resolved
destination may be dialed at all, and it carries every `HOST_NOT_ALLOWED`, `SSRF_ATTEMPT`,
`PIN_MISS`, and `PIN_IP_MISMATCH` catch record this vocabulary defines. It is distinct from
`policy.network_hook`, which evaluates an already-admitted connection's request/response shape
(method, path, host glob) against the L7 rule engine — a destination can pass
`policy.authorize_context` and still be blocked at `policy.network_hook` moments later; and from
`netconfig.dns_pin`, whose pin table binds a TLS certificate's SPKI to Quad9, a wholly different pin
than the resolved-IP `PinTable` this layer enforces. Reusing `netconfig.dns_pin` for a resolved-IP
mismatch would misrepresent a dialer-level admission refusal as a DNS-resolver-level certificate
rejection.

`policy.network_hook` names the L7 rule engine that matches an in-flight HTTP request or response
against a network-hook rule's host glob, HTTP method set, and path pattern, blocking a request or
response shape rather than a destination. It is distinct from `policy.authorize_context`, which
decides whether the underlying destination may be dialed at all, upstream of any request being
formed; a network-hook rule fires only after a connection has already cleared authorize-context
admission. It is also distinct from `policy.egress_sinkhole`, which — in a sinkhole deployment —
captures a request that has already cleared BOTH `policy.authorize_context` and
`policy.network_hook` and would otherwise have been forwarded; a `policy.network_hook` block means
the request never reached that point at all.

`policy.engine` names the umbrella policy `Engine` type in `pkg/policy` that owns and coordinates
the IDNA, scrub-encodings, network-hook, authorize-context, and egress-sinkhole sub-components as a
single construction/configuration unit. Unlike every sibling `policy.*` layer, it has NO production
emit site today: every actual policy-layer catch record is tagged with the specific sub-component
that fired (`policy.idna`, `policy.scrub_encodings`, `policy.network_hook`,
`policy.authorize_context`, or `policy.egress_sinkhole`), and no code path in the engine's own
construction or dispatch logic emits a catch record tagged `policy.engine` directly. It is
registered so a future engine-level failure — one attributable to none of the specific sub-layers —
has a name to emit under without a registry revision, but as of this version it names a reserved
value, not an observed catch.

`netconfig.dns_pin` names the Quad9 SPKI certificate-pin enforcement on the DoT (DNS-over-TLS)
upstream handshake: the `VerifyPeerCertificate`, `VerifyConnection`, and `VerifyECHRejection`
closures and the shared `enforce` decay-window logic that together produce every
`QUAD9_PIN_MISMATCH`, `QUAD9_PIN_EMPTY_CHAIN`, `QUAD9_PIN_ALL_MALFORMED`, and
`ECH_REJECTION_UNEXPECTED` catch record. It is distinct from the resolved-IP `PinTable` enforced
under `policy.authorize_context` (`PIN_MISS`, `PIN_IP_MISMATCH`) — that pin binds a hostname to the
IP addresses it is allowed to resolve to, this pin binds the Quad9 resolver's own TLS certificate to
a known SPKI hash, and the two mechanisms share no code path. It is also distinct from
`netconfig.dns_ad_quorum`, which corroborates a DNSSEC AD-bit answer across independent resolvers
rather than pinning any single resolver's certificate; the two layers defend against different
adversaries (a compromised Quad9 cert versus a compromised or BGP-hijacked primary resolver) and
neither can substitute for the other in signed evidence.

`netconfig.dns_ad_quorum` names the cross-resolver DNSSEC AD-bit corroboration gate: when the
primary resolver (Cloudflare DoH, CA-only validated) returns an `AD=1` answer, this layer fans the
same query out to independent peer resolvers (NextDNS, Mullvad) within a bounded window and requires
at least one to corroborate the primary's answer digest before the chain will surface it, producing
every `AD_QUORUM_MISMATCH`, `AD_QUORUM_TIMEOUT`, and `AD_QUORUM_ALL_PEERS_ERRORED` catch record. It
is distinct from `netconfig.dns_pin`, which pins a single resolver's own TLS certificate rather than
cross-checking that resolver's answer against independent peers — a compromised primary with a
perfectly valid certificate would pass `netconfig.dns_pin` but fail `netconfig.dns_ad_quorum` the
moment no peer corroborates its forged answer. The two layers are deliberately independent gates on
the same DNS path, not variants of one mechanism, so a verifier can tell which specific defense
caught an attack.

`sandbox.firecracker_isolation` names the Firecracker microVM isolation boundary itself — the
hypervisor-level containment the sandbox package constructs around a guest. Unlike its sibling
`sandbox.overlay_limits`, which has a live production emit site guarding the overlay-disk
layer/device budget on the same VM-construction path, `sandbox.firecracker_isolation` has NO
production emit site today: no code path in `pkg/sandbox` currently emits a catch record tagged with
this layer. It is registered as the intended home for a future isolation-boundary catch — a
hypervisor-level containment failure distinct from the disk-layout preconditions
`sandbox.overlay_limits` already guards — but as of this version it names a reserved value, not an
observed catch.

`sandbox.overlay_limits` names the overlay-disk layout precondition check
`validateOverlayDiskLayout` runs before a Firecracker microVM is launched: it enforces the
20-lower-layer budget and the 24-block-device Firecracker ceiling (root + lowers + upper + extra),
producing `OVERLAY_LAYER_LIMIT` and `OVERLAY_DISK_LIMIT`. It is distinct from
`sandbox.firecracker_isolation`, the sibling layer name reserved for the hypervisor isolation
boundary itself rather than the disk-layout precondition that must hold before that boundary is even
constructed — `sandbox.overlay_limits` fires and prevents the guest VM's `exec.Command` launch from
ever happening, so no isolation boundary exists yet to have failed. Keeping the two names distinct
preserves the fact that a disk-budget rejection is not an isolation failure.

`vet.idna_recheck` names the static analyzer that enforces a post-encode-recheck
contract on CALLER code: after any call to `idna.ToASCII` (package-level or via a `*idna.Profile`),
the analyzer requires a `net.ParseIP` or `netip.ParseAddr` recheck within the next ten statements of
the same block, or an explicit `//nolint:idnarecheck` suppression, and it emits
`VET_IDNA_RECHECK_MISSING` through `trace.EmitGlobal` when neither is present. It is distinct from
`policy.idna`, which is the runtime layer that performs IDNA normalization and already runs its OWN
internal IP-literal rechecks inside `NormalizeHost` — `vet.idna_recheck` exists precisely because
code OUTSIDE `NormalizeHost` can call `idna.ToASCII` directly and skip that protection, and a
build-time catch here is what proves such a caller was checked at all. A `vet.idna_recheck` catch is
a source-code finding at analysis time, never a runtime block of a live request.

`vet.ide_settings_recheck` names the `idesettings` static analyzer
(`ide_settings_recheck.go`), which models CVE-2025-68433 (the Zed editor
trust-boundary issue): it flags a value loaded from a per-project IDE settings file
(`.zed/settings.json`, `.vscode/settings.json`, `.cursor/settings.json`) that reaches a privileged
sink (`os/exec.Command`, `syscall.Exec`, `os.WriteFile`, `net/http.Get`, `net.Dial`, and siblings)
without an intervening `Validate`/`Recheck`/`Verify`/`Allowlist`/`Sanitize`/`AssertSafe`-named call.
The analyzer is real and runs — it reports every finding via `pass.Reportf()` — but unlike its
sibling `vet.idna_recheck`, it carries NO `trace.EmitGlobal` call anywhere in its implementation: it
is structurally disconnected from the runtime catch-record vocabulary this registry governs.
`VET_IDE_SETTINGS_DRIFT` is therefore a registered value with a real, running producer whose
findings never reach a signed catch record today — the analyzer and the registry both exist, but
nothing wires one to the other yet.

`policy.egress_sinkhole` names the userspace capture-and-drop interception point: in sinkhole mode,
the upstream connection for a guest's dial is a local capture pipe rather than the real destination,
and this layer records the signed `EGRESS_SINKHOLE_CAPTURE` catch — carrying the intended pre-DNAT
destination and the scrubbed payload — immediately before the captured stream is dropped, across all
three legs (raw TCP, HTTP cleartext, HTTPS). It is distinct from `netconfig.nftables_moat`, which is
a KERNEL-level unconditional forward-chain drop that requires no user-space component to have run at
all; `policy.egress_sinkhole` is precisely the opposite shape — a user-space capture that requires
the connection to have already cleared every upstream policy gate and reached the proxy. It is also
distinct from `policy.network_hook` and `policy.authorize_context`, both of which can block a
request before it ever reaches the sinkhole; a `policy.egress_sinkhole` catch means those upstream
gates already admitted the traffic and the sinkhole is the last line recording what would otherwise
have reached the real destination.

`netconfig.nftables_moat` names the kernel packet-filter ruleset that the substrate stages around a
sandbox's tap: the forward chain a guest's traffic must traverse, whose terminal verdict is deny. A
catch at this layer is a verdict the KERNEL reached, before and independently of any user-space
component. No existing layer can carry it, because every other layer names a user-space component
that need not have run at all for the packet to be dropped: `policy.authorize_context` asserts that a
host-admission decision was made, but a dropped packet may carry no host semantics to decide over (a
conntrack-invalid retransmit, a neighbour-discovery frame, a raw ethertype); `netconfig.dns_pin`
names the rebind-pin table specifically; `policy.egress_sinkhole` asserts that a stream was
terminated and captured. Reusing any of them would place a mechanism into signed evidence that did
not execute, which is precisely what a closed vocabulary exists to prevent.

The last two (`vmi.attribution`, `quarantine.coverage`) name the block-without-kill evidence-record
classes rather than a layer that fired a catch: an owning-task attribution record and a
coverage-closure snapshot. Both are catch-record-v1 records that ride the drained evidence stream, so
they belong in the closed layer vocabulary a verifier validates. See
[catch-record](catch-record.md) for their record shapes.

A verifier MUST reject any `layer` value not in this list. A renamed layer is a breaking change (new
major version).

### `violation_type`

The symbolic reason a catch fired. A given `violation_type` MAY legitimately appear under more than
one `layer`; the registry does not constrain which violation types co-occur with which layers. The v1
registered values are:

```
PATH_ESCAPE_DENIED
VFS_HOOK_BLOCKED
IDNA_IP_LITERAL_BLOCKED
IDNA_INTEGRITY_FAILED
IDNA_MALFORMED_LABEL
IDNA_TOO_LONG
IDNA_EMPTY_WILDCARD
ENCODED_PAYLOAD_SCRUBBED
HOST_NOT_ALLOWED
SSRF_ATTEMPT
NETWORK_HOOK_BLOCKED
PIN_MISS
PIN_IP_MISMATCH
PIN_EXPIRED
QUAD9_PIN_MISMATCH
QUAD9_PIN_EMPTY_CHAIN
QUAD9_PIN_ALL_MALFORMED
AD_QUORUM_MISMATCH
AD_QUORUM_TIMEOUT
AD_QUORUM_ALL_PEERS_ERRORED
ECH_REJECTION_UNEXPECTED
OVERLAY_LAYER_LIMIT
OVERLAY_DISK_LIMIT
VET_IDNA_RECHECK_MISSING
VET_IDE_SETTINGS_DRIFT
EGRESS_SINKHOLE_CAPTURE
MOAT_PACKET_DROPPED
EGRESS_OWNING_TASK_RESOLVED
QUARANTINE_COVERAGE
```

`PATH_ESCAPE_DENIED` names a guest path resolution that would climb above its mount root (a
`..`-relative traversal caught by `resolveName`'s `os.Root`-style confinement in
`pkg/vfs/realfs.go`, refused with `EPERM` rather than silently rewritten). The refusal itself is
real and enforced today — `TestEscapeRefusalReportsEPERM` pins it — but it has NO production emit
site: `resolveName` and its `confineError` wrapper return the refusal to the caller without ever
calling `trace.Emit` or `trace.EmitGlobal`, so a path-escape refusal produces no signed catch record
in the current codebase. This is distinct from its sibling `VFS_HOOK_BLOCKED`, which IS wired
end-to-end (`pkg/vfs/hooks.go`'s `emitHookBlock`) for every other guest filesystem refusal — stat,
readdir, open, create, mkdir, chmod, remove, remove_all. `PATH_ESCAPE_DENIED` is registered because
a path-escape refusal is a distinct violation class from a hook-engine block, but as of this version
it names a reserved value with no observed emission.

`VFS_HOOK_BLOCKED` reports that the guest filesystem hook engine resolved a request — stat, readdir,
open, create, mkdir, chmod, remove, or remove_all — to `HookActionBlock`, either through a
caller-supplied `ActionFunc` or a static block rule, and `emitHookBlock` (`pkg/vfs/hooks.go`)
recorded it before the operation reached the real filesystem. It is the general-purpose catch for
every VFS-hook refusal, in contrast to its narrower sibling `PATH_ESCAPE_DENIED`, which names one
specific refusal reason (a `..` traversal) and — unlike `VFS_HOOK_BLOCKED` — currently emits no
record at all. It is also distinct from `NETWORK_HOOK_BLOCKED`, the network-layer analog fired by
the L7 rule engine under `policy.network_hook`; the two share a naming pattern but block entirely
different primitives (a filesystem syscall versus a network request/response).

`IDNA_IP_LITERAL_BLOCKED` reports that `NormalizeHost` rejected a value because it is (or, after UTS
#46 NFKC-style digit folding, becomes) an IP-literal rather than a DNS name — fired at three points
in the pipeline: the pre-encode literal check, the wildcard-body re-check, and the post-IDNA-mapping
re-check that exists specifically because non-ASCII digit sequences (superscripts, fullwidth digits)
can fold into an ASCII IPv4 literal after `ToASCII` (the class `FuzzNormalizeHost` discovered,
regression seed `5602a60fa23a9bf8`). It is distinct from `IDNA_MALFORMED_LABEL` (a structurally
invalid DNS label under RFC 1035 section 2.3.4 — empty labels, over-length labels, all-dots input)
and `IDNA_TOO_LONG` (a well-formed name that exceeds the 253-octet FQDN cap): an IP literal is a
categorically different kind of value than a malformed or oversized DNS name, and the policy
engine's IP-CIDR surface and DNS-name surface must never be conflated, which is exactly what this
violation type exists to keep separate.

`IDNA_INTEGRITY_FAILED` reports one of three distinct integrity failures `NormalizeHost` can detect
over the same input: a pre-encode `xn--`-prefixed label whose decoded form is pure ASCII (the
CVE-2024-12224 pattern Go's `idna` library silently accepts but Python's does not, so Probity
pre-checks the INPUT itself before encoding); a bare `ToASCII` encoding error from the UTS #46 +
IDNA 2008 profile; or a failed encode-decode-re-encode round trip, which catches any non-idempotent
output a future Unicode table update might introduce even after `CheckHyphens` already rejects the
known `xn--example-` pattern. All three share this one violation type because each is an INTEGRITY
failure — the library or the input behaved inconsistently — as opposed to `IDNA_MALFORMED_LABEL`,
which reports a structurally well-formed-but-invalid DNS label (RFC 1035 section 2.3.4 geometry:
empty labels, 64-octet labels, all-dots input) where the encoding itself succeeded honestly.

`IDNA_MALFORMED_LABEL` reports that `validateACELabels` rejected the post-`ToASCII`
ASCII-compatible-encoding form for violating RFC 1035 section 2.3.4 DNS label geometry — empty
labels, an all-dots input, or a label exceeding the 63-octet limit — rules that
`golang.org/x/net/idna` does not itself enforce because it short-circuits pure-ASCII labels through
LDH-only validation. It is distinct from `IDNA_TOO_LONG`, which caps the fully-assembled FQDN
(including any wildcard prefix) at 253 octets and fires only after label geometry has already
passed; a name can fail `IDNA_MALFORMED_LABEL` on a single 64-octet label while being well under the
253-octet total, and conversely pass every per-label geometry check yet still be rejected as
`IDNA_TOO_LONG` once assembled. The two violation types cover RFC 1035's per-label and whole-name
length rules respectively, and neither subsumes the other.

`IDNA_TOO_LONG` reports that the fully-assembled result of `NormalizeHost` — the wildcard prefix
(`*.`, if present) plus the canonical ASCII-compatible-encoding form — exceeds the RFC 1035
253-octet FQDN limit, checked after label geometry (`IDNA_MALFORMED_LABEL`) has already passed and
before the final round-trip integrity check (`IDNA_INTEGRITY_FAILED`) runs. It is a whole-name
budget, not a per-label one: a name assembled entirely from valid, correctly-sized labels can still
fail `IDNA_TOO_LONG` once the label count pushes the total over budget, which is exactly why it is a
separate violation type from `IDNA_MALFORMED_LABEL` rather than a variant of it — the two check
different RFC 1035 constraints (label shape versus total length) at different stages of the same
pipeline.

`IDNA_EMPTY_WILDCARD` reports that a value entered `NormalizeHost` with wildcard matching enabled,
began with the `*.` prefix, and had nothing left after the prefix was stripped — a pattern like `*.`
with no host body to match against. It fires early in the pipeline, before any IDNA encoding is
attempted, which is what distinguishes it from every other `IDNA_*` violation type:
`IDNA_IP_LITERAL_BLOCKED`, `IDNA_MALFORMED_LABEL`, `IDNA_TOO_LONG`, and `IDNA_INTEGRITY_FAILED` all
evaluate the CONTENT of a hostname, whereas `IDNA_EMPTY_WILDCARD` fires on the absence of content —
a degenerate pattern that would otherwise either error obscurely deeper in the pipeline or, worse,
be silently treated as matching every possible host.

`ENCODED_PAYLOAD_SCRUBBED` reports that `SecretScrubber.ReplaceAll` or `ReplaceAllString` found and
replaced secret bytes inside an HTTP response body or SSE data line — the record fires only when the
scrubbed output actually differs from the input, and it deliberately never carries the raw secret
material or even its byte length in any signed or quantified field, only the fact that a replacement
happened. It is distinct from `EGRESS_SINKHOLE_CAPTURE`, which captures and drops an entire stream
rather than rewriting bytes within one that continues onward: an `ENCODED_PAYLOAD_SCRUBBED` record
describes traffic that WAS still forwarded, with the secret redacted in flight, whereas an
`EGRESS_SINKHOLE_CAPTURE` record describes traffic that never reached its intended destination at
all. The two violation types therefore report opposite dispositions of an in-flight secret —
sanitized-and-forwarded versus captured-and-dropped.

`HOST_NOT_ALLOWED` is the generic host-admission denial: a destination that failed the allowlist
check for any reason other than resolving to a private, link-local, or IMDS address. It fires from
every `policy.authorize_context` call site that refuses a host — an empty resolved-address set, an
`AllowedIPs`-filtered survivor set of zero, or a bare `IsHostAllowed` observation reported through
`EmitHostDenyObservation` — and it is also the value `hostDenyViolation` falls back to whenever
`BlockPrivateIPs` is off or the rejected address is not itself private. It is distinct from
`SSRF_ATTEMPT`, its own specialization: when `BlockPrivateIPs` is enabled and every surviving
resolved address was dropped specifically because it is private/link-local/IMDS, `hostDenyViolation`
reports `SSRF_ATTEMPT` instead, because a hostname resolving into the private network inside a
sealed gate is a server-side-request-forgery signal the generic bucket would otherwise hide from
downstream triage.

`SSRF_ATTEMPT` is the specialization of `HOST_NOT_ALLOWED` for an egress whose resolved destination
is a private, link-local, or loopback address (RFC 1918, `169.254.0.0/16` IMDS, and the rest of the
`privateRanges` set) denied under `BlockPrivateIPs`. `hostDenyViolation` and `FilterPinned` choose
this value instead of the generic `HOST_NOT_ALLOWED` precisely because, in a sealed gate, the system
under test has no legitimate private-network destination — so a hostname or address that lands in
the private range is a server-side-request-forgery or metadata-exfiltration probe, not merely an
un-allowlisted public host. Emitting it as a distinct value lets a downstream triage pipeline
auto-classify the attempt instead of collapsing every allowlist denial into one undifferentiated
`HOST_NOT_ALLOWED` bucket, which is the whole reason the two values are kept separate rather than
merged.

`NETWORK_HOOK_BLOCKED` reports that the L7 network-hook rule engine matched an in-flight request or
response against a rule's host glob, HTTP method set, or path pattern and blocked it, recorded by
`emitNetworkBlock` under the `policy.network_hook` layer with `SinkBlocked` fixed at `Dial`. It is
distinct from `HOST_NOT_ALLOWED`, which is decided earlier and at a coarser grain — whether the
destination may be dialed AT ALL, before any request has even been formed — whereas
`NETWORK_HOOK_BLOCKED` can fire against a destination that already passed host-admission, on the
finer-grained shape of the specific request (its method or path). It is also distinct from its
filesystem-layer namesake `VFS_HOOK_BLOCKED`: the two share a naming convention (`*_HOOK_BLOCKED`)
but block entirely different primitive classes, and neither vocabulary entry implies anything about
the other's layer.

`PIN_MISS` reports that `FilterPinned` found a populated `PinTable` but no entry at all for the
canonicalized host being authorized — every resolved address survived to this point, but the pin
table has nothing recorded for this hostname to check them against. It is distinct from
`PIN_IP_MISMATCH`, which reports the opposite shape: an entry EXISTS for the host, but the resolved
(or, at `VerifyDialedAddr`, the actually-dialed) address is not among the pinned addresses for it.
`PIN_MISS` is therefore a missing-registration failure and `PIN_IP_MISMATCH` is a wrong-address
failure over an existing registration — a verifier reading either can tell immediately whether the
pin table knew the host at all.

`PIN_IP_MISMATCH` reports that a `PinTable` entry exists for the host but the address under test —
either the intersection of resolved addresses at `FilterPinned` or the actually-dialed address
re-checked at `VerifyDialedAddr`'s `ControlContext` callback — is not among the pinned set for it.
It is distinct from `PIN_MISS` (no entry exists for the host at all, so there is nothing to mismatch
against) and from the registered-but-unemitted `PIN_EXPIRED`, which `PinTable.Verify`'s documented
contract reserves for an entry that exists but has aged out (`ErrPinExpired`) rather than one whose
addresses simply disagree with the ones under test — today every `ErrPinExpired` return reaches the
caller as a Go error without a corresponding catch record, so `PIN_IP_MISMATCH` remains the only
address-disagreement value actually observed in signed evidence.

`PIN_EXPIRED` names the pin-table outcome `PinTable.Verify`'s documented contract calls entry
expired: a populated pin exists for the host, but its recorded validity window has aged out
(`ErrPinExpired`, per `VerifyDialedAddr`'s doc comment at `pkg/policy/authorize_context.go`). The
error path exists in the type's documented contract, but NO call site in the current codebase maps
an `ErrPinExpired` return to `emitAuthorizeReject(trace.ViolationPinExpired, ...)` or any other
`trace.EmitGlobal` call — unlike its siblings `PIN_MISS` and `PIN_IP_MISMATCH`, which both have live
emit sites in `FilterPinned` and `VerifyDialedAddr`, `PIN_EXPIRED` has NO production emit site
today. It is registered because pin expiry is a distinct outcome from a missing entry or a
mismatched address, but as of this version it names a reserved value with no observed emission.

`QUAD9_PIN_MISMATCH` reports that `enforce()` computed the SHA-256 of at least one usable
certificate's `RawSubjectPublicKeyInfo` in the presented chain but none matched the embedded Quad9
SPKI pin set, checked only after the graceful-decay window (`GracefulDecayDays`, the default beyond
which a non-`HardFail` policy accepts CA-trust alone) has NOT yet elapsed. It is distinct from
`QUAD9_PIN_ALL_MALFORMED`, which fires earlier in `enforce()` when the chain contains zero usable
certificates at all (nothing to hash), and from `QUAD9_PIN_EMPTY_CHAIN`, which fires even earlier,
at the wrapper level, when the wire itself carried no certificates (`rawCerts` or `PeerCertificates`
empty) before `enforce()` is ever called. The three violation types mark three different points at
which a Quad9 DoT handshake can fail to produce a trusted, pin-matching certificate.

`QUAD9_PIN_EMPTY_CHAIN` reports that the TLS stack handed `VerifyPeerCertificate` or
`VerifyConnection` a wire-level empty certificate chain (`rawCerts` or `cs.PeerCertificates` has
zero entries) before `enforce()`'s pin logic ever runs. This is documented as wire-side semantics —
a stdlib glitch or genuine peer misbehavior at the transport level — which is what distinguishes it
from `QUAD9_PIN_ALL_MALFORMED`: that value fires INSIDE `enforce()` when the wire delivered cert
bytes but every one of them failed to parse into a usable certificate (parse-side semantics: an
attacker-shaped chain designed to look non-empty while carrying nothing `enforce()` can hash). The
distinction matters operationally — an empty chain and an all-malformed chain point an incident
responder toward different root causes even though both result in the same hard failure.

`QUAD9_PIN_ALL_MALFORMED` reports that `enforce()` received a chain of one or more certificates but
none had a usable `RawSubjectPublicKeyInfo` to hash — a chain shape that is malformed regardless of
the decay window's state, checked and rejected before the decay short-circuit runs so a wire that
carried bytes but produced zero usable certs cannot silently fall through to a decay-window CA-trust
accept at day 90+. It is distinct from `QUAD9_PIN_EMPTY_CHAIN`, which is the wire-level empty-chain
case caught by the wrapper closures before `enforce()` is even invoked (nothing arrived at all,
versus something arrived that could not be used), and from `QUAD9_PIN_MISMATCH`, which fires only
after usable certificates ARE found but none of them match the pin set. `QUAD9_PIN_ALL_MALFORMED`
sits between the two: certificates arrived, but none of them were usable enough to even attempt a
pin comparison.

`AD_QUORUM_MISMATCH` reports that every peer resolver responded (or errored) within the quorum
window, at least one peer response was NOT an error, and yet no peer's answer corroborated the
primary's `AD=1` digest — the peers were reachable and answered, but they disagreed with (or among)
themselves rather than confirming the primary. It is distinct from `AD_QUORUM_TIMEOUT`, which fires
when the bounded window expires before ANY peer responds at all — an infrastructure-silence signal
rather than a disagreement signal — and from `AD_QUORUM_ALL_PEERS_ERRORED`, which fires when every
single peer returned a transport error rather than an answer. The three violation types deliberately
separate 'peers disagreed' (suspicious — possible primary compromise) from 'peers were silent' and
'peers were unreachable' (both infrastructure problems), because an operator reading the metric
stream needs to tell those apart.

`AD_QUORUM_TIMEOUT` reports that the bounded cross-quorum window (`DefaultADQuorumWindow`, 200ms in
production) elapsed before any peer resolver produced a result at all, so the primary's `AD=1`
answer has zero corroboration to evaluate. It is distinct from `AD_QUORUM_ALL_PEERS_ERRORED`, which
requires every peer to have actively returned before the window closed, each with a transport error
— timeout means the peers never got the chance to answer either way, whereas all-peers-errored means
they all did answer, and every answer was a failure. It is also distinct from `AD_QUORUM_MISMATCH`,
where peers DID respond in time but simply failed to corroborate; `AD_QUORUM_TIMEOUT` specifically
names the case where the window itself, not the peers' answers, is what closed the query.

`AD_QUORUM_ALL_PEERS_ERRORED` reports that every peer resolver in the quorum set returned a
transport error within the window rather than any of them answering — the DoH transport to every
configured peer failed, not merely disagreeing with the primary. It is distinct from
`AD_QUORUM_TIMEOUT`, where the window expired without ANY peer result (error or success) arriving in
time, and from `AD_QUORUM_MISMATCH`, where at least one peer DID return a non-error result but none
corroborated the primary. The distinction between all-errored and timeout is deliberate: an operator
monitoring the metric stream needs to tell 'the peer DoH transport is broken' apart from 'nothing
came back in the window at all', and collapsing the two into one value would erase that diagnostic
signal.

`ECH_REJECTION_UNEXPECTED` reports that the stdlib TLS stack invoked `VerifyECHRejection` on the
Quad9 DoT connection at all — Probity does not adopt Encrypted Client Hello on this path (the
DNS-bootstrap chicken-and-egg problem plus a moot privacy gain for a service-controlled outbound
client), so under the default `ECHReject` policy this closure treats its own invocation as a hard
failure regardless of what the server's rejection actually contained. It is distinct from every
other Quad9 violation type (`QUAD9_PIN_MISMATCH`, `QUAD9_PIN_EMPTY_CHAIN`,
`QUAD9_PIN_ALL_MALFORMED`) because those all fire from cert-chain inspection inside
`VerifyPeerCertificate` or `VerifyConnection`, while an ECH rejection SUPPRESSES both of those
callbacks entirely — `ECH_REJECTION_UNEXPECTED` is the only Quad9 catch that can fire without either
of the other two ever having had a chance to run.

`OVERLAY_LAYER_LIMIT` reports that `validateOverlayDiskLayout` rejected a sandbox construction
because the requested lower-layer count exceeds `maxOverlayLowerLayers` (20), checked first and
independently of the total device count. It is distinct from `OVERLAY_DISK_LIMIT`, which is a
separate budget over the TOTAL block-device count (root + lowers + upper + extra, capped at
Firecracker's 24-device ceiling) checked only after the layer-count check has already passed — a
request can fail `OVERLAY_LAYER_LIMIT` alone (too many lowers, even before adding root/upper/extra),
or pass it and still fail `OVERLAY_DISK_LIMIT` once the fixed overhead pushes the total over budget.
Keeping the two separate lets a reader tell which specific budget (layer count versus total device
count) the request violated.

`OVERLAY_DISK_LIMIT` reports that `validateOverlayDiskLayout` rejected a sandbox construction
because the total block-device count — root plus lower layers plus the upper writable layer plus any
extra user-provided disks — exceeds Firecracker's 24-device ceiling, checked after the
`OVERLAY_LAYER_LIMIT` layer-count budget has already passed. It is the total-device budget as
opposed to `OVERLAY_LAYER_LIMIT`'s lower-layer-only budget: a request within the 20-layer limit can
still exceed the 24-device ceiling once the fixed root/upper overhead and any extra disks are added
in, which is exactly the case this violation type exists to catch separately.

`VET_IDNA_RECHECK_MISSING` reports that the `idnarecheck` static analyzer found a
call to `idna.ToASCII` whose result is not re-checked with `net.ParseIP` or `netip.ParseAddr` within
the next ten statements of the enclosing block, and is not suppressed with a `//nolint:idnarecheck`
comment — the UTS #46 NFKC digit-folding hazard that lets a visually-distinct Unicode digit sequence
encode into an ASCII string that parses as an IP literal. It is the only violation type this
vocabulary registers that is emitted from a `go/analysis` pass rather than from a runtime code path,
and it is distinct from the runtime `IDNA_IP_LITERAL_BLOCKED`: that value reports a live rejection
INSIDE `NormalizeHost`'s own internal recheck, whereas `VET_IDNA_RECHECK_MISSING` reports that some
OTHER caller's code, outside `NormalizeHost`, was never proven to have that same protection at all.

`VET_IDE_SETTINGS_DRIFT` names the diagnostic the `idesettings` analyzer reports
(`ide_settings_recheck.go`) when a value loaded from a per-project IDE settings
file reaches a privileged sink without an intervening revalidation call, modeling CVE-2025-68433.
The analyzer is real, runs today, and reports every finding through `pass.Reportf()` with a detailed
diagnostic message — but it never constructs a `trace.CatchRecord` or calls `trace.EmitGlobal`,
unlike its sibling `VET_IDNA_RECHECK_MISSING`, whose analyzer (`idnarecheck`) DOES emit through
`trace.EmitGlobal` alongside its `pass.Reportf` call. `VET_IDE_SETTINGS_DRIFT` is therefore
registered vocabulary with a genuine, currently-running producer, but that producer is structurally
disconnected from the signed catch-record path this registry governs — a finding reaches the `go
vet`-style output stream and nowhere else today.

`EGRESS_SINKHOLE_CAPTURE` reports that, in sinkhole deployment mode, a guest's dial to a
policy-gated destination was routed to a local capture-and-drop pipe instead of the real upstream,
and the intended pre-DNAT destination plus the scrubbed payload were recorded before the stream was
dropped — the one violation type shared across all three sinkhole legs (raw TCP, HTTP cleartext,
HTTPS), distinguished from each other only by the accompanying `sink` value (`tcp.Passthrough`,
`Dial`, `tls.Handshake`). It is distinct from `MOAT_PACKET_DROPPED`, the kernel-level forward-chain
drop that requires no captured payload and no user-space component to have run; and from
`ENCODED_PAYLOAD_SCRUBBED`, which redacts secret bytes inside traffic that is still forwarded rather
than capturing and dropping the whole stream. `EGRESS_SINKHOLE_CAPTURE` is also one of the four
violation types (alongside `HOST_NOT_ALLOWED`, `SSRF_ATTEMPT`, and `NETWORK_HOOK_BLOCKED`) the
quarantine block-without-kill arm binds on, because a captured egress carries the destination sink
tuple the arm needs to attribute a blocked flow to its owning guest task.

`MOAT_PACKET_DROPPED` reports that the kernel discarded a packet at the forward hook. It is distinct
from the three violation types a reader might otherwise reach for, each of which asserts something
stronger than a drop: `HOST_NOT_ALLOWED` asserts an allowlist verdict reached over a resolved name or
address; `NETWORK_HOOK_BLOCKED` asserts a decision by the L7 rule engine; `EGRESS_SINKHOLE_CAPTURE`
asserts a capture that, for a dropped packet, did not happen.

One violation type covers every kernel drop, and the producer's internal drop taxonomy is
deliberately NOT mirrored into this registry. A published cross-language vocabulary must not become
the bottleneck on ordinary substrate hardening: a producer that adds, splits or retires a drop rule
would otherwise need a registry revision, and its verifiers would need to adopt it, before the rule
could ship. Producers MAY carry a finer internal classification in their own unsigned telemetry, and
MAY ride the split on the OPTIONAL `sink` when one applies, exactly as
`EGRESS_SINKHOLE_CAPTURE` distinguishes its L4 and L7 legs through `sink` rather than through two
violation types.

`EGRESS_OWNING_TASK_RESOLVED` is `CatchRecord.ViolationType` for a `vmi.attribution` record: the
SECOND signed catch record in the block-without-kill evidence pair, joined to the triggering egress
catch (one of `HOST_NOT_ALLOWED`, `SSRF_ATTEMPT`, `NETWORK_HOOK_BLOCKED`, or
`EGRESS_SINKHOLE_CAPTURE`) by `catch_seq`, attributing the blocked-but-non-terminal egress to the
guest task and `ModelBOM` that owned it via a paused-guest socket walk. It is distinct from every
violation type above it in this list in that it does not itself describe a policy refusal — the
refusal already happened and was already recorded under its own layer — this value describes a
SEPARATE, subsequent act of attribution over that refusal. It is also distinct from
`QUARANTINE_COVERAGE`, its sibling block-without-kill record type: `EGRESS_OWNING_TASK_RESOLVED` is
a per-catch attribution record, one per blocked egress, while `QUARANTINE_COVERAGE` is a run-level
accounting snapshot covering every catch the arm has seen.

`QUARANTINE_COVERAGE` is `CatchRecord.ViolationType` for a `quarantine.coverage` record: a run-level
snapshot of the block-without-kill arm's own bookkeeping — how many catches it saw versus attributed
versus suppressed, under a strict equality closure (`catches_seen == attributed + dedup_suppressed +
budget_suppressed + breaker_suppressed + pending_overflowed + pending_unserviced + not_bindable +
internal_faulted + in_flight + queued`). It is explicitly a vocabulary marker, not an offense: the
record accounts the arm's own work rather than reporting a caught egress, which is what separates it
from every other violation type in this registry — `HOST_NOT_ALLOWED` through
`EGRESS_OWNING_TASK_RESOLVED` all describe something the substrate caught the guest doing, while
`QUARANTINE_COVERAGE` describes whether the arm's own accounting is complete and trustworthy up to
its last signed flush.

A verifier MUST reject any `violation_type` value not in this list.

### `sink`

The syscall, dial, or exec primitive that was prevented. This vocabulary is OPTIONAL in a catch
record (a catch record MAY omit `sink`), but when present the value MUST be a registered one. The v1
registered values are:

```
openat
Dial
exec.Command
vfs.Hook
tls.Handshake
dns.Exchange
tcp.Passthrough
netfilter.Forward
```

`openat` names the sink `emitHookBlock` (`pkg/vfs/hooks.go`) always records for a
`substrate.vfs_hook` block, regardless of which specific hook operation fired — stat, readdir, open,
create, mkdir, chmod, remove, and remove_all all record `openat`, not a syscall-accurate name per
operation. It stands for the guest filesystem hook's file-primitive family as a whole rather than
the literal `openat(2)` syscall for every one of those ops. It is distinct from `vfs.Hook`, a
differently-worded sink that — despite the name — is NOT emitted by the VFS hook layer at all; its
sole production emission is under `policy.scrub_encodings`. A reader must not assume `vfs.Hook` and
`openat` are interchangeable names for the same primitive family; they are emitted by entirely
different layers today.

`Dial` names the network-dial primitive prevented from reaching its destination, and it is the fixed
sink for every `policy.authorize_context` denial (`emitAuthorizeReject` hardcodes it, since every
call site in that file is a dialer-family phase method), for every `NETWORK_HOOK_BLOCKED` record,
and for the HTTP-cleartext leg of an `EGRESS_SINKHOLE_CAPTURE` (the sinkhole intercepts the `Write`
that would otherwise reach the real destination). It is distinct from `tcp.Passthrough`, the
sinkhole's raw-TCP leg sink, and from `tls.Handshake`, the sinkhole's HTTPS leg sink — the three
sinks let a forensic reader tell which of the three sinkhole legs (cleartext HTTP, raw TCP, or
TLS-terminated HTTPS) captured a given egress, even though all three can carry the same
`EGRESS_SINKHOLE_CAPTURE` violation type.

`exec.Command` names the process-launch primitive an `OVERLAY_LAYER_LIMIT` or `OVERLAY_DISK_LIMIT`
rejection prevents: `validateOverlayDiskLayout` runs before the sandbox's Firecracker microVM
process is ever launched, so a disk-layout rejection means the `exec.Command` invocation that would
start the VMM binary never happens at all. It is distinct from `Dial`, `vfs.Hook`, and
`tls.Handshake`, which all name primitives prevented at RUNTIME against an already-running guest;
`exec.Command` is the only sink in this registry that names a primitive prevented before a guest
sandbox exists to run anything, at the moment of sandbox construction itself.

`vfs.Hook` names the hook-mediated content-interception point at which `SecretScrubber.ReplaceAll` /
`ReplaceAllString` rewrites bytes; its sole production emission today is under
`ENCODED_PAYLOAD_SCRUBBED` at the `policy.scrub_encodings` layer, invoked from the HTTP response
body and SSE data-line scrub calls in `pkg/policy/network_hooks.go`. This is the one place in the
registry where a sink's name and its actual emitting layer diverge from what the name suggests:
`substrate.vfs_hook` — the layer whose name most resembles `vfs.Hook` — records `openat` as its
sink, never `vfs.Hook`. A reader must resolve `vfs.Hook` by its actual emission site (a
content-scrub interception point) rather than by name association with the VFS hook layer, which is
a distinct, unrelated mechanism.

`tls.Handshake` names the TLS handshake primitive, and it is emitted from two unrelated call sites
that happen to share the same primitive name: the Quad9 SPKI pin verifier (`emitPinReject` in
`pkg/netconfig/dns_pin_quad9.go`), where it marks the DoT upstream handshake a pin rejection
prevented from completing; and the HTTPS leg of an egress-sinkhole capture (`emitSinkholeCapture` in
`pkg/netconfig/http.go`), where it marks the upstream TLS leg the sinkhole stood in for. The two
emission sites carry different layers and different violation types (`netconfig.dns_pin` /
`QUAD9_PIN_*` versus `policy.egress_sinkhole` / `EGRESS_SINKHOLE_CAPTURE`), so a verifier must read
`layer` and `violation_type` together with `sink` to tell a Quad9 pin failure from a captured HTTPS
egress — `tls.Handshake` alone under-determines which of the two occurred.

`dns.Exchange` names the DNS query/response exchange primitive, and it is emitted from three
distinct call sites: every `IDNA_*` rejection (`emitIDNAReject` fixes this sink for the whole
`policy.idna` layer, since a rejected hostname was never going to reach a DNS exchange with that
value), the DNS-proxy allowlist chokepoint's `HOST_NOT_ALLOWED` observation
(`policy.EmitHostDenyObservation` in `pkg/netconfig/dns.go`, for a name or CNAME target the DNS-side
allow-predicate refused), and every `AD_QUORUM_*` rejection (`emitQuorumReject` in
`pkg/netconfig/dns_ad_quorum.go`). All three call sites genuinely prevented a DNS exchange from
completing on the attacker's terms, unlike `vfs.Hook`, whose name-to-emission-site relationship is a
documented exception; `dns.Exchange` is a straightforward, literal match between its name and what
it records across every site that emits it.

`tcp.Passthrough` marks an egress-sinkhole capture on the raw, non-HTTP TCP
passthrough path: a guest dialled a policy-gated TCP port that is not HTTP/HTTPS,
so there is no request line to parse — the sinkhole captured the raw stream and
dropped it. It is distinct from `Dial` (the HTTP cleartext sinkhole leg) so a
forensic reader can tell an L4 raw exfil (e.g. a database `DROP TABLE` to port
5432) from an L7 HTTP capture.

`netfilter.Forward` names the packet-forwarding primitive itself: the prevented operation is
forwarding the packet onward, and every rule that can reach a drop verdict sits on the forward chain.
It is registered so that a kernel drop is not the one egress record with no `sink` at all — `sink` is
a field forensic consumers pivot on, and a whole record class that never carries one reads as missing
data rather than as an answered question.

A verifier MUST reject any present `sink` value not in this list.

### `task_resolution`

How a `vmi.attribution` catch record's egress-owning task was resolved from the paused-guest socket
walk. Present in the record's `task_resolution` field and summed into the `quarantine.coverage`
record's `task_resolution` histogram. The v1 registered values are:

```
4-tuple
dst-fallback
dst-only
```

`4-tuple` is the strongest binding (the owning task was matched on the full source/destination
address 4-tuple); `dst-fallback` matched on the destination alone after the source could not be
recovered; `dst-only` is a destination-only attribution with no source corroboration. A verifier
MUST reject any `task_resolution` value not in this list.

### `verity_device_pairing`

How a `quarantine.coverage` record's dm-verity lower device was paired to its model image during the
host-VMI walk. Present in the record's `verity_device_pairing` field. The v1 registered values are:

```
verity-exact-root-digest
verity-heuristic-size
```

`verity-exact-root-digest` is the strong pairing (the enforced dm-verity root hash recovered from
guest memory matched the model image's `veritysetup` digest exactly); `verity-heuristic-size` is the
weaker size-based heuristic used when the exact root digest could not be recovered. A verifier MUST
reject any present `verity_device_pairing` value not in this list.

### `auth_downgrade_cause`

The composition-safety violation reason an offline mesh evaluator records in an
[a2a-mesh-scorecard](../v2/a2a-mesh-scorecard.md)'s `verdict.causes`. These are the
**trace-to-auth-downgrade invariant** cause set: the specific ways authority can fail to attenuate as it
flows hop-to-hop through an A2A mesh. The v1 registered values are:

```
IDENTITY_MISMATCH
TEMPORAL
DOWNGRADE
SCOPE_BROADENING
TOKENLESS_BREAK
```

A verifier MUST reject any `auth_downgrade_cause` value not in this list. The per-cause meaning is documented in
[a2a-mesh-scorecard.md](../v2/a2a-mesh-scorecard.md). A renamed or removed cause is a breaking change (new
major version); an added cause is a registry revision.

## How consumers use the registry

- A [catch-record](catch-record.md) MUST carry `layer` and `violation_type` values drawn from this
  registry, and MAY carry a `sink` value drawn from this registry.
- A producer MUST map any internal or per-runtime naming onto these exact registered strings before
  emitting a record. Equality comparisons in a verdict (for example, matching an actual layer to an
  expected layer) are decided over these normalized strings.
- A verifier validates every enum value in every catch record it processes against the registry
  version it implements, and rejects the record (and any verdict that depends on it) on any
  unregistered value.
- An [a2a-mesh-scorecard](../v2/a2a-mesh-scorecard.md) MUST carry `verdict.causes` values drawn from the
  `auth_downgrade_cause` list, and a verifier MUST reject a scorecard carrying an unregistered cause.

## Related standards

- **[SARIF](https://sarifweb.azurewebsites.net/)** - SARIF defines its own taxonomies and rule
  identifiers. This registry is independent of SARIF's taxonomy; per-finding `check_id` values in
  layer-carrying verdicts are the SARIF crosswalk point, not these layer / violation / sink
  vocabularies. (Layer findings are out of the
  [adversarial-execution-evidence](adversarial-execution-evidence.md) core; its `layer` /
  `violationType` values ARE gated by this registry.)
- **[IANA registries](https://www.iana.org/protocols)** - the governance model (a maintained,
  versioned, publicly-reviewed list of allowed values) follows the spirit of an IANA-style registry,
  scaled to this standard.

## Versioning

This is the v1 registry. Any addition is a registry revision published as a new list; any rename or
removal is a breaking change requiring a new major version. A v1 verifier evaluates values strictly
against the v1 lists above and fails closed on anything else.
