Repository navigation
Bind requestState to the principal and the request it was minted for #623
Description
RequestStateCodec seals the requestState of a multi round-trip request with an HMAC and a TTL.
That makes it tamper-proof, but not tied to anything: a valid state verifies on any request within its TTL.
A client can echo a state minted for one call on another tool, on the same tool with other arguments, or under another user's token, and the server hands its payload to the handler as if it belonged there.
The 2026-07-28 specification asks for more than integrity (Multi Round-Trip Requests, Server Requirements):
To prevent replay, servers SHOULD include the following inside the integrity-protected
requestStatepayload and verify each on receipt:
- the authenticated principal, rejecting state presented by a different principal.
- a short expiry (TTL), rejecting state presented after it lapses;
- an identifier for the originating request, e.g. the method name and a digest of its salient parameters, rejecting state presented on a request that does not match.
The SDK covers the TTL.
A handler can put the principal and request identity into the payload itself and compare them on the retry, but every handler has to remember to, and ElicitationReplay (the state the SDK mints for ClientGateway::elicit() on the modern era) has no way to do it at all.
Proposal
A binder the server configures once, whose value the SDK covers with the MAC of every state it mints for a request and recomputes when it verifies the echoed state:
RequestStateBinderInterface::bind(Request $request, ?AccessToken $accessToken): string, called once per modern-era request with the request as the client sent it and the token it was authorized with.- an optional
binderargument ofBuilder::setRequestState()to configure it. - A shipped
OriginatingRequestBinderimplementing what the specification recommends: the token's issuer, client and subject, the method, the tool or prompt name and a digest of its arguments, or the resource URI. - A mismatch fails exactly like a bad signature:
-32602"The supplied requestState failed verification." with HTTP 400. - Opt-in: without a binder, the wire format and verification stay as they are.
Precedents
- TypeScript SDK:
createRequestStateCodec({ key, bind })takes an optionalbind(ctx)callback whose value is HMAC'd into a tag in the envelope (requestStateCodec.ts). - Python SDK:
RequestStateSecuritybinds the principal (client, issuer, subject), the target (tool or prompt name, or resource URI) and a digest of the arguments, from a per-method allowlist (request_state.py, python-sdk#3032).
Context
Shopware (https://github.com/shopware/shopware) plans URL-mode elicitation on the 2026-07-28 era, for example to confirm an action out of band before a tool runs.
The requestState then carries what was confirmed, so it has to be bound to the user and the call that asked for it.
Activity