Declarative policy wire contracts
Surface B exposes serializable rule and policy types for library hosts, even though the built-in web profile constructs them in Rust. These objects are security semantics, not free-form metadata: losing a matcher, scope, or condition must not silently make a rule apply more broadly.
The built-in CLI does not load reasoning, verification, or adaptation rules from remote responses or local configuration. The contracts on this page are therefore host-facing and preparatory for future declarative profiles; they are not a claim that the default CLI accepts an attacker-supplied policy file.
Strict semantic objects
Unknown fields reject in the following complete policy objects and their private wire representations:
- expression nodes;
- evidence selectors, calibrations, conclusions, and reasoning rules;
- outcome selectors, pipeline directives, and adaptation rules;
- verification rules.
These types have no documented extension namespace. Accepting an unknown field
would let a spelling error erase a constraint. Objects that intentionally carry
extensions, such as AttackAction and VerificationCase, retain that behavior
but reject unknown names in their reserved policy namespaces.
Missing is different from explicit null
Three nullable fields must be present on the wire:
| Object | Field | Explicit null |
Missing field |
|---|---|---|---|
| claim expression | value |
predicate existence | reject |
| evidence selector | value |
predicate existence when no other matcher is present | reject |
| adaptation rule | condition |
deliberately unconditional rule | reject |
This preserves the historical serialized shape. Existing serializers already
emitted these fields, including null; the reader now distinguishes that
explicit choice from an omitted or misspelled field.
Compatibility guards
Canonical non-default policies emit a small guard describing the constraint that must still be present:
| Policy | Canonical guard | Protected loss |
|---|---|---|
| constrained evidence selector | matcher_policy_guard: true |
exact/text/list matcher |
| bounded evidence calibration | aggregation_policy_guard: true |
contribution cap |
| scoped verification rule | verification_scope_guard |
action, case, or both |
| conditional adaptation rule | condition_policy_guard: true |
condition |
A present guard must exactly agree with the semantic fields. Guardless policy objects produced by the immediately preceding format remain readable and are canonicalized with a guard when serialized again. Default existence, independent aggregation, unscoped verification, and explicitly unconditional adaptation remain guardless.
These guards detect a missing or inconsistent semantic field; they are not a cryptographic integrity mechanism. Removing both a field and its guard cannot be distinguished from a historical default object. Archive integrity and authentication remain the host's responsibility.
The aggregation guard is also old-reader fail-closed because the preceding calibration reader already rejected unknown fields. The selector, verification, and adaptation guards protect current-reader reconstruction; preceding readers understand every existing guarded matcher, scope, and condition and therefore preserve an intact object. A future semantic variant that an older reader does not understand needs its own old-reader rejection design. The current guards must not be treated as generic versioning.
This does not retroactively harden preceding binaries against damaged wire. Those readers ignore the selector, verification, and adaptation guards, and the preceding expression reader has no equivalent guard, so removing one of their known semantic fields can still broaden an object when it is read by an older binary. Updated readers reject the same corruption. If durable mixed-version declarative loading becomes a product surface, it needs a separately designed versioned policy envelope; intact current semantics do not justify that wire migration before a real consumer exists.
Validation boundaries
PipelineDirective validates standalone deserialization as well as nested rule
construction. Blank scheduled action IDs, zero throttle delays, unknown fields,
and unknown directives reject. VerificationCase reserves payload_*,
applies_hypothesis_*, and verification_*, preventing a policy-looking
extension from being ignored and reconstructed as transition-authorized.
Accepted objects still pass the same constructors and invariants used by the
programmatic API. Deserialization does not infer negative knowledge, reject a
hypothesis, or turn a missing field into false.
Resource ownership
Expression evaluation, cloning, and serialization are recursive. The standard
profiles contain small fixed trees, and serde_json applies its own input
recursion limit, but public hosts can construct larger trees directly or use a
different deserializer. A host that introduces a declarative loader must bound
the policy byte size, tree depth, node count, string/list sizes, action count,
and dependency depth before installing it. This contract intentionally does not
invent one global limit without a production loader and measured workload.
Fuzz targets use stricter harness-only limits so scheduled campaigns remain reproducible and cannot become an unbounded CI workload.
Output compatibility
The built-in web profile, request behavior, planner utility, verification
outcomes, hypothesis states, ExperienceStore, AttackPlan, and
decision-scan/v1 output are unchanged. The only wire-shape additions are
guards on non-default declarative policy definitions. Public-library hosts that
previously relied on unknown fields inside strict semantic objects now receive
an error instead of a broader policy.