[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 thatRuntimeEngine.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
Which caller audit events are signed is covered on Caller Audit Trail.Related
- Caller Audit Trail closes the analogous gap for
caller-authentication events, signed, durable, and, per caller, chained.
POST /audit/verifymirrors this page’sPOST /refusal/verifyexactly: 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
Decisionreaches REJECT in the first place, includingSignalIntentBinderbinding violations. - Security and Limitations for the wider threat model this evidence trail supports.