ERC-8410 - Portable Execution Plan Artifact

Created 2026-09-04
Status Draft
Category ERC
Type Standards Track
Authors
Requires

Abstract

This proposal defines the execution plan: a self-contained JSON document that describes an ordered sequence of calls one sender is asked to make on one chain, together with a keccak256 plan digest over exactly the fields that determine what gets broadcast.

A plan is produced by software that holds no keys — a routing service, a protocol front end, a scheduled job, an AI agent's tool server — and is consumed by a wallet that holds the key and decides, on its own, whether to sign. The document is transport-neutral: it can be returned by an HTTP endpoint, embedded in a data: URI, stored in a queue, or carried across a process boundary, without changing meaning or digest.

The digest is the artifact's contribution to safety. Because it covers the broadcast-determining fields and nothing else, a human approval, a policy decision, and a later audit record can all name the same bytes, and a preparer cannot alter what executes after the moment of approval without producing a different digest.

The proposal also defines the artifact reference: a small envelope that names a stored plan by URL and keccak256 digest instead of carrying it, so that a relaying party — typically an AI agent between the producer and the wallet — holds authority over which plan is used and none over what it says. Because a wallet that fetches a caller-supplied URL can be aimed at things, the network admission rules for that fetch are part of the specification.

A second document type, the typed-data signature request, carries a concrete EIP-712 message for the wallet to sign. It uses the same reference envelope and has its own request digest. It is separate from the execution-plan object and supports flows that end with an offchain signature rather than a wallet-broadcast transaction.

Motivation

Wallets increasingly execute calls they did not construct. The construction is done by a swap router, a lending front end, a yield optimizer, or an autonomous agent, and the wallet's job is reduced to deciding whether to sign. That split is now the normal shape of an onchain interaction, but the object that crosses the split has no agreed form.

Today it crosses as an ad-hoc JSON blob whose fields are whatever the producer's SDK happened to emit, or as an in-page JSON-RPC call that exists only for the lifetime of a browser tab. Both work. Neither gives the wallet anything to hold onto:

This proposal fixes the shape of that object, fixes what it means to be the same object, and fixes how it may be handed across an untrusted relay. How a wallet decides whether to sign, and what interface a producer is called through, are deliberately out of scope.

The relay deserves its own word. Plans are increasingly assembled by one party, relayed by a second, and executed by a third, and the middle party is often the least trustworthy link — a browser extension, an automation runner, or an agent acting on text from an untrusted source. Handing it the full bytes and asking it to forward them faithfully means it can rewrite them, can corrupt them by re-serializing rather than copying (a failure mode disproportionately common when the relay is a language model), and destroys any record of where they came from. Content addressing solves all three, and wallets have no agreed way to express it.

Specification

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 RFC 2119 and RFC 8174.

Machine-readable schemas are provided for the execution plan, the typed-data signature request, and the artifact reference.

Quantity encoding

A quantity is a JSON string holding a canonical unsigned decimal integer: the single character 0, or an ASCII digit string with no leading zero. A quantity MUST fit in a uint256. Decimal strings are used rather than JSON numbers because IEEE-754 doubles cannot represent the full uint256 range, and rather than hexadecimal because these values are read by humans on approval screens.

A byte string is a JSON string of the form 0x followed by an even number of hexadecimal digits. An address is a byte string of exactly 20 bytes. Producers MUST emit both in lowercase. Consumers MUST normalize both to lowercase before computing the digest, and MAY additionally reject input that is not already lowercase; the mixed-case checksum form is a display convention, not part of the artifact's identity.

The plan document

Field Type Required Description
schema_version string yes MUST be "1" for this revision
chain_id quantity yes EIP-155 chain identifier the plan executes on
caip2_chain_id string yes MUST equal eip155: concatenated with chain_id
sender address yes Account expected to send every step
ordered_steps array yes 1 to 4096 steps, in execution order
required_capabilities array of string no Behaviors the consumer must implement
simulation_failure_policy object no What the producer advises on failure
extensions object no Producer-defined data the consumer ignores

A document containing a member not listed here MUST be rejected. Silently ignoring unknown members would let a producer place meaning where a consumer cannot see it, in a document whose whole purpose is that both sides agree on what it says.

Both chain_id and caip2_chain_id are present, and redundant, on purpose. The quantity is what the consumer computes with; the CAIP-2 identifier is what it matches against multi-ecosystem configuration and displays. Requiring both, and requiring that they agree, means a plan cannot carry a namespace and a number that disagree about which chain it is for.

