Risk-Acceptance Receipts
When a team accepts a known security risk and ships anyway, the decision usually lives in a comment thread, a ticket, or a spreadsheet cell that anyone can edit after the fact. A risk-acceptance receipt turns that exception decision into a signed, hash-chained, anchored evidence object. Asqav signs a receipt of type protectmcp:lifecycle:risk_acceptance that proves a named approver authored a reason, bound to a specific scan artifact digest and a point-in-time risk snapshot, key-signed with the agent's post-quantum key, linked into the per-agent hash chain, and timestamped by an anchor. It is the portable audit-trail object the approval-workflow gap is missing, and it reads the same whether the underlying scan came from a SAST tool or a secrets scanner.
What a risk-acceptance receipt proves, and what it does not
A risk-acceptance receipt proves exactly three things and nothing more: the exception decision existed, it was authored by the holder of a specific signing key, and it held a fixed position in the hash chain at the time it was issued. Every field below carries that and only that meaning.
The signature is applied server-side by Asqav, an operator unaffiliated with the agent's operator. The recorded party never holds the signing key, so it cannot forge or backdate its own record. That separation is what lets the receipt support audit-trail and recordkeeping obligations: the evidence comes from outside the party it is evidence about.
Asqav never makes a value true, reproducible, enforced, or trusted. The receipt is a record, not a control. It does not re-run the scan, recompute a score, enforce an expiry, or check that the approver was entitled to approve. Those are the deploying organization's decisions. Asqav's job is to make the decision that was taken tamper-evident and independently verifiable.
The scope rule, stated explicitly
These honesty statements are normative on the wire, published at GET https://api.asqav.com/.well-known/governance.json, and enforced by validators on the signing path:
- The
risk_snapshotis a snapshot of third-party signals as the producer read them at decision time. It is NOT a score Asqav computed or verified, and it is NOT reproducible from inputs Asqav holds. Asqav does not fetch, compute, verify, or query any EPSS, KEV, or CVSS source. - The
sarif_digestproves the SARIF scan artifact existed unaltered at time T. It does NOT prove the scan was re-run, that the finding is real, or that the scan was correct. Asqav never parses, fetches, re-runs, or validates the scan. - The
expires_atvalue is declared by the producer, not enforced by Asqav. Asqav records the declared expiry and does NOT auto-revoke. Refusing a stale acceptance is the deploying organization's gate. - The
approver_idandinitiator_ididentities are bound into the signed bytes as fields. Asqav performs NO separation-of-duties check and never compares the two values. Binding an identity is not authenticating it.
The EPSS, KEV, and CVSS signals in risk_snapshot are labeled point-in-time reads, not a closure of any reproducibility gap. A verifier reads them as what the producer asserted at snapshot_at from snapshot_source, never as platform-derived risk.
The fields
A risk-acceptance receipt carries the base receipt fields every Asqav receipt carries, plus the acceptance-specific extension fields. Every extension field below is producer-supplied and signed into the payload.
approver_id: producer-asserted identity that authored the acceptance. Bound into the signed bytes. No authority check, no segregation-of-duties comparison, no auth. Required.initiator_id: producer-asserted identity that requested the acceptance. Bound into the signed bytes. Never compared toapprover_idby Asqav.acceptance_reason: free-text producer rationale for accepting the risk. Proves the reason existed and was key-authored at signing time, never parsed or scored. Required. A practical convention: open the text with the policy or risk-appetite clause the acceptance rests on, so a reader meets the authority before the rationale.accepted_at: producer-asserted authoring time of the acceptance. Self-declared. The only Asqav-attested times areissued_atand the entries inanchors[].expires_at: the time after which the deployer treats the acceptance as stale. The server derives it from the sign request'svalid_seconds(signed_atplusvalid_seconds, falling back to a fixed default of 86400 seconds, 24 hours, overridable per request viavalid_seconds_default, when unset). Recorded, never enforced. Asqav does not auto-revoke.supersedes: producer-asserted pointer to the prior risk-acceptance receipt this one replaces. Proves the supersession claim existed at T. Asqav never invalidates the prior receipt, which stays immutable in the chain. The supersedes walk below shows how a verifier decides which receipt is in force.sarif_digest: SHA-256 over the producer-declared canonical SARIF bytes, insha256:<64 lowercase hex>form. A shape-only existence proof. Asqav never re-runs or validates the scan. It is context forfinding_ref: the digest pins the exact scan artifact the finding came from.finding_ref: the primary binding between the acceptance and the finding it accepts. The recommended form is the SARIFruleIdplus the finding'spartialFingerprints, which hold stable across scan runs while line numbers and file URIs drift. Producer-asserted, never resolved or validated by Asqav.approval_ref: producer-asserted opaque correlation pointer to a human-approval id or external ticket. Free-text, never resolved or validated by Asqav.risk_snapshot: point-in-time snapshot of third-party risk signals as read by the producer. Carriessnapshot_at,snapshot_source, optionalepss,cvss,cvss_vector,kev_listed, andcve_ids. Asqav does not fetch, compute, verify, or vouch for these values.
Inside risk_snapshot, every numeric is a string on the wire: floats are rejected, so Asqav never holds a number it could be accused of computing. snapshot_source is required whenever any numeric signal is populated, which structurally pins the label that this is a snapshot and not a score.
A risk-acceptance receipt
{
"payload": {
"v": 1,
"type": "protectmcp:lifecycle:risk_acceptance",
"issued_at": "2026-06-01T19:26:44.289388Z",
"issuer_id": "org_acme_00001",
"action_ref": "sha256:82b51ff0063d5f0569eba20c2fce749eb8ddb601e7f1adaf67636c0fbff74b78",
"payload_digest": { "hash": "82b51ff0063d5f0569eba20c2fce749eb8ddb601e7f1adaf67636c0fbff74b78", "size": 512 },
"policy_digest": "sha256:3ac0b2df9f295d0f61d4ddf5d8cadafc77d0d141dc1841ae0504f7be39af6ec4",
"previousReceiptHash": "0000000000000000000000000000000000000000000000000000000000000000",
"decision": "observation",
"approver_id": "approver:alice@example.com",
"initiator_id": "initiator:bob@example.com",
"acceptance_reason": "Compensating control in place; risk accepted for 90 days pending patch.",
"accepted_at": "2026-06-01T19:05:00Z",
"expires_at": "2026-08-30T19:26:44.289388Z",
"supersedes": "act_prior_risk_acceptance_0001",
"sarif_digest": "sha256:9cbaf8102f5eb904e89b9ab2a505091188364805a4b65c23075e5f3a5af13faa",
"finding_ref": "github.com/acme/repo/security/code-scanning/42",
"approval_ref": "JIRA-SEC-9001",
"risk_snapshot": {
"snapshot_at": "2026-06-01T19:00:00Z",
"snapshot_source": "CISA KEV catalog + FIRST EPSS feed",
"epss": "0.94210",
"cvss": "9.8",
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"kev_listed": true,
"cve_ids": ["CVE-2026-1234", "CVE-2026-5678"]
}
},
"signature": { "alg": "ML-DSA-65", "kid": "org_acme_00001", "sig": "BASE64..." },
"anchors": [
{ "type": "opentimestamps", "value": "BASE64-OTS", "status": "pending" }
]
}
The decision is observation, never allow. A risk-acceptance receipt records an acceptance, not a policy outcome, so it asserts no policy evaluation. The controls_evaluated block that Asqav stamps on this sign carries no policy key, which is the truthful statement for a receipt that evaluated no policy. See Control Attestation for how that omission-over-false rule reads to a verifier.
How to sign one
Sign with compliance_mode: true, receipt_type: protectmcp:lifecycle:risk_acceptance, and policy_decision: none. The acceptance fields only project into the signed bytes on the compliance-mode signing path. The Asqav SDK defaults compliance_mode to true, so SDK callers get them by construction.
Set the validity window with valid_seconds. The server writes expires_at into the signed payload as signed_at plus valid_seconds, falling back to a fixed default of 86400 seconds (24 hours, overridable per request via valid_seconds_default) when unset. expires_at itself is not a request field.
curl -X POST https://api.asqav.com/api/v1/agents/agt_example/sign \
-H "X-API-Key: $ASQAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action_type": "risk:accept",
"hash": "sha256:82b51ff0063d5f0569eba20c2fce749eb8ddb601e7f1adaf67636c0fbff74b78",
"hash_algo": "sha256",
"payload_size": 512,
"compliance_mode": true,
"receipt_type": "protectmcp:lifecycle:risk_acceptance",
"policy_decision": "none",
"approver_id": "approver:alice@example.com",
"initiator_id": "initiator:bob@example.com",
"acceptance_reason": "Compensating control in place; risk accepted for 90 days pending patch.",
"accepted_at": "2026-06-01T19:05:00Z",
"valid_seconds": 7776000,
"sarif_digest": "sha256:9cbaf8102f5eb904e89b9ab2a505091188364805a4b65c23075e5f3a5af13faa",
"risk_snapshot": {
"snapshot_at": "2026-06-01T19:00:00Z",
"snapshot_source": "CISA KEV catalog + FIRST EPSS feed",
"epss": "0.94210",
"cvss": "9.8",
"kev_listed": true,
"cve_ids": ["CVE-2026-1234"]
}
}'
The validators on the signing path keep the record honest:
risk_acceptance_missing_required_field: a risk-acceptance receipt requiresapprover_idandacceptance_reason.risk_acceptance_requires_no_policy_decision: a risk-acceptance receipt records no policy evaluation. Sign it withpolicy_decision: none.risk_acceptance_fields_require_risk_acceptance_receipt: the acceptance fields are only valid onreceipt_type: protectmcp:lifecycle:risk_acceptance.sarif_digest_not_sha256_wire_form:sarif_digestmust look likesha256:<64 lowercase hex>.risk_snapshot_numeric_requires_snapshot_source: a populatedrisk_snapshotsignal requiressnapshot_sourceso the value is never read as an Asqav-derived score.
How to verify one
A verifier can check four independent things offline, with no access to Asqav:
- Recompute the agent's post-quantum signature over the canonical payload bytes against the
kidnamed insignature. A valid signature proves the receipt is exactly as the key holder signed it. - Recompute SHA-256 over the immediately preceding receipt's signed payload and compare it to
previousReceiptHash. A mismatch is a tamper signal. This confirms adjacency to the predecessor. The receipt's absolute position is established by re-deriving the chain from its seed, which the audit-pack export does over the full set of prior receipts. - Resolve each entry in
anchors[]against its timestamp authority to prove the payload existed no later than the anchored time T. - Recompute SHA-256 over the retained SARIF bytes and compare to
sarif_digest. A mismatch proves the scan artifact changed after the acceptance was signed.
Every step runs without Asqav in the loop, which is the third-party-notary posture: the decision is independently verifiable by anyone who holds the receipt and the referenced artifacts. See Third-Party Notary for why the unaffiliated-verifier property is the load-bearing one.
Which acceptance is in force: the supersedes walk
A finding gets re-accepted, narrowed, or extended over time, and each of those decisions is its own immutable receipt. Nothing in the chain is ever edited, so a verifier decides which acceptance is in force by walking the supersedes pointers:
- A receipt is current only if no verified receipt names it in
supersedes. - When several verified receipts target the same finding, the latest unsuperseded one wins.
expires_atis evaluated only on the current receipt. An expired receipt that something else already superseded is history, not a live exception.
Every pointer in the walk must come from a receipt that itself verifies. A superseding claim inside an unverified blob displaces nothing.
The expiry loop: Asqav answers, CI enforces
Asqav records the expiry and never enforces it, so the enforcement loop belongs in CI:
- When a finding is accepted, the suppression entry in the scanner's baseline carries the receipt id.
- A scheduled CI job walks the suppression entries, loads each referenced receipt, runs the supersedes walk, and checks
expires_aton the current receipt. - The job fails the build when a referenced receipt is expired, superseded with no current replacement, or missing.
- Removing the failed suppression lets the scanner re-flag the finding on its next run, which forces a fresh decision and a fresh receipt.
Asqav answers the question "is this acceptance still in force". The build answers "then you may not ship". Keeping the two roles apart is the point: the record stays honest because the recorder has no stake in the gate.
Honest limits: signed garbage is still garbage
The receipt proves the values were asserted at a specific time, under a named key, in a tamper-evident chain. It does not make them true, and it does not make them enforced. An approver_id read from an environment variable anyone on the build can set, or an epss value typed by hand, signs just as cleanly as a real one. Signing a forgeable source notarizes the forgery. Fix the source first (take identities from your SSO or CI identity token, take snapshot values straight from the feed response), then sign what came out.
Regulatory framing: evidence, not enforcement
A risk-acceptance receipt is evidence for an audit, not an enforcement control. It maps to records-and-audit-trail obligations without claiming to satisfy them on its own:
- EU AI Act Article 12 requires automatic recording of events over a system's lifetime. A signed, chained, anchored acceptance is a tamper-evident event record an auditor can verify offline.
- DORA Article 17 sets an ICT-related incident management and logging process. A risk-acceptance receipt gives the logging side a record that proves who accepted what, when, and against which scan artifact, in a form an independent reviewer can check.
In both cases Asqav supplies the evidence layer. The receipt proves the decision happened and held its place in the chain. Whether the acceptance was wise, the score correct, or the control sufficient stays the deploying organization's judgment.