Policy DSL
A policy rule used to require a PR into the verifier. As of v1.7.0 you author
rules declaratively in YAML — a bounded, no-eval predicate the engine
evaluates, no code change per rule. This page is the operator reference; the
full contract is docs/policy-dsl.md.
Where rules live
Section titled “Where rules live”Two homes, both additive (a rule with no when keeps working as before):
- Global —
global_rules[]inpolicies/global-policy.yaml. An optionalwhenpredicate; non-overridable. - Surface —
surfaces.<surface>.custom_rules[]in a repo policy. Per scenario_result; repo-authored; reject/warn/info only.
surfaces: web: custom_rules: - id: actor-allowlist severity: reject description: web proof must come from an allowlisted actor when: field: attested_by op: not_in value: [ci-bot, release-bot]The predicate
Section titled “The predicate”A predicate node is one of:
| Node | Shape |
|---|---|
| leaf | { field, op, value? } |
| all | { all: [ … ] } (AND) |
| any | { any: [ … ] } (OR) |
| not | { not: … } |
| implies | { implies: [ antecedent, consequent ] } |
when describes the VIOLATION — the rule fires when when is true. A
forbidden combination reads naturally; an “X requires Y” rule uses implies
(it matches the inputs that break X ⇒ Y, so you write positive clauses
instead of a double negation).
Operators (closed set)
Section titled “Operators (closed set)”| Operator | Meaning |
|---|---|
equals / not_equals |
strict scalar compare |
in / not_in |
membership in an array value |
contains / not_contains |
array membership or string substring |
exists / not_exists |
truthy / falsy (no value) |
gt / gte / lt / lte |
numeric compare |
There is no matches (regex) operator — the named use cases do not need it and
an operator-supplied regex is a ReDoS surface. exists is truthiness by design
(an empty string reads as absent), which keeps attested-if-human faithful to
its original behavior.
Field paths and the [] footgun
Section titled “Field paths and the [] footgun”field is a dotted path; a [] segment means “any element” (existential).
scenario_results[].tags selects every scenario’s tags.
A [] path over an empty/absent array matches nothing, so a negative
operator over [] is a fail-open for a reject rule. To say “reject if no
element satisfies P”, use the fail-closed idiom:
# reject web proof that has no log evidence:when: not: any: - field: evidence[].kind op: contains value: lognot(any(...)) fires on an empty collection; a bare not_contains over []
would not. The dogfood-verify lint verb (below) warns when it spots a bare
negative operator over a [] path, so you catch this at authoring time rather
than when a real submission slips through.
Scope and reasons
Section titled “Scope and reasons”A rule’s scope is submission (default for global rules) or
scenario_result (always, for custom_rules). At scenario_result scope the
predicate runs per element, in array order, emitting one reason per offending
element. A reason_template interpolates {slot} fields of the matched element:
- id: attested-if-human severity: reject scope: scenario_result when: implies: - field: execution_mode op: in value: [human, mixed] - field: attested_by op: exists reason_template: >- scenario "{scenario_id}": execution_mode is "{execution_mode}" but attested_by is missingSeverity, non-weakening, safety
Section titled “Severity, non-weakening, safety”rejectfails verification;warnis accepted-with-warning;infologs only.- A repo policy can never weaken a global gate:
custom_ruleshave no accept/except verb (the grammar to grant an exception does not exist), and combining is deny-overrides. - The engine is a pure interpreter — no
eval,Function,vm, or dynamic require. Field reads never touch the prototype chain. Work is bounded: combinator depth ≤ 5, width ≤ 64, a per-evaluation node budget, and a[]fan-out cap. A pathological predicate is refused as a classified fault, never a hang.
When a rule is malformed
Section titled “When a rule is malformed”Most mistakes (unknown operator, bad node shape, a banned __proto__ path
segment) are caught when the policy loads and inherit the right routing.
An eval-time semantic fault in a repo custom rule (a type mismatch, an
unknown field) is a policy-config: rejection — the repo fixes its rule. The
same fault in the global policy is operational (it pages ops; the studio
fixes its config). See the error codes.
Lint a policy before you ship it (dogfood-verify lint)
Section titled “Lint a policy before you ship it (dogfood-verify lint)”You don’t have to wait for a submission to find a malformed rule. As of v1.8.0:
dogfood-verify lint policies/repos/<org>/<repo>.yamlIt runs the structural gate plus the data-independent predicate checks
(unknown leading field, combinator over-depth, node budget) over every when
predicate and batch-reports each fault — no submission needed. It also emits an
advisory warning on the [] footgun above (and suggests the fail-closed
rewrite; it never auto-applies it). Exit codes: 0 clean or warnings-only, 1
errors, 2 bad invocation. Add --json for CI.
Be aware of the boundary: static lint cannot catch a type_mismatch (a
numeric op over a non-number) or a fan-out overrun — those depend on submission
data. The lint says so; run dogfood-verify --file <submission> --explain to
exercise that path. Full reference:
docs/policy-lint.md.