Steps

Each entry of ordered_steps is an object:

Field Type Required Description
step number yes 1-indexed position; entry n MUST have step equal to n
kind string yes One of the step kinds below
transaction object yes The call to make
revert_decode object no ABI for rendering a revert from this step

transaction has members chain_id (quantity), from (address), to (address), data (byte string), value (quantity), and an OPTIONAL gas (quantity). to is REQUIRED: a step is a call to an existing account, and contract creation is not expressible in this format. Every step's chain_id MUST equal the plan's chain_id and every step's from MUST equal the plan's sender. A plan is for one account on one chain; a document that mixes them MUST be rejected rather than partially executed.

gas is a producer hint. A consumer MUST NOT broadcast a producer-supplied gas limit without its own estimation, because the value is chosen by a party that does not pay for it.

kind explains why a step is in the plan, and MUST be one of:

The value is presentation-affecting rather than semantic: it lets a consumer tell a reviewer why a step they did not ask for is present. Consumers MUST NOT derive authority from kind. It is a producer's claim, and a producer that mislabels a transfer as allowance_cleanup has changed only the caption, never the calldata that the digest covers and the reviewer sees.

revert_decode, when present, is an object with kind equal to error_result, an abi array holding a JSON ABI fragment declaring at least one error, and an OPTIONAL boolean required defaulting to false. It exists so a consumer can render a custom revert as its declared error rather than as an opaque byte string. required records whether the producer considers a successful decode necessary to present the failure meaningfully; it is advisory, and a consumer MAY ignore it. Consumers MUST treat the decoded result as display text only, and MUST bound the fragment they accept; the implementation this proposal is drawn from allows at most 128 entries and 65536 bytes.

Required capabilities

required_capabilities lists behaviors the plan needs from whatever executes it. A consumer MUST reject a plan naming any capability it does not implement, rather than executing the plan without that behavior. This is the inverse of the usual extension-negotiation default, and it is deliberate: the interesting capabilities are ones whose absence silently breaks a plan's safety, not ones whose absence merely degrades it.

This proposal defines one:

Consumers commonly satisfy atomic_batch through an account-abstraction batch executor, or through an EIP-7702 authorization delegating the sender to a batching implementation for the duration of the transaction.

A multi-step plan that does not list atomic_batch MAY be executed as separate transactions. A consumer doing so MUST execute steps in order, MUST NOT begin a step until the previous one has been mined successfully, and MUST stop at the first failure. A consumer that can execute atomically MAY do so regardless of whether the plan asks for it.

Capability names MUST be at most 64 printable ASCII characters, and a plan MUST NOT list more than 32. Further names are expected to be registered by later proposals.

Simulation failure policy

When present, simulation_failure_policy MUST contain all three of rpc_error, execution_reverted, and simulation_setup_error. Each is an object with an action and a human-readable instruction of 1 to 2000 characters.

action MUST be one of:

execution_reverted and simulation_setup_error MUST NOT specify retry_same_plan. Calldata that reverted against current state will revert again, and an automatic retry loop over unchanged bytes is a way to spend fees without making progress.

This block is advice from the party that knows why the plan is shaped the way it is; a stale quote wants reprepare_plan where a rate-limited endpoint wants retry_same_plan. Consumers MAY ignore it entirely, and MUST NOT treat it as authorization to broadcast anything.

Extensions

extensions is an object reserved for producer-defined data. Consumers MUST ignore its contents and MUST bound its serialized size; 65536 bytes is RECOMMENDED. It is excluded from the digest, so a producer MUST NOT place in it anything that changes what executes or what a reviewer needs to see.

Size limits

A consumer MUST enforce limits on any plan it accepts, and MUST reject rather than truncate. The following are RECOMMENDED: at most 4096 steps, at most 8 MiB of data summed across steps, and at most 16 MiB of serialized document.

Plan digest

The plan digest is the keccak256 hash of the UTF-8 serialization of a canonical JSON object built from the plan:

{
  "schema_version": "1",
  "chain_id": "1",
  "sender": "0x...",
  "ordered_steps": [
    {
      "step": 1,
      "kind": "approval",
      "transaction": {
        "chain_id": "1",
        "from": "0x...",
        "to": "0x...",
        "data": "0x...",
        "value": "0"
      }
    }
  ]
}

The serialization MUST place members in exactly the order shown, MUST contain no whitespace between tokens, and MUST encode addresses and data as lowercase byte strings. step MUST be serialized as a JSON number and every other scalar as a JSON string.

This member order is normative and is not lexicographic. A generic canonicalizer such as RFC 8785 sorts members by name and would order the outer object chain_id, ordered_steps, schema_version, sender, producing different bytes and a different digest. Implementations MUST emit the order given here rather than delegating to a general-purpose canonicalization routine.

The digest covers schema_version, chain_id, sender, and each step's step, kind, and the five broadcast-determining transaction members. It excludes caip2_chain_id (derivable from chain_id), gas, revert_decode, simulation_failure_policy, required_capabilities, and extensions.

The exclusions define what the digest is for. It is the identity of the bytes that will hit the chain and the labels shown beside them, so that two documents share a digest exactly when approving one is the same act as approving the other. A producer that re-serializes a plan with a different gas hint has not changed what the user agreed to. A producer that changes a single calldata byte has.

Consumers SHOULD display the digest wherever they display a plan for approval, SHOULD record it against the resulting approval, and SHOULD record it against the broadcast transaction hash.

The digest is not a signature and carries no authenticity claim. It answers "are these the same bytes", never "did anyone vouch for them".

Typed-data signature requests

A typed_data_signature_request is a separate document carrying one concrete EIP-712 message and the account expected to sign it. The typed-data signature request specification is normative for this document type. It defines the request and result shapes, the request digest, and optional delivery of the signature to the producer.

Supporting signature requests is OPTIONAL. A consumer that supports execution plans only MUST reject signature requests. A signature request MUST NOT be encoded as an execution-plan step or placed in its ignored extensions field. The execution-plan document and plan digest are unchanged.

Producers MAY return several signature requests, receive their results, and then return another concrete signature request or an execution plan. Each new document requires its own validation and authorization. No execution plan is required when the workflow ends with an offchain signature and producer relay.

Delivery by reference

A plan or typed-data signature request MAY be handed to a consumer inline, or by an artifact reference: an envelope that names the stored document and lets the consumer fetch and verify it itself.

Field Type Required Description
kind string yes MUST be "artifact_reference"
artifact_type string yes "execution_plan" or "typed_data_signature_request"
url string yes https URL, data: URI, or file URI locating the bytes
integrity object conditional Digest over the exact stored bytes
bytes number conditional Exact byte length of the stored bytes
instruction string no Human-readable note from the producer

integrity and bytes are REQUIRED when url is an https or file URL, and OPTIONAL when it is a data: URI; in a data: URI the bytes are the reference. When either is supplied for a data: URI it MUST still be checked. Consumers MUST ignore unrecognized top-level members of the envelope, so producers can enrich it without stranding deployed wallets.

integrity is an object with algorithm, which MUST be "keccak256" for this revision, and value, 0x followed by 64 hexadecimal digits. Unlike the envelope, integrity is strict: a consumer MUST reject an integrity object carrying any other member. An integrity block containing something the consumer does not understand is one it cannot claim to have verified.

artifact_type MUST be exactly execution_plan or typed_data_signature_request. No other artifact types are defined or accepted by this revision. A consumer MUST reject a type it does not implement, and MUST reject a reference whose artifact_type differs from the one expected at the point of use. The discriminator is what stops an envelope obtained for one purpose from being redeemed for another, and it lets a consumer apply the right size bound before fetching anything.

In the retrieval rules below, the maximum document size and validation rules are those of the selected type. Fetching a signature request MUST NOT use the larger execution-plan size limit merely because the envelope is shared.

instruction is producer prose. It is display context and MUST NOT influence any authorization decision.

Retrieval

Given an envelope, a consumer MUST, in this order:

  1. Reject the envelope if its serialized length exceeds the consumer's limit, before parsing the URL.
  2. Reject the reference if bytes is present and exceeds the maximum plan size, before opening any connection or reading any file.
  3. Retrieve the bytes according to the transport rules below. A consumer that does not implement the file transport MUST reject a file envelope.
  4. Reject the result if bytes is present and the retrieved length differs.
  5. Reject the result if integrity is present and the digest of the retrieved bytes differs.
  6. Parse and validate the bytes exactly as it would a plan supplied inline.

Length is checked before the digest because it is the cheaper refutation, and both before parsing because a parser is a larger attack surface than a comparison. Where the transport applied a content encoding, the digest is over the decoded bytes.

Verification says nothing about whether the plan should be executed. A verified plan has been shown to be the plan the envelope named, and that is all; it earns no trust from having a URL.

The file transport

A file URI names bytes on the consumer's own filesystem — the cheap transport for a producer on the same machine, such as a local MCP server that writes the plan where the wallet can read it. The relay carries only the path, the digest, and the length, never the body, and the URL fully specifies where the bytes are with no out-of-band agreement.

A consumer fetching a file URL MUST enforce all of the following, and MUST fail rather than degrade when any cannot be met:

Consumers SHOULD additionally restrict file reads to one or more user-configured directories and treat anything outside them as failure. An unrestricted file transport lets any relay that can reach the wallet name any file the wallet can read; only the rules above stand between a guessed path and a read the user never intended.

The https transport

A consumer fetching an https URL MUST enforce all of the following, and MUST fail rather than degrade when any cannot be met:

Error messages MUST NOT include any part of the response body. A consumer that reports what a refused endpoint returned has become a way to read its own network from outside it.

The data: transport

A data: URI MUST have media type application/json, MAY use the ;base64 parameter, and MUST decode to at most the maximum plan size. Consumers MUST bound the encoded length before decoding and MUST NOT perform network access for a data: reference. Base64 packs three bytes into four characters and percent-encoding never contracts, so nothing longer than ⌊max ÷ 3⌋ × 4 + 4 characters can decode to an acceptable body.

Provenance

A consumer that reports where a plan came from MUST distinguish an https host, which is a fact the transport proved, from an inline data: URI, which has no host and no provenance beyond whoever handed over the envelope. A file path is likewise caller-chosen text, not a statement about who published anything, and MUST NOT be presented as provenance beyond the local machine. The transport-proved host is one of the few facts about a plan that is attested rather than asserted, and a wallet's authorization rules MAY key on it.

Relationship to wallet_sendCalls

A plan maps onto the parameters of EIP-5792 wallet_sendCalls and back, and a wallet or producer that speaks both MAY convert between them:

Plan wallet_sendCalls
chain_id (decimal) chainId (hexadecimal)
sender from
ordered_steps[n].transaction.to / data / value calls[n].to / data / value
atomic_batch in required_capabilities atomicRequired: true
kind, revert_decode, simulation_failure_policy, extensions no equivalent; dropped

Converting in the other direction, caip2_chain_id is derived, every step's kind is execution, and atomicRequired: true becomes atomic_batch. The plan digest is computed from the plan form, so a plan that round-trips through wallet_sendCalls keeps its digest exactly when its calls are unchanged.

Rationale

Why an artifact rather than an interface call

EIP-5792 also carries a batch of calls to a wallet, as wallet_sendCalls arguments, and this proposal borrows its capability negotiation. The difference is lifetime. An interface call exists between two live endpoints; an artifact is a value that can be stored, forwarded, queued for tomorrow, reviewed on a different device, and compared to what was broadcast a month later. The use cases that motivate this proposal — a plan prepared by a service and approved by a person somewhere else, a scheduled job whose output is audited after the fact — need the value, not the call.

The two compose, and the mapping above is normative so that they do. A wallet that speaks wallet_sendCalls can accept a plan by converting it, and a producer can serve a plan to an in-page wallet the same way. What this proposal adds is a digest that survives the conversion, so the identity of the intent outlives the connection that carried it, and a reference form that lets the intent cross an untrusted relay intact.

Why the digest excludes gas

Including gas would be the cautious choice, and it is the wrong one. Gas limits are re-estimated by the consumer against current state as a matter of course, and a digest that changes on re-estimation would break on every honest path, which teaches implementers to ignore digest mismatches. A digest that changes only when the executed bytes change is one whose mismatches mean something.

The economic risk from gas is real and is addressed where it belongs: the wallet bounds what it is willing to pay through its own policy, described in the wallet's own authorization rules, using its own estimates rather than the producer's hint.

Why capabilities fail closed

required_capabilities inverts the usual "ignore what you do not understand" rule. A plan that needs atomicity and does not get it does not fail loudly — it succeeds partway and leaves a live allowance behind. Fail-open extension negotiation is right for features whose absence degrades an experience and wrong for features whose absence silently removes a safety property, and the ones worth naming here are all of the second kind.

Why one sender and one chain

Allowing a plan to span accounts or chains would make the digest cover something no single signer can execute, and would make partial execution the normal case. Cross-chain and multi-account intent is real, and is better expressed as several plans with an outer structure that sequences them, where each plan remains a thing exactly one signer can accept or refuse in full.

Why a reference envelope rather than Subresource Integrity

