Guard acceptance suite
A guard is the check an agent runs where the action happens: ask CrawlCheck, verify the signed decision, and act only if it permits this operation. This suite is the line every guard must hold, ours included: 54 cases with signed receipts, 9 of them through redirects. Download cases.json; run it against any implementation.
54
cases, suite 1.2.0
54 / 54
@crawlcheck/sdk 1.3.0 (npm)
54 / 54
crawlcheck 1.3.0 (PyPI)
41 / 41
reference verifier, receipt cases (eligibleToAct)
What a guard must check
In this order; the first failure refuses.
| Check | Rule | Cases |
|---|---|---|
| Kind and version | The answer is a crawlcheck-decision with v 1. Any other document, an empty object, an array or text is refused. | wrong-kind, wrong-version, malformed-* |
| Integrity | The canonical JSON hashes to sha256 and the Ed25519 signature verifies over “crawlcheck-data-v1\n” + sha256 with the key whose RFC 7638 thumbprint is the kid. A field added or changed after signing is refused; an unknown field that was signed is accepted. | tampered, tampered-unsigned-field, unknown-signed-field, unsigned, kid-mismatch |
| Issuer | The kid is in CrawlCheck’s published key directory (or a pinned list), and so is every key the receipt leans on (the key of the resolve answer it names). Keys are separated by purpose, so those may differ; a key that is not published is refused: integrity is not trust. | foreign-key, resolve-key-foreign, purpose-key-decision |
| Same domain | The receipt names the domain about to be contacted (www. is the same site; a subdomain is not). | other-domain, subdomain, www-same |
| Same action | The receipt names the action about to be taken. A read allow never covers transact. | other-action |
| This request | The nonce the guard sent comes back in the receipt. Another request’s receipt, or one without the nonce, is a replay. | nonce-other, nonce-missing |
| Same operation | When the guard names the operation (method and URL, or an MCP tool with the digest of its definition and its scopes), the receipt must carry exactly that operation. A receipt for another URL, method, tool definition or set of scopes is refused; a guarded fetch asks again for every hop. | op-*, fetch-receipt-reuse |
| In time | Now is before expires_at plus the stated clock skew, capped at 300 s; decided_at is not in the future; the receipt is not valid for longer than its action allows. | expired, expired-in-skew, skew-inflated, future, lifetime-*, no-expiry |
| Bound to evidence | The receipt names the resolve answer it was made from (resolve.sha256), signed by the same key. | resolve-unbound |
| The decision | allow proceeds. warn proceeds only when the caller opted in. require_confirmation proceeds only when a human confirmed. block, unsupported and any unknown value never proceed. | allow, warn-*, confirm-*, block, unsupported, decision-unknown |
| Fail closed | No answer, an error status, a rate limit or an unreachable CrawlCheck refuses. Nothing proceeds on a default. | http-500, http-429, unreachable |
| Every hop | A guarded fetch checks before the first request and again before each redirect to a new domain; a refused hop is never requested; only http(s) to a domain name; at most 10 redirects; the receipt header goes only to the domain it names. | fetch-*, redirect-* |
How long a receipt may last, per action
CrawlCheck issues shorter ones when the evidence is due sooner.
| Action | At most |
|---|---|
| read | 24 h |
| cite | 24 h |
| connect | 1 h |
| transact | 5 min |
| administer | 5 min |
| clock skew | 5 min |
Every case and what must happen
Receipts are signed with a test key published in the suite’s own directory.
| Case | Situation | Must |
|---|---|---|
allow | allow proceeds positive control | proceed |
warn-default | warn does not proceed by default warn means a stated doubt; the caller must opt in | refuse |
warn-allowed | warn proceeds when the caller allows warn positive control | proceed |
confirm-missing | require_confirmation without a confirm callback no human, no action | refuse |
confirm-no | require_confirmation, human says no | refuse |
confirm-yes | require_confirmation, human says yes positive control | proceed |
block | block never proceeds, whatever the options | refuse |
unsupported | unsupported never proceeds | refuse |
decision-unknown | an unknown decision value (signed) never proceeds fail closed on values this guard does not know | refuse |
other-domain | a valid allow for another domain a receipt is valid only for the domain it names | refuse |
subdomain | a valid allow for the parent domain, asked for a subdomain no inheritance across hosts | refuse |
www-same | www. is the same site CrawlCheck stores a site without www. | proceed |
other-action | a valid read allow, used to transact a receipt is valid only for its action | refuse |
expired | expired an hour ago | refuse |
expired-in-skew | expired 60 s ago, inside the stated 300 s clock skew the skew is part of the receipt | proceed |
skew-inflated | claims a 1-day clock skew to stay alive skew is capped at 300 s | refuse |
future | decided an hour in the future a clock that far off cannot be trusted | refuse |
lifetime-transact | a transact allow valid for 24 hours transact receipts last at most 300 s | refuse |
lifetime-connect | a connect allow valid for 24 hours connect receipts last at most 3600 s | refuse |
no-expiry | no expires_at | refuse |
nonce-other | a receipt carrying another request's nonce replayed receipt | refuse |
nonce-missing | the request sent a nonce, the receipt has none | refuse |
op-match | a receipt for exactly this request positive control | proceed |
op-other-url | a receipt for another URL on the same domain a receipt is valid only for the request it names | refuse |
op-other-method | a GET receipt used for a POST | refuse |
op-missing | the request names an operation, the receipt names none an unscoped receipt cannot stand in for a scoped one | refuse |
op-tool-schema | a receipt for the same tool with a different definition the tool changed since it was decided | refuse |
op-scopes | a receipt for fewer scopes than requested | refuse |
purpose-key-decision | a decision signed by another published key than its resolve answer keys are separated by purpose; both are published | proceed |
resolve-key-foreign | a decision whose resolve answer names a key that is not published every key a receipt leans on must be published | refuse |
tampered | a block receipt edited to allow the digest no longer matches | refuse |
tampered-unsigned-field | a field added after signing every field is signed | refuse |
unknown-signed-field | an unknown field that was signed forward compatible: ignore unknown fields, but only signed ones | proceed |
foreign-key | a valid signature by a key CrawlCheck does not publish integrity is not trust | refuse |
kid-mismatch | a published kid on a signature by another key the key id must be the key's thumbprint | refuse |
unsigned | no signature | refuse |
resolve-unbound | no resolve answer named a decision must name the answer it was made from | refuse |
wrong-kind | a signed document of another kind only a decision receipt decides | refuse |
wrong-version | a decision of an unknown version | refuse |
malformed-empty | an empty object | refuse |
malformed-array | a JSON array | refuse |
malformed-text | not JSON | refuse |
http-500 | CrawlCheck answers 500 fail closed | refuse |
http-429 | CrawlCheck answers 429 fail closed | refuse |
unreachable | CrawlCheck cannot be reached fail closed | refuse |
fetch-ok | a guarded fetch with no redirect positive control | proceed |
fetch-blocked | a blocked domain is never contacted the check runs before the request | refuse |
redirect-to-blocked | an allowed page redirects to a blocked domain every hop is re-checked; the blocked hop is never requested | refuse |
redirect-to-allowed | an allowed page redirects to another allowed domain each domain gets its own receipt | proceed |
redirect-relative | a relative redirect stays on the checked domain | proceed |
redirect-scheme | a redirect to a file: URL only http(s) | refuse |
redirect-loop | more than the hop limit at most 10 redirects | refuse |
redirect-ip | a redirect to a bare IP address an address is not a domain CrawlCheck can vouch for | refuse |
fetch-receipt-reuse | a receipt for one page used for another page on the same domain each hop asks again; the first page's receipt never covers the second | proceed |
Run it against your guard
CI runs it on every SDK change, against the copies on npm and PyPI after each release, and every Monday.
- Serve
directoryat/.well-known/http-message-signatures-directoryand answerPOST /api/v1/preflightwith each case’scrawlcheck(status and body;unreachable= refuse the connection;by_domain= the receipt for the domain asked). - Call your guard with the case’s
request, pinningnonceandnow. For a fetch case, also servesitesand record every URL requested. expect.proceedmust match, and for fetch cases the requested URLs must match exactly. A refusal must say why.
Our runners: sdk/test/guard-acceptance.test.js in @crawlcheck/sdk and tests/test_guard_acceptance.py in crawlcheck (both ship the suite in their test fixtures). For a receipt you already hold: node crawlcheck-verify.mjs receipt.json --act example.com:read --nonce <yours> --online, or run-verifier.mjs over the whole suite.
What the SDKs before 1.1.0 did: their guard verified the signature and the decision but not the scope. In this suite 1.0.9 proceeded on a valid receipt for another domain or action, an expired one, one with an inflated skew or too long a life, a replayed nonce, and a signed document of another kind or version (14 cases), and had no guarded fetch, so nothing re-checked redirects (8 cases). 1.1.0 added those checks but did not scope a receipt to the operation: it accepted a receipt for another URL, method, tool definition or set of scopes (5 cases), and its guarded fetch reused one receipt across pages. 1.2.0 still required a decision and its resolve answer to share one key, which stops holding once decisions get their own key on 2026-11-09. Upgrade with npm i @crawlcheck/sdk@^1.3.0 or pip install -U crawlcheck.
Suite built 2026-10-10. Related: quickstart (the guarded fetch), failure policy, assurance.