Skip to main content

Module cursor

Module cursor 

Source
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_state within 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 rejects RunToolContinue whose tool_id ≠ embedded.
  • caller_id — if the connection’s identity changes (UDS Hello vs HTTP bearer), the cursor is invalidated (mismatch returns ERR_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 (returns ERR_CURSOR_EXPIRED so adopters can distinguish from forgery).

Structs§

CursorIssuer
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).
CursorPayload
Decoded cursor payload. Server-internal; clients never see the individual fields, only the base64url-encoded signed blob.

Enums§

CursorError
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.__cursor fields, 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_fingerprint for a RunTool.args value. 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) passes serde_json::Value::Null on 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) at Server::new so they don’t have to take a direct getrandom dep.
signing_key_from_env_or_random
SP-pagination-v1 §4.2 — pick a cursor signing key by precedence: