Expand description
Stateless HMAC-signed cursors for paginated tool results.
SP-pagination-v1 §4.2 + §4.5. The reference cursor implementation is
deliberately stateless: the server doesn’t keep a cursor table — it
HMAC-signs the payload and trusts the verified bytes on each
RunToolContinue. Trade-off: 512-byte wire cap limits embedded
state (~256B for opaque_state after fixed-field overhead).
Adopters writing their own paginating tools can use CursorIssuer
to issue + verify cursors without touching keys directly — call
ctx.cursor_issuer() from inside Tool::call_paginated (Phase D wiring).
§Cursor wire shape
base64url( CBOR(CursorPayload) || HMAC-SHA256(key, CBOR(CursorPayload)) )- CBOR encoding is ~80B for the fixed fields, leaving ~250B for
opaque_statewithin the 512-byte cap. - HMAC tag is 32 bytes (SHA-256 output, constant-time verified).
- base64url-no-pad keeps the cursor safe to ship inside JSON
arguments.__cursor(the HTTP / MCP-bridge surface, Phases F-G).
§Cursor scope
Cursors are bound to (tool_id, caller_id, args_fingerprint, server_session):
tool_id— server rejectsRunToolContinuewhosetool_id≠ embedded.caller_id— if the connection’s identity changes (UDS Hello vs HTTP bearer), the cursor is invalidated (mismatch returnsERR_CURSOR_INVALID).args_fingerprint— SHA-256 of canonical-JSON-serialized original args; continuing with mutated args is a protocol violation.server_session— random nonce minted at issuer construction; server restart → new nonce → all outstanding cursors invalidated (returnsERR_CURSOR_EXPIREDso adopters can distinguish from forgery).
Structs§
- Cursor
Issuer - HMAC-SHA256 cursor issuer + verifier. One per server process; constructed
at startup with
SharedServerConfig.cursor_signing_key. Multi-instance deployments behind a load balancer can share a key via env (ATD_CURSOR_SIGNING_KEY=base64...); single-instance deployments use a fresh random key per startup (default). - Cursor
Payload - Decoded cursor payload. Server-internal; clients never see the individual fields, only the base64url-encoded signed blob.
Enums§
- Cursor
Error - Errors from cursor issue / verify operations.
Constants§
- MAX_
CURSOR_ BYTES - Maximum encoded cursor length on the wire. Bounded so cursors can
safely ride inside HTTP headers, JSON-RPC
arguments.__cursorfields, and audit log lines without truncation. - MAX_
OPAQUE_ STATE_ BYTES - Maximum opaque-state size inside a cursor payload. Operators who need
more should store state server-side keyed by a 16-byte cursor ID and
put just the ID in
opaque_state.
Functions§
- args_
fingerprint - Compute the canonical
args_fingerprintfor aRunTool.argsvalue. Used at issue time (server) and could be used at verify time (server) if the dispatch layer wants to check that a continuation’s args match the original. The reference dispatch (Phase D) passesserde_json::Value::Nullon continuations and binds args via the embedded opaque_state, so this is mainly an integrity tool. - random_
signing_ key - Generate a fresh 32-byte cursor signing key from the OS RNG. Used by
listener crates (
atd-server/atd-server-http) atServer::newso they don’t have to take a directgetrandomdep. - signing_
key_ from_ env_ or_ random - SP-pagination-v1 §4.2 — pick a cursor signing key by precedence: