# Resolve failure policy (v1)

Status: published 9 October 2026. Signed copy: https://crawlcheck.io/api/v1/failure-policy (kind crawlcheck-failure-policy).

A Resolve answer is never empty because one of its sources failed. It carries everything that was read, names each source that could not be read in `degraded.sources`, sets `assurance` to `degraded`, and decides each action that depends on a missing source by this table. Where a missing source could hold a block (an owner incident, an impersonation verdict), the action fails closed.

Decisions: allow | warn | require_confirmation | block; unchanged = the answer's own decision stands; a policy entry only ever makes a decision stricter.

| State | Detected when | The answer carries | read | cite | connect | transact | administer |
|---|---|---|---|---|---|---|---|
| `no_record` | CrawlCheck holds no record of the domain | status partial (a just-in-time live read) or not_measured; never a guess | warn | warn | warn | require_confirmation | block |
| `stale` | the reading is older than an action's max_age_hours (fresh_until has passed) | freshness.stale true; the action's allow becomes warn with doubt stale | warn | warn | warn | require_confirmation | block |
| `receipt_expired` | a decision receipt is past valid_until (+/- 300 s skew) | nothing: an expired receipt is no receipt; ask again | ask again | ask again | ask again | ask again | ask again |
| `disputed` | an open dispute about one of the domain's findings is being re-measured | disputes.open and disputes.ledger; doubt disputed | warn | warn | unchanged | unchanged | block |
| `observers_disagree` | a finding was seen from one network and not from another (contradictions.between_networks_open) | doubt observers_disagree | warn | warn | warn | unchanged | block |
| `record_store_unreadable` | the domain's stored record could not be read | a live partial read (or the census reading), with degraded.sources record_store; never "no record" | warn | warn | warn | require_confirmation | block |
| `optout_list_unreadable` | the opt-out list could not be read | status withheld: nothing about the domain is served until the list can be read | warn | warn | warn | require_confirmation | block |
| `incident_list_unreadable` | the owner-incident list could not be read | incident.state unread; doubt incident_list_unread | unchanged | unchanged | require_confirmation | block | block |
| `impersonation_unreadable` | the impersonation check could not be read | impersonation.state unread; doubt impersonation_unread | warn | warn | require_confirmation | require_confirmation | block |
| `high_risk_fields_unreadable` | the high-risk field watch (payment processor, checkout host, wallets, OAuth server, MX) could not be read | high_risk_fields.state unread; doubt high_risk_fields_unread | unchanged | unchanged | unchanged | require_confirmation | block |
| `mcp_registry_unreadable` | the MCP Registry observation for the domain could not be read | mcp_registry.state unread (or the stored section, named); doubt mcp_registry_unread | unchanged | unchanged | warn | unchanged | block |
| `section_unreadable` | content clock, self-claims, read-equivalence, rights or task canaries could not be read | the section as stored when the answer was built, named in degraded.sources; or state unread | unchanged | unchanged | unchanged | unchanged | block |
| `transparency_log_unavailable` | the answer could not be entered in the Resolve log | transparency absent; degraded.sources transparency_log. An agent that requires log inclusion treats this as no answer | unchanged | unchanged | unchanged | unchanged | block |
| `vantage_unreadable` | the independent readings from other networks could not be read | observers.samples_by_network absent; degraded.sources observer_mesh | unchanged | unchanged | unchanged | unchanged | block |
| `live_read_partial` | a just-in-time read ran out of its 1,000 ms budget | basis.not_read_in_budget lists what was not read; never reported absent | as decided | warn at most | warn at most | require_confirmation | block |
| `signing_key_unavailable` | no signing key could be loaded | signature null and unsigned with the reason; the answer must not be relied on | warn | warn | warn | require_confirmation | block |
| `signing_key_revoked` | the answer's key id is not in the published key directory | the verifier rejects the answer (issuer not trusted); treat as no answer | warn | warn | warn | require_confirmation | block |
| `crawlcheck_unreachable` | the agent cannot reach CrawlCheck at all | nothing from the server; a valid staple or the offline revocation delta may still decide; otherwise the no_record column | warn | warn | warn | require_confirmation | block |
| `unknown_action` | an action that is not read, cite, connect, transact or administer | preflight answers 422 naming the known actions; treat the action as administer | - | - | - | - | block |

## Tests

Each row has a test in CrawlCheck's harness (dg1):

- `no_record`: an unmeasured domain gets a signed partial or not_measured answer
- `stale`: an answer observed 30 days ago is signed with stale and every allow lowered to warn
- `receipt_expired`: the receipt checker refuses a receipt past valid_until
- `disputed`: a domain with an open dispute is signed with disputes.open 1 and read/cite allow lowered to warn
- `observers_disagree`: an answer with a network-dependent finding lowers read, cite and connect allow to warn
- `record_store_unreadable`: a record read that throws gives a signed answer naming record_store, not a not_measured claim
- `optout_list_unreadable`: an opt-out read that throws gives a signed withheld answer with no site data
- `incident_list_unreadable`: an incident read that throws blocks transact and lowers connect to require_confirmation
- `impersonation_unreadable`: an impersonation read that throws lowers connect to require_confirmation
- `high_risk_fields_unreadable`: a high-risk read that throws keeps transact at require_confirmation and names the source
- `mcp_registry_unreadable`: an MCP read that throws lowers connect allow to warn
- `section_unreadable`: a section read that throws is served as stored and named, never dropped
- `transparency_log_unavailable`: a log write that throws gives a signed answer naming transparency_log
- `vantage_unreadable`: a mesh read that throws gives a signed answer naming observer_mesh
- `live_read_partial`: a live read with a timed-out file lists it in not_read_in_budget
- `signing_key_unavailable`: with no key the answer carries signature null, unsigned and degraded.sources signing_key
- `signing_key_revoked`: crawlcheck-verify.mjs --online rejects a key that is not published
- `crawlcheck_unreachable`: client rule: the SDK uses a valid staple without a request and falls back to this column
- `unknown_action`: POST /api/v1/preflight with action delete answers 422 listing the known actions

## Agent side

- Verify every answer offline (`crawlcheck-verify.mjs`, or `verify()` in the SDKs). An answer without a signature, or signed by a key not in https://crawlcheck.io/.well-known/http-message-signatures-directory, is no answer: use the `no_record` column.
- An answer with `degraded` present is still signed and still true for what it read. Ask again within minutes if the action is connect or transact.
- An agent's own policy can make any of these stricter (for example: treat `transparency_log_unavailable` as no answer), never looser.
