Rule Cascade
ReferenceDecision records (ADRs)

ADR 0011: The 422 answer shows only the rules on the client channel

Status: accepted

Status: accepted

Context

A runtime's helper for the answer to a denied operation (problem(), the problem-details helpers, 422 with type urn:rule-cascade:rule-violation) put the whole evaluation in the body, and the enforcement guide told backends to return it. When a server-only rule denied, such as org.transfer.blocked-country, its id, message and params reached the browser, although the client manifest (specification section 7) leaves that rule out on purpose. A user could learn which hidden checks exist and how they are configured.

Decision

  • The answer keeps only the findings and effects of rules on the client channel, in order: the rules the client manifest contains (specification section 9). Every runtime derives that list the same way (clientRules()): the client manifest's rule ids when there is one, otherwise the server rules with enforcement client or both that are not actions.
  • A hidden blocking finding becomes one generic finding, added after the ones shown: code RULE-DENIED, message This operation is not allowed., blocking: true, no rule and no source. A hidden warning is dropped without one. detail counts the blocking findings shown. The error keeps the whole result for the server's logs.
  • Without the client rule ids, no rule is on the client channel (fail closed): the answer holds only RULE-DENIED when something blocked. enforce always passes the ids; a caller that builds the answer from evaluate passes clientRules().
  • Effects are filtered too, not only findings: value and state effects name their rule, and a server-only compute rule can expose a value. Commands need nothing, since they only exist on allow.

Consequences

  • The 422 body changes for server-only denials, and Go's ProblemDetails gains a clientRules argument. Released while the packages are alpha.
  • Existing middleware users keep their client findings unchanged: every shipped adapter builds the body from the error, and enforce passes the client rules.
  • A hand-built answer that forgets the ids shows less, not more.
  • RULE-DENIED, like RULE-EVALUATION-ERROR, is a code the runtimes produce; a ruleset finding should not use it.
  • A RULE-EVALUATION-ERROR on a server-only rule becomes RULE-DENIED, so its detail is hidden too. The same error on a client rule still carries its detail.
  • Conformance has six new problem-details cases (2 → 8).

Alternatives considered

  • Keep the shape and add an opt-in filter (problem({ channel: 'client' })), documenting that server-only messages and params must be safe to show. No break, but the default stays leaky and safety depends on every rule author.
  • Keep it as it is. No work, but the error path undoes what the client manifest hides.
  • Show everything when the client rule ids are absent. Friendlier to hand-built answers, but it keeps the leak as the default.

On this page