The stateful GET stream and the stateless subscriptions/listen stream
capture the client address when they open and resolve every periodic
re-check with towel.WithClientIP, as the bearer middleware does, so a
failing re-check (a revoked admin token, for example) is logged with the
remote address. TestStreamRecheckClientIP records the address the verifier
sees on every verification of both streams.
promptHandler logs a failing Render's error with the prompt name and
answers the fixed JSON-RPC internal error "pier: the prompt could not be
rendered", as dispatch does for tool calls (A-WR-08), so database or
provider text never reaches the agent or its transcript.
TestPromptRenderError renders a prompt that fails with a database error
naming its host and asserts, on both paths, that the client sees only the
fixed text and the cause is in the log.
Fan-out matching snapshots the sessions (and stateless entries) under the
router-wide lock, releases it, and reads each identity through a small
identity box that is never held across I/O, so an event no longer waits on
a session that is re-filtering while it holds sr.mu or sp.mu.
Widening and the fan-out recomputes run their guard checks with
guardContext: not cancelled with the triggering request, but bounded by
mcp.call_timeout, so a provider that never answers cannot hold a session
lock forever. README and docs/services/mcp.md describe the wider meaning of
mcp.call_timeout.
TestLockScopeUnderWidening blocks a widening guard check, fires a
permission event for that session and asserts that another session's call
still finishes, and that a never-answering check ends at call_timeout.
- Identity.fingerprint covers scopes, both subjects, the ceiling and the
token kind; a stateful lookup or stateless hit whose fingerprint changed
re-filters the server with the event diff under its own mutex (A-WR-04)
- Mount warms a Redis store in the background with a 1 s to 30 s backoff
and every request kicks a pending open without waiting, so stateless-only
instances subscribe to fan-out (A-WR-05)
- a stateless subscriptions/listen POST re-verifies its bearer every
recheck interval; an LRU-evicted stateless server closes its sessions
(A-WR-06)
- resolvePersonalToken refuses a subject with TokenID 0, and the
pact.MCPPersonalTokenResolver contract says one resolver per application,
false means unauthorized and TokenID keys the binding (A-WR-09, A-IN-03)
- an unexported reserver (Reserve, Release) next to updater: the memory
store reserves under its mutex, the Redis store in one Lua script
(prune, SCARD, SET PX plus SADD); serveNew reserves instead of Count
then Put and releases on a failed connect or initialize (A-WR-03)
- serveNew, lookup's rehydration and the stateless getServer build the
SDK server with no router-wide lock held and insert or adopt under the
lock afterwards, so a slow guard or database check no longer stalls
the instance (A-WR-07)
- a reaper started at Mount checks every local session against the store
every min(idle_ttl/2, 1m) and closes each copy whose record is gone,
logging the expired step (A-CR-01, A-IN-06)
- the GET stream re-check ends the stream when the record is gone (A-IN-01)
- tool calls served without a session on the stateful path log the
session as ephemeral (A-IN-06)
- TestPierReaperSmoke and the ReapNow, LocalSessions, StopReaper seams
- persisting a session's protocol state after a request now rewrites an
existing record only (Redis SET XX, the memory store likewise) and closes
the local copy when the record is gone
- before, a request that finished after DELETE, a revoked or deleted subject
event or the idle expiry put the record back, and the next request
rehydrated the ended session
- found by plan 19.7-05's TestFanOut (revocation and deletion cases, about
one run in a hundred)
- store_redis.go: records with PX TTLs, per-binding sets, pub/sub events,
opened lazily from redis.url under cooler's prefix (nothing dials at mount)
- events.go: pact.MCPSubjectChanged republished through the store; every
instance recomputes the subject's sessions and stateless servers
(list_changed) or ends them on revocation; the link cache is dropped
- stateless.go: the SDK's stateless handler over per-identity servers for
2026-07-28 clients, routed by the protocol header or server/discover
- prm.go: RFC 9728 metadata at both well-known paths; long GET streams
re-verify their bearer every minute
- TestPierSessionsSmoke, TestPierFanOutSmoke, TestPierStatelessSmoke on
miniredis; README features, configuration and operational notes
- modules/pier: Host contract, mcp.* config, catalog compiled from
pact.HasMCP (prefixing, route binding, guard resolution, schema
reflection, annotations), per-request identity through the published
verifier and resolvers, built-in backend guard provider, in-process
dispatch with the bouncer marker, per-identity SDK server with the
per-request re-filter, stateful session router with rehydration over a
memory store, audit lines
- modules/surf/pier_on.go: the summercms_mcp hook adapting the router to
pier.Host; the untagged hello binary carries no SDK
- go.mod: go-sdk v1.8.0 and jsonschema-go v0.4.3 (decision note committed
before this change)
- TestPierTracerSmoke: SDK client lists and calls acme_echo end to end
through surf's compiled guard chain; README and root table row