Subresource Integrity is the same idea in the browser, and it is not reusable here. Its digests are SHA-2 where everything else a wallet handles — plan digests, transaction hashes, selectors — is keccak256, and mixing hash functions across one trust boundary invites verifying one and reporting the other. It has no type discriminator, so nothing prevents redeeming a reference in the wrong context. It has no inline form. And it is an attribute of an HTML element, not a value that can be relayed through the non-browser transports that are the whole use case.

Why redirects are refused rather than re-checked

Re-running admission on every hop is possible and buys less than it costs. Each hop is another chance for a check to be applied inconsistently, and the producer already knows where its own bytes are. Refusing redirects makes the URL in the envelope the URL that was fetched, which is also what makes the reported host meaningful.

Why integrity is strict when the envelope is not

The envelope is a message that will grow, and a consumer that rejected an enriched envelope would break on a producer's routine improvement. The integrity block is an assertion the consumer either verified in full or did not; there is no partial credit, so tolerating unknown members inside it would let a producer believe it had constrained something the consumer never checked.

Why decimal strings

The alternative, 0x-prefixed hexadecimal quantities as used in the JSON-RPC interface, is more idiomatic for Ethereum tooling. Decimal wins here because a plan is read by people during approval. Amounts, chain identifiers, and step counts appear on review screens, and 1000000000000000000 is checkable by a human where 0xde0b6b3a7640000 is not. Byte strings that are never read as numbers stay hexadecimal.

Backwards Compatibility

This proposal introduces a new document format and no changes to the chain, to existing interfaces, or to deployed contracts. Producers and consumers that do not implement it are unaffected.

Typed-data signature requests are a separate, explicitly named document type. They do not alter execution-plan v1 fields, step kinds, digest computation, or test vectors. Existing execution-plan references remain valid. Consumers that only support execution plans reject the new type; existing transaction signing permissions MUST NOT implicitly authorize typed-data signatures. The reference discriminator is now limited to these two types rather than arbitrary names.

The document composes with EIP-5792 as described in the Rationale, and with EIP-7702, which is one way for a consumer to satisfy atomic_batch.

schema_version is a string rather than a number so that future revisions may use qualified values. A consumer MUST reject a version it does not implement.

Test Cases

Typed-data signature request vectors and a fixture runner cover the new request digest, EIP-712 domain binding, delivery constraints, and structural validation. The runner also checks every original execution-plan vector below.

Vectors are provided in the digest test vectors. Each case gives a plan, the exact canonical byte string its digest is taken over, and the resulting digest.

Case Digest
A one-step plan with empty calldata 0x93aeec006e55dfe0f54041d53c94387e08c504d4f3b3826cd3426dbc7da38ea5
A two-step plan, whose step 2 carries a gas hint the canonical form omits 0x13c4b058741ee7bdfe5a51825c71bd205859b89436505a15adf60bd3f2281deb
The same two-step plan with different gas, extensions, required_capabilities, and a different failure instruction 0x13c4b058741ee7bdfe5a51825c71bd205859b89436505a15adf60bd3f2281deb
The same two-step plan with the final byte of step 2 calldata changed 0x5bf27f490fa44998b6386dfe1b0fae6df0c1b2a9604b69d83bbbbd7a6d285549

The third and fourth cases are the whole point of the digest. Everything the third case changes is invisible to what gets broadcast, so it shares a digest with the second; the one byte the fourth case changes is broadcast, so it does not.

The first case is cross-checked against two independent implementations of the canonical form — one in Rust, one in JavaScript — which agree on its digest. Implementers are encouraged to start there, because a canonicalization mistake produces a plausible-looking digest that is silently incompatible with everyone else's.

Reference Implementation

Ekubo Wallet, a desktop wallet at github.com/EkuboProtocol/wallet, implements the execution-plan format as its only route from a plan producer to a broadcast transaction. Its ExecutionPlan type in crates/ekubo-wallet-core/src/core/execution_plan.rs performs the validation described above, computes the digest, and rejects unknown members, over-long capability lists, oversized extension bags, and retry_same_plan where this proposal forbids it. The wallet displays the digest at approval and records it against both the approval and the broadcast hash. It satisfies atomic_batch by executing every multi-step plan as one EIP-7702 batch that reverts as a whole, whether or not the plan asks for it, and executes a single-step plan as a direct call. It accepts mixed-case addresses and normalizes them before digesting.

