Status: NORMATIVE
Version: 0.11.2
Design Decisions: DD-124 (type placement), DD-127 (transport size limits), DD-129 (immutability), DD-131 (ASI-04 defense), DD-135 (receipt_url locator hint), DD-141 (schema validation-only)
This document defines the Evidence Carrier Contract: the universal interface that lets any protocol carry PEAC receipts without kernel changes. The carrier is a protocol-neutral envelope that wraps a content-addressed receipt reference with optional verification metadata.
Key words: The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals, as shown here.
The Evidence Carrier Contract covers:
- The
PeacEvidenceCarriertype: the carrier envelope itself - The
CarrierAdapter<TInput, TOutput>interface: how protocol mappings produce and consume carriers CarrierMeta: transport-level metadata for constraint validationcomputeReceiptRef(): canonical receipt reference computationvalidateCarrierConstraints(): transport-aware structural validationverifyReceiptRefConsistency(): tamper detection for attached carriers- Per-transport placement rules: how carriers map to MCP, A2A, ACP (Agentic Commerce Protocol), UCP, x402, and HTTP
The contract does NOT define:
- Wire format changes (the underlying
peac-receipt/0.1format is FROZEN) - Receipt signing, verification, or issuance (see
@peac/crypto,@peac/protocol) - Transport negotiation or capability discovery (see DISCOVERY-PROFILE.md)
The carrier contract belongs to the same family of signed attestation envelopes as Entity Attestation Tokens (EAT, RFC 9711, Oct 2025). Both use signed claim sets transported across protocol boundaries. The carrier wraps a content-addressed receipt reference for protocol-specific transport without altering the underlying attestation model. The specific content-addressing mechanism (receipt_ref = SHA-256 of the compact JWS) is PEAC-specific and is not derived from EAT's nonce or freshness primitives.
- Wire format:
peac-receipt/0.1(FROZEN) - Types:
packages/kernel/src/carrier.ts - Schemas and helpers:
packages/schema/src/carrier.ts - Conformance fixtures:
specs/conformance/fixtures/carrier/ - Kernel constraints:
docs/specs/KERNEL-CONSTRAINTS.md
The PeacEvidenceCarrier is the canonical carrier envelope. All protocol-specific adapters produce and consume this type.
| Field | Type | Required | Description |
|---|---|---|---|
receipt_ref |
sha256:<hex64> |
MUST | Content-addressed receipt reference: SHA-256 of the compact JWS bytes |
receipt_jws |
string (compact JWS) | SHOULD (embed) | The signed receipt in compact JWS format (header.payload.signature) |
receipt_url |
string (HTTPS URL) | MAY | Locator hint for detached receipt resolution (DD-135, v0.11.2+) |
policy_binding |
string | MAY | Policy binding hash for verification |
actor_binding |
string | MAY | Actor binding identifier |
request_nonce |
string | MAY | Request nonce for replay protection |
verification_report_ref |
string | MAY | Reference to a verification report |
use_policy_ref |
string | MAY | Reference to a use policy document |
representation_ref |
string | MAY | Reference to a content representation |
attestation_ref |
string | MAY | Reference to an attestation |
receipt_refMUST match the patternsha256:[a-f0-9]{64}(lowercase hex, exactly 64 characters after the prefix).receipt_jwsMUST be a valid compact JWS (three base64url-encoded segments separated by periods).- If
receipt_jwsis present,receipt_refMUST equalsha256(receipt_jws)where the hash is computed over the UTF-8 bytes of the compact JWS string (DD-129). - All optional string fields MUST NOT exceed
KERNEL_CONSTRAINTS.MAX_STRING_LENGTH(8192 bytes). - The total serialized carrier MUST NOT exceed the transport-specific size limit (DD-127).
| Format | Description | receipt_jws |
|---|---|---|
embed |
Full receipt inline in carrier | SHOULD be present |
reference |
Receipt available via external resolution | MUST be absent; resolve via receipt_ref |
When the carrier format is embed, the receipt_jws field SHOULD be present so recipients can verify the receipt without a network round-trip. When the format is reference, the receipt_jws field MUST be absent; consumers resolve the receipt via receipt_ref through a trusted registry or the issuer's well-known endpoint.
The receipt_url field is an optional locator hint that points to a detached receipt JWS. It enables scenarios where the full receipt is too large for inline transport or where a publisher prefers to serve receipts from a dedicated endpoint.
Constraints:
receipt_urlMUST use the HTTPS scheme. Non-HTTPS URLs MUST be rejected at schema validation.receipt_urlMUST NOT exceed 2048 characters.receipt_urlMUST NOT contain credentials (userinfo component).receipt_urlis a locator hint only. Implementations MUST NOT trigger implicit fetch when areceipt_urlis present (DD-55, no-implicit-fetch invariant).- Resolution is always opt-in: callers who choose to fetch MUST use an SSRF-hardened HTTP client.
Post-fetch invariant:
If a caller resolves receipt_url and obtains a JWS string, it MUST verify:
sha256(fetched_jws) == carrier.receipt_ref
This check ensures the fetched content matches the content-addressed reference. Failure to verify this invariant means the fetched JWS may not correspond to the carrier and MUST be discarded.
Resolution helper:
@peac/net-node (Layer 4) provides resolveReceiptUrl() as an opt-in, SSRF-hardened fetch helper. It rejects private IPs, enforces HTTPS, and applies timeout and size limits. The resolution helper does NOT perform the receipt_ref consistency check; that is the caller's responsibility.
@peac/schema (Layer 1) does NOT provide any resolution or fetch helper (DD-141: schema layer is validation-only, no I/O).
Transport surface:
| Transport | receipt_url surface |
|---|---|
| MCP | _meta["org.peacprotocol/receipt_url"] |
| A2A | metadata[extensionURI].carriers[].receipt_url |
| ACP | PEAC-Receipt-URL HTTP header |
| UCP | Passed through in carrier object |
| x402 | PEAC-Receipt-URL HTTP header |
| HTTP | PEAC-Receipt-URL HTTP header |
Protocol-specific mapping packages implement CarrierAdapter<TInput, TOutput> to bridge between the carrier envelope and the protocol's native message format.
interface CarrierAdapter<TInput, TOutput> {
extract(input: TInput): { receipts: PeacEvidenceCarrier[]; meta: CarrierMeta } | null;
attach(output: TOutput, carriers: PeacEvidenceCarrier[], meta?: CarrierMeta): TOutput;
validateConstraints(carrier: PeacEvidenceCarrier, meta: CarrierMeta): CarrierValidationResult;
}The extract() method reads carrier data from a protocol-specific message and returns structured PeacEvidenceCarrier objects. It MUST:
- Validate the carrier structure against
PeacEvidenceCarrierSchemabefore returning - Return
nullif no carrier data is present in the input - Return a
carriersarray (even for single-carrier transports) and ametadescribing the transport
Important: extract() on CarrierAdapter is synchronous and performs structural validation only (schema checks, size checks). Mapping packages MUST also expose an extractAsync() wrapper that runs verifyReceiptRefConsistency() when receipt_jws is present (DD-129). The async consistency check is performed at the mapping layer, keeping kernel types synchronous.
The attach() method places carrier data into a protocol-specific output message. It MUST:
- Accept a
carriersarray uniformly (even for single-carrier transports) - Call
validateCarrierConstraints()before placing the carrier - Reject carriers that exceed transport size limits
- Use
computeReceiptRef()from@peac/schemaifreceipt_jwsis provided butreceipt_refis missing
The validateConstraints() method checks a carrier against transport-specific constraints using the provided CarrierMeta. Implementations SHOULD delegate to the canonical validateCarrierConstraints() function from @peac/schema.
Transport-level metadata describing how a carrier is placed. Used by validateConstraints() to enforce transport-specific size limits and format requirements.
| Field | Type | Required | Description |
|---|---|---|---|
transport |
string | MUST | Transport identifier: 'mcp', 'a2a', 'acp', 'ucp', 'x402', 'http' |
format |
'embed' | 'reference' |
MUST | Carrier format |
max_size |
number (bytes) | MUST | Maximum carrier size for this transport |
redaction |
string[] |
MAY | List of field names that have been redacted |
Computes the content-addressed receipt reference from a compact JWS string. This is the single source of truth for receipt reference computation; all carrier adapters MUST use this function rather than computing SHA-256 locally.
Algorithm:
Input: jws (string, compact JWS format)
Output: receipt_ref (string, "sha256:<hex64>")
1. Assert crypto.subtle is available (WebCrypto runtime guard)
2. Encode jws as UTF-8 bytes
3. Compute SHA-256 digest of the bytes
4. Encode digest as lowercase hex
5. Return "sha256:" + hex
Runtime portability: Requires WebCrypto (crypto.subtle). Supported runtimes for published packages: Node >= 20, Cloudflare Workers, Deno, Bun. Missing crypto.subtle is a hard error with a diagnostic message identifying supported runtimes. Note: the monorepo development baseline is Node >= 22 (see .node-version); the Node >= 20 floor applies to consumers of published @peac/* packages.
Validates a carrier against transport-specific constraints. This is the canonical validator that all CarrierAdapter.validateConstraints() implementations delegate to.
Checks performed:
receipt_refformat: MUST matchsha256:[a-f0-9]{64}receipt_jwsformat (if present): MUST be a valid compact JWS- Total serialized size: MUST NOT exceed
meta.max_size - String field lengths: all optional string fields MUST NOT exceed
MAX_STRING_LENGTH
Returns a CarrierValidationResult with valid: boolean and violations: string[].
Verifies that receipt_ref matches sha256(receipt_jws) when both are present (DD-129). This async check prevents carrier tampering after attachment.
Algorithm:
Input: carrier (PeacEvidenceCarrier)
Output: null (consistent) | error string (inconsistent)
1. If receipt_jws is absent, return null (nothing to verify)
2. Compute expected = computeReceiptRef(receipt_jws)
3. If expected != carrier.receipt_ref, return error
4. Return null
Each transport has a maximum carrier size. These limits are defined in CARRIER_TRANSPORT_LIMITS:
| Transport | Max Size | Default Format | Rationale |
|---|---|---|---|
MCP (_meta) |
64 KB | embed | JSON in memory |
A2A (metadata) |
64 KB | embed | Metadata map |
| ACP (headers) | 8 KB | embed | PEAC-Receipt header (compact JWS) |
| UCP (webhook) | 64 KB | embed | Webhook body |
| x402 (headers) | 8 KB | embed | PEAC-Receipt header (compact JWS) |
| HTTP (headers only) | 8 KB | embed | Generic header transport |
| gRPC (metadata) | 8 KB | embed | HTTP/2 header budget (conservative default) |
Carriers are placed in the _meta object of JSON-RPC responses using reverse-DNS keys:
{
"_meta": {
"org.peacprotocol/receipt_ref": "sha256:abc123...",
"org.peacprotocol/receipt_jws": "eyJhbGciOi..."
}
}The org.peacprotocol/ prefix is NOT reserved under MCP 2025-11-25 rules because the second label is peacprotocol (not modelcontextprotocol or mcp).
Legacy compatibility (DD-125): Two legacy formats are supported for extraction:
_meta["org.peacprotocol/receipt"](v0.10.13): a single_metakey containing the JWS string without a separatereceipt_ref. When found,extractReceiptFromMetaAsync()computesreceipt_reffrom the JWS and returns a properPeacEvidenceCarrier.- Top-level
peac_receipt(pre-v0.10.13): a top-level field on the MCP tool response. Read by the legacyextractReceipt()function.
New attachReceiptToMeta() defaults to the v0.11.1 _meta carrier format with both receipt_ref and receipt_jws keys.
Carriers are placed in the metadata map of A2A messages using the PEAC extension URI as the key:
{
"metadata": {
"https://www.peacprotocol.org/ext/traceability/v1": {
"carriers": [
{
"receipt_ref": "sha256:abc123...",
"receipt_jws": "eyJhbGciOi..."
}
]
}
}
}The extension URI (https://www.peacprotocol.org/ext/traceability/v1) is registered in the A2A Agent Card's capabilities.extensions[] array per A2A spec v0.3.0.
Carriers are attached via the PEAC-Receipt HTTP header, which carries a compact JWS. The header surface enforces an 8 KB size limit. The receipt_jws field is required; carriers without a JWS are rejected at attach() time.
Carriers are placed in the peac_evidence field of webhook payloads. Backward compatibility with the extensions["org.peacprotocol/interaction@0.1"] key is maintained.
Carriers are attached via the PEAC-Receipt HTTP header on x402 offer (HTTP 402) and settlement (HTTP 200) responses. The header carries a compact JWS and enforces an 8 KB size limit. The receipt_jws field is required; carriers without a JWS are rejected at attach() time.
Carriers are attached via gRPC metadata keys:
peac-receipt: compact JWS of the signed receiptpeac-receipt-type: receipt typ value (default:interaction-record+jwt)
gRPC metadata rides in HTTP/2 headers. The default maximum carrier size is 8 KB (conservative interoperability-safe default). Environments with known larger server limits can override via createGrpcCarrierMeta({ max_size: ... }).
Binary metadata (keys with -bin suffix) is rejected for PEAC receipt data. The A2AGrpcCarrierAdapter computes a real SHA-256 receipt_ref from the JWS bytes at extraction time using node:crypto.
For receipts exceeding the metadata budget, prefer reference mode (receipt_url) instead of embedding the full JWS.
For generic HTTP transport where only headers are available:
PEAC-Receiptheader: MUST contain a compact JWS (never a barereceipt_ref)- Reference-only transport (resolving via
receipt_refwithout an inline JWS) is deferred to a future version pending standardization of a retrieval mechanism
The wire token is exactly PEAC-Receipt (mixed-case, hyphenated). This is the only valid spelling in conformance fixtures and attach() output. Alternative casings are non-conformant; implementations MUST emit the canonical spelling exactly.
HTTP header lookups in code SHOULD be case-insensitive per RFC 9110, but conformance fixtures and attach() output MUST use PEAC-Receipt exactly.
The PEAC_RECEIPT_HEADER constant in @peac/kernel provides the canonical spelling.
The PEAC-Receipt header MUST always carry a compact JWS, never a bare receipt_ref or a JSON carrier object. Reference-only transport (without an inline JWS) is not supported in v0.11.1.
Every extract() implementation MUST validate carrier structure before returning. This prevents poisoned extension data in _meta, metadata, or other protocol-specific containers from propagating as valid carriers. This aligns with OWASP ASI-04 (Supply Chain) defense.
Receipt reference integrity is enforced at two levels:
- Producers MUST compute
receipt_refviacomputeReceiptRef(receipt_jws)before callingattach(). Sinceattach()is synchronous, it does not re-verify the hash; correct computation is the producer's responsibility. - Consumers MUST verify receipt_ref consistency via
verifyReceiptRefConsistency()in theirextractAsync()path. Tampered carriers (wherereceipt_refdoes not matchsha256(receipt_jws)) MUST be rejected with a validation error.
The sync extract() on CarrierAdapter performs structural validation only (schema checks, size checks). The async extractAsync() wrapper at the mapping layer adds the DD-129 consistency check when receipt_jws is present.
Carriers exceeding transport size limits MUST be rejected at attach() time. This prevents denial-of-service via oversized carriers that could overwhelm protocol-specific containers (MCP _meta memory, HTTP header buffers, A2A metadata maps).
Carrier fields MUST NOT contain raw user prompts, conversation context, or other sensitive content. The carrier envelope contains only receipt references, cryptographic bindings, and protocol metadata.
An implementation is conformant if it:
- Implements
CarrierAdapter<TInput, TOutput>with correctextract(),attach(), andvalidateConstraints()methods - Uses
computeReceiptRef()from@peac/schemafor all receipt reference computation - Validates carrier structure at extraction time (DD-131)
- Provides an
extractAsync()wrapper that runsverifyReceiptRefConsistency()whenreceipt_jwsis present (DD-129) - Enforces transport-specific size limits per DD-127
- Uses
PEAC-Receiptas the canonical header spelling (DD-127) - Passes all conformance fixtures in
specs/conformance/fixtures/carrier/
- v0.11.1: Initial specification (DD-124, DD-127, DD-129, DD-131)