fix(19.7): RA-WR-01 never wait on a session lock under the router lock
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.
This commit is contained in:
@@ -65,8 +65,12 @@ type localSession struct {
|
||||
transport *mcp.StreamableServerTransport
|
||||
ss *mcp.ServerSession
|
||||
|
||||
// mu serializes re-filtering (widening, fan-out recomputes) and guards
|
||||
// fingerprint, tools and prompts; it may be held across guard checks,
|
||||
// bounded by mcp.call_timeout. identity has its own box, so fan-out
|
||||
// matching never waits on mu.
|
||||
mu sync.Mutex
|
||||
identity Identity
|
||||
identity identityBox
|
||||
fingerprint string // of the identity the tool set was last filtered for
|
||||
tools map[string]bool
|
||||
prompts map[string]bool
|
||||
@@ -330,7 +334,7 @@ func (sr *sessionRouter) lookup(w http.ResponseWriter, r *http.Request, id Ident
|
||||
}
|
||||
}
|
||||
ls.mu.Lock()
|
||||
ls.identity = id
|
||||
ls.identity.set(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 {
|
||||
@@ -344,7 +348,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, fingerprint: id.fingerprint()}
|
||||
ls := &localSession{id: sid, binding: id.Binding, created: time.Now().UTC(), identity: identityBox{id: 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}
|
||||
|
||||
Reference in New Issue
Block a user