Skip to main content
[AVAILABLE]. packages/runtime/src/RefusalRecordBuilder.ts, packages/crypto/src/RefusalCrypto.ts.

The gap this closes

An approved execution has always left cryptographic evidence behind: the Execution Trust Record and its signature (see Execution Trust Records). A refused one used to leave only an HTTP response and whatever the caller’s own logs happened to capture, nothing durable, nothing independently checkable. A Refusal Record is the REJECT-path counterpart to an Execution Trust Record: durable, signed, and third-party-verifiable the same way, without requiring a caller credential or database access to check it. Scope is deliberately narrow. A Refusal Record is produced only for a policy decision that RuntimeEngine.execute actually reaches: an ordinary PolicyEngine.evaluate REJECT, or a SignalIntentBinder binding-violation REJECT (see Policies and the decision). Caller-authentication failures and webhook signature failures are a separate, unsigned audit-sink capability by design, treat any claim about Refusal Records as “a policy decided to reject,” not “every rejection of any kind.”

What’s in the record

RefusalRecordBuilder (packages/runtime/src/RefusalRecordBuilder.ts) builds the record from the rejected transaction, its Decision, any binding violations, and the hash of the policy content that decided it (policyContentHash), the same value an Execution Trust Record carries in transaction.policy.contentHash, so a refusal and an approval under the same policy name the same policy content. Refusal Records written before 2026-10-06 have no policyContentHash and still verify. RefusalCrypto (packages/crypto/src/RefusalCrypto.ts) hashes and signs it with the same signer and key every Execution Trust Record uses (SignerBootstrap: a local key, or AWS KMS in production), one root of trust, not a separate one for refusals. Verify it with the key from GET /keys/default. At most one RefusalRecord exists per businessTransactionId: unlike an Execution Trust Record, a refusal is a single terminal event, not an append-only aggregate.

Two routes, two different trust models

  • POST /refusal/verify, verifies a submitted Refusal Record’s signature, returns { valid }. Deliberately mounted ahead of caller-auth middleware (packages/api/src/app.ts): no API key, no lookup by ID, nothing but the artifact itself and Parmana’s public key. This is what makes a refusal independently checkable by whoever actually received the rejection, not only by Parmana.
  • GET /refusal/:businessTransactionId, looks up a Refusal Record from Parmana’s own storage by transaction ID. Unlike the route above, this one stays behind caller-auth and ownership scoping (isOwnedByCaller), identically to /verify, /verification, and /trust-records, the underlying transaction content (signals, intent parameters) may be sensitive, even though the signature-verification capability above is intentionally open.

Scope caveat: writing fails open

Refusal Record writing fails open, deliberately, not fail closed like caller audit writes. RuntimeEngine.writeRefusalRecord runs after the Decision is built and may not delay or change the rejection that follows. A write failure is logged (refusal_record_write_failed) and swallowed, never thrown. The rejection itself is unaffected: a request that should be denied is denied, whether or not its record lands. What can be missing is the durable proof of why, not the refusal. Making the write fail closed was considered and rejected: it would turn a correct policy rejection into an opaque 500 on a storage hiccup, for no security gain, since the request was already denied when the Decision was built. Test: packages/runtime/tests/unit/refusal-record-fail-open.test.ts.
Which caller audit events are signed is covered on Caller Audit Trail.
  • Caller Audit Trail closes the analogous gap for caller-authentication events, signed, durable, and, per caller, chained. POST /audit/verify mirrors this page’s POST /refusal/verify exactly: mounted ahead of caller-auth, structurally checks and verifies any signed audit event’s signature. See Error catalog for both routes’ error shapes.
  • Execution Trust Records, the APPROVE-path counterpart.
  • Policies and the decision, how a Decision reaches REJECT in the first place, including SignalIntentBinder binding violations.
  • Security and Limitations for the wider threat model this evidence trail supports.