Raxol.MCP.Client.Reservation (Raxol MCP v2.8.0)

Copy Markdown View Source

Single-use handles for a priced tools/call (ADR-0037 decision 7).

The transport refuses a priced tool whose call carries no spend-gate handle. That refusal was Map.get(request, :reservation) truthiness, which is not a check: Raxol.MCP.Client.call_tool/4 is public, so any caller could pass reservation: "anything" and the transport could not tell a live reservation from a settled one, a refused one, or a string someone invented. The metering gate sits on the function that issues the request, and a nominal gate there is the same hole as no gate at all.

So a handle is a value that must be MINTED and can be spent once. mint/1 writes 18 bytes of :crypto.strong_rand_bytes/1 into the shared table Raxol.MCP.Client.Tables owns; consume/2 takes it out. A literal, a replay of a handle already spent, and one past its TTL are all indistinguishable from an absent handle, which is what Raxol.Agent.McpSpendHook mints against and the transport refuses.

What this does and does not defend

It defends the transport's own gate: a call arriving through the public API with a forged, reused or stale handle is refused, and a handle the spend hook minted is good for exactly one request. It does not pretend to defend against arbitrary code on this node -- the table is :public, because the minting side is raxol_agent and the consuming side is this package, and a process that can write another process's ETS table can equally call this transport directly. The authority being protected is the SPEND HOOK's: only the hook that reserved against a budget has a handle to hand on.

Why a TTL

A minted handle is not always spent. The transport refuses before issuing for a full in-flight window (:busy), an open breaker, or an unknown price, and each of those leaves the handle unconsumed. Every mint sweeps what has expired, so the table is bounded by the handles minted inside one TTL rather than by the lifetime of the node.

Summary

Functions

Spend token, or refuse it.

Mint a single-use handle in the shared table, starting its owner if needed.

Mint a single-use handle in table.

A table for minted handles. Raxol.MCP.Client.Tables owns the shared one.

Functions

consume(table, token)

@spec consume(:ets.table(), term()) :: :ok | :error

Spend token, or refuse it.

:ok exactly once per minted handle. :error for anything else: a handle never minted, one already spent, one whose TTL has passed, or a term that is not a handle at all.

mint()

@spec mint() :: String.t()

Mint a single-use handle in the shared table, starting its owner if needed.

mint(table)

@spec mint(:ets.table()) :: String.t()

Mint a single-use handle in table.

new(name)

@spec new(atom()) :: :ets.table()

A table for minted handles. Raxol.MCP.Client.Tables owns the shared one.