The reference envelope and its admission rules are implemented in crates/ekubo-wallet-core/src/plan_fetch.rs, which verifies byte count and digest before parsing, bounds outbound retrievals with a semaphore and per-type body caps, and surfaces the transport-proved host or the inline origin at approval time.

The Ekubo protocol's plan producer at mcp.ekubo.org emits this format for transfers, swaps, liquidity, and yield operations, and returns reference envelopes for the plans it stores; the relaying agent passes each envelope through verbatim.

Security Considerations

A plan is a request, never an authorization

Nothing about a well-formed plan implies it should be executed. A plan is attacker-controlled input in the general case: producers are untrusted by construction, and an agent relaying a plan may itself be acting on prompt-injected instructions. Consumers MUST validate every constraint in this proposal, MUST evaluate their own authorization rules against the resulting calls, and MUST NOT treat producer-supplied fields — kind, instruction, extensions, revert_decode output — as inputs to that decision.

What the digest does not do

The digest binds bytes to bytes. It does not attest that a producer is honest, that a plan does what its labels claim, or that anyone approved it. A consumer that shows a user a digest and a description has shown them a fingerprint of the calldata and an unverified caption; the description is the producer's word, and only the decoded calls are evidence.

Because the digest excludes gas, two plans sharing a digest may carry different gas hints. This is intended, and it is why consumers MUST re-estimate rather than broadcast the hint.

Simulation is not a guarantee

A plan that simulates cleanly may still revert or execute differently when mined, because state moved in between. Consumers SHOULD re-simulate immediately before signing rather than relying on an earlier result, and MUST NOT treat a stored simulation result as standing permission to broadcast.

Resource exhaustion

The step count, calldata, extension, and document limits are load-bearing. A producer that can hand a consumer an unbounded document can exhaust its memory or stall the process that parses it. Consumers MUST apply limits before parsing where the transport allows, and MUST reject oversized input rather than truncate it — a truncated plan that still parses is a plan whose digest no longer describes what a reviewer saw.

Revert decoding as an attack surface

revert_decode hands the consumer a producer-controlled ABI fragment and asks it to decode producer-influenced bytes. Consumers MUST bound the fragment, MUST run the decoder in a context where a malformed fragment cannot do more than fail, and MUST treat the output as untrusted text requiring escaping before display.

The reference digest binds bytes, not intent

A verified reference proves the retrieved bytes are the bytes the envelope named. It proves nothing about whether the producer is honest, whether the plan does what its instruction claims, or whether it should be executed. A consumer that treats verification as approval has replaced a review with a checksum.

A file reference makes the wallet read on the relay's behalf

Unlike the https transport, which is fenced off the user's network by the admission rules, a file reference aims the consumer at its own filesystem — inside its trust boundary, read with the wallet's own privileges, on behalf of a relay that may be remote. The digest check proves the bytes are the bytes the envelope named, but it does not make the read harmless:

Consumers that implement the file transport accept this posture deliberately, and the directory restriction above is what keeps a local-producer convenience from becoming a general file-reading primitive for any relay that can reach the wallet. Consumers that do not want it MUST reject file envelopes; a producer that needs every wallet to succeed hands the plan over https or inline instead.

Server-side request forgery is the primary transport risk

A consumer that fetches caller-supplied URLs is a request forwarder inside the user's trust boundary. Every admission rule — public unicast only, no redirects, no credentials, no non-default ports, no fragments — is load-bearing. Consumers MUST check the resolved address rather than the hostname alone, and SHOULD connect to the address they checked, because a name that resolves publicly at check time and privately at connect time defeats a hostname-only check. Suppressing response bodies in errors is part of the same control.

Denial of service through the fetch primitive

Every retrieval consumes a resolver query, a connection, a timeout window, and up to the plan size cap in memory. The concurrency bound, the timeouts, and the pre-fetch size refusal are all required, and belong to the retrieval primitive rather than to any one caller of it. A consumer that decodes a content encoding before hashing MUST bound the decoded stream as it decodes.

data: references are not safer for being local

A data: URI avoids the network, and that is its only advantage. Its bytes came from whoever handed over the envelope and carry no provenance. A consumer that shows a fetched plan's host and shows nothing for an inline one is reporting this correctly.

Stale references

A digest mismatch means the bytes changed or the reference is stale, and the two are indistinguishable from the consumer's side. Consumers MUST treat both as failure and MUST NOT fall back to the retrieved bytes. Producers SHOULD serve immutable bodies at each URL so that a mismatch is unambiguously an integrity failure.

Copyright

Copyright and related rights waived via CC0.