- check-phase19.7.sh: workspace_check (framework to this checkout, the OAuth
server plugin's user plugin to USER_PLUGIN) in the plugins, consumers and
docker stages; consumer_with_user_plugin builds fonoteka.go and sm-bm-app
against USER_PLUGIN through a scratch go.work; demo_pins accepts
unreleased commits its canonical checkout holds and refuses any drift
with PHASE197_RELEASED=1; self-test cases for each, one PASS line each
- TestSessionReaper pins A-CR-01, A-IN-01 and A-IN-06 (memory and Redis
stores, store errors, the GET stream re-check, the ephemeral label)
- TestAuthTime pins auth_time at mint and through every refresh path
- GO_TESTS and GO_TAGGED_TESTS require them with the plan 06/07 smokes
- pact.MCPSubjectChanged and pier.Event gain TokenID and ClientID; a
narrowed revoked event closes only matching sessions and stateless servers
- cabana fires the admin token revoke with the token row id
- towel.WithClientIP/ClientIP; pier puts the address (surf applies
http.trusted_proxies) on the resolve context and ResolveBackendToken logs it
- pact.MCPPrompt.Writes (additive; the zero value keeps today's listing)
- pier's allowsPrompt hides a prompt declared with Writes from an identity
without write, as it hides a tool that is not read-only (C-IN-01
framework part, P-28)
- a path field must be required in the reflected schema (boot error), and
a missing value, an empty, . or .. segment, or a / inside one segment is
a tool error before dispatch (A-WR-01, A-WR-02)
- a failed guard dispatch logs its cause and answers a fixed tool text;
only an invalid token and an unknown or deactivated account keep their
own fixed texts (A-WR-08)
- path and query scalars decode with UseNumber, so integers above 2^53
keep their digits (A-IN-04)
- the dispatched request carries the outer Host, X-Forwarded-Proto and
X-Forwarded-Host (A-IN-05)
- an embedded struct on a GET or DELETE tool fails boot (A-IN-07)
- a dispatch whose guard list or principal kind contradicts the tool's
guard is refused (A-IN-08)
- 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
- TestEndpointGuards: a foreign browser Origin is 403 before the bearer is
read, a body over the endpoint cap is 413, a long GET stream ends once
its grant stops verifying (T-19.7-18, T-19.7-19)
- TestDispatch: an argument key the input struct does not declare is
refused before dispatch (T-19.7-17)
- the gate requires TestEndpointGuards (untagged and tagged) and runs the
demo's deploy check (T-19.7-37)
- pier: TestCatalog, TestDispatch, TestGuardRules, TestSessions (Redis
across two instances and the memory store), TestFanOut; the store clock
and stateless-server seams in export_test.go
- surf: TestStreamingRoute (throttle, CORS profile and access-log pattern on
a stream route) and TestPierMount in both builds; the streaming smoke
restores the mount hook it replaces
- wristband TestMCPScopes; cabana TestAccountContributions (Postgres part:
transaction and unchanged My account payloads), TestRecordActionPrompt,
TestScopeLimitForMethods; TestRecordActionPromptRun pins the empty Value
of an action without a prompt
- vitest: 403 and failed-load cases for My account contributions, the
cancelled prompt
- gate: every name required (tagged runs included), account.go unchanged
since 51d088a, admin stage live
- 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)
- MCP's tools/call arguments are optional; pier validated a JSON null
against the input schema and refused every call of a tool without
required input that an agent sent without arguments
- found by plan 19.7-05's TestDispatch
- scripts/check-phase19.7.sh: fail-closed multi-repo gate (self-test, go,
marker; plugins, consumers, docker, admin and docs are stubs until the
later tasks fill them)
- modules/pier/helpers_test.go: the plan 02 fixtures moved out of the smokes
plus fakeGuard, fakeVerifier, fakeLinkResolver, fakePersonalResolver,
routePlugin, newApp, twoInstances, sdkClient and fireSubjectChanged
- TestToolsList (pier), TestDispatchMarker (bouncer, cabana with a Postgres
part through the screens harness, which now keeps its app)
- personal tokens only behind mcp.allow_personal_tokens (401 without a
lookup otherwise); an admin token yields a backend-only identity with
its scope limit per dispatched method, a user token a frontend-only one
- read/write ceiling and prompts proven by the tracer's subtests; the
audit line asserted to carry the tool and never the arguments or token
- the mounted *pier.Pier is published on the application (test seams in
export_test.go)
- docs/services/mcp.md with the tested acme.blog example, site.yaml note,
cross-links from authentication.md, registration.md and the surf README
- 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