fix(pier): widen returning identities, warm the Redis store, end stateless listen streams, refuse personal tokens without a TokenID

- 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)
This commit is contained in:
Jakub Zych
2026-10-23 15:59:54 +02:00
parent c1c0c77ddc
commit 240457291f
10 changed files with 217 additions and 39 deletions

View File

@@ -33,6 +33,7 @@ const (
methodListPrompts = "prompts/list"
methodGetPrompt = "prompts/get"
methodDiscover = "server/discover"
methodListen = "subscriptions/listen"
)
// ephemeralSession is the session value of the audit line of a tool call
@@ -64,10 +65,11 @@ type localSession struct {
transport *mcp.StreamableServerTransport
ss *mcp.ServerSession
mu sync.Mutex
identity Identity
tools map[string]bool
prompts map[string]bool
mu sync.Mutex
identity Identity
fingerprint string // of the identity the tool set was last filtered for
tools map[string]bool
prompts map[string]bool
initialized atomic.Bool
persisted atomic.Bool
@@ -329,6 +331,7 @@ func (sr *sessionRouter) lookup(w http.ResponseWriter, r *http.Request, id Ident
}
ls.mu.Lock()
ls.identity = id
sr.p.widen(ctx, ls.server, id, &ls.fingerprint, ls.tools, ls.prompts)
ls.mu.Unlock()
if err := sr.p.store.Touch(ctx, sid, sr.p.cfg.IdleTTL); err != nil {
sr.p.storeUnavailable(w, err)
@@ -341,7 +344,7 @@ func (sr *sessionRouter) lookup(w http.ResponseWriter, r *http.Request, id Ident
// with the session id, from the stored state when rehydrating. A goroutine
// drops the local entry when the session closes.
func (sr *sessionRouter) connect(ctx context.Context, sid string, id Identity, state *mcp.ServerSessionState) (*localSession, error) {
ls := &localSession{id: sid, binding: id.Binding, created: time.Now().UTC(), identity: id}
ls := &localSession{id: sid, binding: id.Binding, created: time.Now().UTC(), identity: id, fingerprint: id.fingerprint()}
srv, tools, prompts := sr.p.newServer(ctx, id, func() { ls.initialized.Store(true) })
ls.server, ls.tools, ls.prompts = srv, tools, prompts
ls.transport = &mcp.StreamableServerTransport{SessionID: sid}