ADR-0034 — Negative claims are observational, not adversarial
ADR-0034 — Negative claims are observational, not adversarial #
- Status: Accepted
- Date: 2026-05-13
- Deciders: Natan
- Source:
docs/research/2026-05-09-hacking-apis-dossier.md↗ §P6;
docs/research/2026-05-09-master-synthesis.md ↗ Convergence G (§2.7); compatible-extension of ADR-0007.
Context #
PRD-15 introduces a ## Negative claims section convention to every contract doc — six initial N-claims appended to docs/contracts/snappy/domain-activation.md (N1–N6) plus a src/lib/negative-probe.ts helper spec.
A negative claim asserts the absence of a documented non-promise. Example: N3 says "snappy does NOT honor a client-supplied status: "active" on POST /api/domains," observed by reading the resulting Mongo domains doc and domain_status_history rows from outside.
ADR-0007 already pins SQA's scope as operational, not experience. Its falsifiability test is LLM-judgment drift: "if we add a probe that requires LLM-as-judge … this ADR is being violated." ADR-0007 was written before negative-contract patterns were considered. The new drift axis ADR-0007 does not cover is adversarial-input drift — the gradual slide from "send one malformed fqdn, verify no side-effect" (operational) toward "send 2,700 XSS payloads and scan for which bypasses validation" (security audit). The hacking-apis dossier flags this risk explicitly and says without an ADR pinning the boundary, drift is "near-certain within 2–3 quarters."
This ADR extends ADR-0007's spirit to the adversarial-input axis, without enlarging ADR-0007's scope or contradicting it. It keeps negative claims firmly inside ADR-0007's in-scope band ("HTTP probes, database reachability, … schema consistency") by pinning three operational invariants on the shape of every N-claim.
Decision #
A negative claim in an SQA contract is an observational assertion, not an adversarial probe. Concretely, every N-claim that ships must satisfy all three of:
- One well-formed request per assertion. A negative claim
issues a single request (or a small bounded battery — see bound below) and reads stores SQA already reads (Mongo / S3 / ClickHouse / Loki / response body). It does not scan, brute-force, enumerate endpoints, or probe protocol edges.
- Bounded payload battery. Where a claim varies input shape
(N2 — malformed-fqdn battery), the battery is at most ~20 inputs, chosen for coverage of input shapes (empty string, oversize, scheme-included, IP-literal, null-byte, SSRF-target, IPv6, encoded variant, etc.), not for coverage of injection signatures (no XSS payload lists, no SQLi corpora, no fuzzing dictionaries). 20 is a hard cap; the first contract (PRD-15) lands well under it (~9 in N2).
- Deterministic boolean outcome. The assertion produces a
boolean (absent / present, equals / differs) over data SQA already observes. No LLM-as-judge, no audience-modelling, no quality scoring — the ADR-0007 primary falsifiability test must still pass for every N-claim.
A claim that needs more than 20 inputs, or that performs scanning / brute-force / endpoint discovery, or that requires LLM judgment to interpret, is not a negative claim. It is either:
- a security audit (use Burp Suite Pro, ZAP, the real WAF
programme — separate tooling, separate cadence, separate ownership), or
- a fuzz test (use a fuzzer — separate tooling), or
- an audience-quality test (out of scope per ADR-0007).
N6 — the borderline case (X-Forwarded-For trust) #
N6 ("the audit trail does not blindly trust origin headers") sits at the edge of operational vs security. The dossier flags it as borderline because asserting "snappy does not honor X-Forwarded-For" sounds like a security claim.
This ADR rules N6 in-scope under one framing constraint: N6 asserts a data-integrity / audit-trail invariant, not a security invariant. The observable is "the IP recorded in domain_status_history.metadata.ip (or in the Loki log line) equals the requester's actual pod IP," not "snappy is not vulnerable to header spoofing." The first is a deterministic equality check against data SQA writes the request from; the second would require modelling threat actors. N6's contract row must be phrased in the integrity framing — if a future contract author phrases it as "rejects spoofed X-Forwarded-For," that's a scope violation under this ADR.
Consequences #
- Pro: Negative claims become a first-class part of every
contract doc without enlarging SQA's operational charter. ADR-0007's in-scope band already covers "deterministic boolean observation against stores we read"; this ADR pins the fact that adversarial-input negative claims sit inside that band when they satisfy the three invariants above.
- Pro: The 20-input cap is a hard signal. A reviewer seeing a
PR with a 200-input payload battery can cite this ADR and reject without re-litigating the scope question. Same for any PR adding scanning or endpoint enumeration.
- Pro: Future contracts (PRD-08
crawl-url-via-enqueue,
PRD-09 auto-extract, etc.) inherit the convention. The ## Negative claims section becomes load-bearing across the contract corpus, not a one-off.
- Con: Negative-contract authors will sometimes want to push
past the 20-input cap (e.g. "what if we added 50 SSRF-target IPs?"). The ADR forces those authors to either narrow the claim shape or file a separate security-audit ticket. This is a feature, not a bug.
- Con: N6 sits closer to the edge than N1–N5. Future
origin-header N-claims (Origin, Host, Referer trust) must each individually pass the integrity-framing test. They are not auto-included by precedent.
- Falsifiability: If an N-claim ships that (a) issues more
than 20 requests per assertion, or (b) requires LLM judgment to interpret, or (c) is phrased as "rejects malicious X" rather than "does not produce side-effect Y on input Z," this ADR is being violated. Either (i) the claim needs to move to a security audit programme, or (ii) the claim needs to be re-framed as integrity, or (iii) we consciously supersede this ADR.
Relationship to ADR-0007 #
ADR-0007 covers LLM-judgment drift. ADR-0034 covers adversarial-input drift. Together they pin SQA's scope on both axes: outputs (no LLM judging quality) and inputs (no adversarial scanning). Neither ADR supersedes the other; both must be checked when a new probe lands.
See also #
- ADR-0007 — the
scope boundary this ADR extends.
- ADR-0027 —
external-observer evidence model; N-claims emit gap rows the same way C-claims do.
§P6 — the proposal text this ADR ratifies.
Convergence G (§2.7) — the charter.
- PRD-15 ↗ — the PRD that
drives this ADR.