Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

policy.toml reference

A repository opts into gating by placing exactly one file at .vidi/policy.toml. No policy file means no gating at all: the verdict is NEUTRAL and the gate exits 0, except for the two integrity failures that are not policy questions (see Unit states).

Source of truth: crates/vidi-core/src/policy/.

Discovery

The path is fixed: .vidi/policy.toml, relative to the repository root. Discovery does not consult home directories, environment variables, ancestor folders, or legacy filenames. There is exactly one place a policy can live, so no repository can be gated by a file you cannot see in its diff.

A .vidi/policy.toml that resolves outside the repository root (a symlink escape) is refused rather than followed.

The file is a trust boundary

Two consequences worth internalising before the key tables:

Unknown keys are a hard error. Every table is deny_unknown_fields. A typo in a security key fails the load; it is never parsed and ignored.

A policy that gates nothing will not load. A file with no [[cover]], no [[scope]], and default = "advisory" is rejected as vacuous. Either declare an obligation or delete the file; “present but inert” is not a state you can reach by accident.

Load failures are refusals, not warnings, and each points at the offending bytes.

Top-level keys

KeyRequiredTypeMeaning
schemayes"MAJOR.MINOR"Schema version. This engine enforces "1.0".
defaultyes"advisory" or accept-row listPosture for paths matched by no gating [[scope]].
[axes]notableThe two ranked reviewer ladders.
[reviewers]notableThe reviewer directory: identity → role.
[[cover]]noarrayProactive coverage obligations.
[[scope]]noarrayReactive path-scoped requirements.
[severity]notableAdvisory thresholds. Never gates.
[[exemptions]]noarrayAudited exclusions from the unreviewed set.

schema

Exactly two dotted integers. No patch component, no zero-fill, no partial parse: "1", "1.2.3" and "1.x" are all rejected.

Version skew fails closed in three of four directions:

FileEngineResult
1.01.0loads
1.01.4loads; the file uses a subset of what the engine knows
1.71.0refused; the file may carry a gating field this engine cannot enforce
0.9 / 2.01.0refused; a major mismatch in either direction

A newer minor is refused rather than best-effort honoured, because the failure mode of guessing is a build that passes while a rule goes unenforced.

default

Either the string "advisory" or a list of accept rows.

default = "advisory"                          # unmatched paths are report-only
default = [ { human = "reviewer" } ]          # unmatched paths must meet this

default = "advisory" is the recommended starting posture: paths matched by no gating [[scope]] are reported but never fail the build. A strict default gates the entire repository, including files nobody has thought about yet.

A default = [] with zero rows is a load error, not “allow everything”.

[axes]: the ranked ladders

Two ladders, human and machine. Each is a weakest-first list; a name’s index is its rank.

[axes]
human.order = ["contributor", "reviewer", "maintainer"]
machine.order = ["coderabbit", "cubic", "claude-sonnet-5", "claude-opus-5"]

The names are yours. Vidi attaches no meaning to "maintainer" beyond “index 2, therefore outranks index 1”. A requirement of human = "reviewer" is satisfied by anyone at that rung or above.

The two ladders never substitute for each other: a tool review supplies a machine rank and never pads the human axis, however capable the tool. This is lane discipline, and it is the point of having two ladders rather than one.

The machine rank resolves against the review’s model first, then its tool, so you can rank a specific model or fall back to ranking the vendor.

aliases: renaming a rung

[axes]
human.order = ["contributor", "reviewer", "maintainer"]
human.aliases = { "owner" = "maintainer" }

An alias lets a renamed rung keep old ledger stamps and directory entries resolvable. Three rules:

  • One hop, never chained. An alias pointing at another alias resolves nothing. You get a load-time warning, and the review supplies no rank.
  • A real rung wins over a same-named alias.
  • Only role names are alias-resolved. An accept-row minimum must name a rung that actually exists in order.

unranked is reserved on both ladders and as an alias name. It is the record-layer sentinel for “vouched with no directory standing”, so it can never double as a real rank.

[reviewers]: the directory

[reviewers]
"vidi:p:018f3c2e-9d41-7c3a-b1f2-4a5d6e7f8a9b" = { role = "maintainer", name = "Ada" }
FieldRequiredMeaning
roleyesMust name a rung on the human ladder.
namenoDisplay only. Never load-bearing for a gate.

Only people listed here have a standing that can satisfy a rank requirement. A review from an identity absent from [reviewers] supplies no human rank at all. It is not an error; it simply does not count. That is deliberate: departure fully weakens, with no ledger rewrite required.

The key must match the recorded identity exactly

This is the detail that most often makes a correct-looking policy gate nothing.

The key is matched as a literal string against the attesterIdentity.id stored in the review record. Which string that is depends on how you logged in:

LoginRecorded identity[reviewers] key to use
Hosted (vidi login)a person GUID"vidi:p:<guid>"
Local (vidi login --local)the identity you established, by default your git config user.emailthat exact string, e.g. "ada@example.com"

Check what your ledger actually recorded before writing the key: the attesterIdentity.id field of any line in .vidi/reviews.jsonl is the string to copy.

A mismatch is silent by design: an unknown identity is a stranger, and strangers supply no rank. If a vouch you know exists is not satisfying a requirement, this is the first thing to check.

roleAtReview: rank at vouch time

A review stamps the reviewer’s standing at the moment of the ceremony. The rank it supplies afterwards is the weaker of the stamped and the current standing:

