Skip to content

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.

Two homes, both additive (a rule with no when keeps working as before):

  • Globalglobal_rules[] in policies/global-policy.yaml. An optional when predicate; non-overridable.
  • Surfacesurfaces.<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]

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).

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 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: log

not(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.

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 missing
  • reject fails verification; warn is accepted-with-warning; info logs only.
  • A repo policy can never weaken a global gate: custom_rules have 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.

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:

Terminal window
dogfood-verify lint policies/repos/<org>/<repo>.yaml

It 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.