StampSupplies
a role namemin(stamped, current)
absent (legacy line)the current directory role
unrankedno rank, ever
a name that resolves to no rungno rank (fail-closed)

So a promotion never retroactively upgrades old vouches, a demotion weakens them immediately, and a vouch made with no standing cannot be converted into coverage later by adding that person to the directory. They re-vouch at the new standing to count.

Accept rows

An accept row is a conjunction of minima, one per axis. Every field is optional individually, but a row with no minimum on any axis is a load error; it would pass with zero review.

{ human = "maintainer", machine = "claude-opus-5", assurance = "session-verified" }
AxisCompared against
humanThe reviewer’s human-ladder rank (see above).
machineThe tool review’s machine-ladder rank.
assuranceThe grade a verified receipt proves. Not a policy ladder; the vocabulary is fixed.

Rows in a list are OR’d; minima within a row are AND’d. So accept reads as “any one of these combinations”.

The grammar admits only >= minima: no negation, no maxima. Policy is therefore monotone by construction: more review, or stronger review, can only ever flip a unit from fail to pass. You cannot write a rule that a further vouch breaks.

The assurance vocabulary

Fixed and server-set, because a client cannot attest to its own assurance:

RungMeaning
self-assertedThe floor. What a receiptless review proves.
session-verifiedStronger.
linkedStronger still.
presence-verifiedThe strongest.

In self-hosted use with no notary, every review sits at self-asserted. An accept row demanding more is unmeetable. It fails every unit rather than passing them, which is the correct direction, but it is not a useful gate until receipts are in play.

[[cover]] vs [[scope]]

The two obligation types differ in a way the names understate.

[[cover]] is proactive. Every unit under paths must carry a fresh, approved review meeting require. Absence is a failure. Use it for a pinned trusted set you intend to hold at full coverage.

[[cover]]
name = "crypto-tcb"
paths = ["crates/vidi-core/src/crypto/**"]
require = { human = "maintainer" }
FieldRequiredNotes
nameyesNames the obligation in diagnostics.
pathsyesExplicit globs. An empty list is a load error.
requireyesA single accept row, not a list.

[[scope]] is reactive. Units under path that are present must meet one of the accept rows.

[[scope]]
path = "crates/vidi-core/src/crypto/**"
accept = [ { human = "maintainer" }, { human = "reviewer", machine = "claude-opus-5" } ]
FieldRequiredNotes
pathyesA single glob. An empty string is a load error.
acceptyesA list of rows, OR’d. An empty list is a load error.

Shortfalls from the two surface under different words in the summary line: [[cover]] and [[scope]] produce requirement failures, while paths falling through to a strict default produce default shortfalls.

An obligation that can never match any path (an empty paths list, an empty-string glob) is refused at load. It would otherwise satisfy the “policy present ⇒ gates something” check from the inside while gating nothing.

Path globs

Matching is segment-wise over /, byte-exact and case-sensitive:

PatternMatches
**zero or more whole path segments
*any run of bytes within a single segment
? [ {literal characters, not metacharacters

crates/**/billing/** matches at any depth, including crates/billing/mod.rs (** spans zero segments). src/*.rs matches src/lib.rs but not src/a/b.rs.

There is no brace expansion and no character classes. This is a deliberately small grammar; a policy glob should not be able to surprise its author.

[severity] and [[exemptions]]

[severity]
major = 0.7

[[exemptions]]
unit = "crates/vidi-core/src/legacy.rs::fn:shim"
reason = "scheduled for deletion, tracked in VIDI-214"

[severity] sets advisory band thresholds. It never gates: significance and severity order the re-review queue and nothing else.

[[exemptions]] deliberately excludes one unit from the unreviewed set; it is removed from the denominator and reads as Exempt. Both fields are required; the mandatory reason gives every exemption its own audit trail in the diff.

A minimal starting policy

Everything advisory except one directory that genuinely requires review:

schema = "1.0"
default = "advisory"

[axes]
human.order = ["contributor", "reviewer", "maintainer"]

[reviewers]
"ada@example.com" = { role = "maintainer", name = "Ada" }

[[scope]]
path = "crates/vidi-core/src/crypto/**"
accept = [ { human = "maintainer" } ]

Expect the requirement-failure count to jump the moment you add that scope: every unit under it is unreviewed until someone vouches. That is the policy working, not a misconfiguration. Widen scopes as coverage grows; a policy that gates the whole repository on day one just fails every build, and a gate that always fails teaches people to ignore it.

Load errors

Every one of these refuses the load outright, and a refusal is exit 2, not the exit 1 of a failed gate. The distinction matters in CI: exit 1 means “the code needs review”, exit 2 means “your policy is broken and nothing was checked”. A job that treats them alike will read a malformed policy as a review backlog.

ErrorCause
parse errorMalformed TOML, an unknown key, or a mistyped field.
schema major mismatchMAJOR differs from the engine’s, either direction.
schema minor aheadFile MINOR is ahead of the engine’s.
empty accept-row listA [[scope]].accept or strict default with zero rows.
accept row states no minimumA row with no axis set.
unknown ladder rungA reviewer role or accept-row minimum naming no rung.
reserved ladder rungA rung or alias named unranked.
vacuous policyPresent, but declares no obligation.
unmatchable obligationA [[cover]]/[[scope]] glob that can match nothing.
policy path escapes the repository.vidi/policy.toml resolves outside the root.

The one non-fatal case is an alias pointing at a non-rung: a warning, and the alias resolves nothing.

Next

Unit states