feat(session): MCP session registry + spawn-on-initialize (forgejo-mcp-broker-t81)
Adds internal/session.Registry, the MCP session glue that maps
Mcp-Session-Id to a running forgejo-mcp child + bridge.
Lifecycle:
- First /mcp POST without Mcp-Session-Id: SpawnFunc creates a backend
(in production: supervisor.Start + bridge.New); registry mints a
192-bit hex session id, attaches it to the response header, and
dispatches the request to the new backend.
- Subsequent POSTs with the header dispatch to the existing backend.
- Unknown sids → 410 Gone (per MCP guidance, so clients re-initialise
instead of retrying forever).
- Sids are bound to the OAuth token that minted them: a different
bearer probing a stolen sid gets 403, distinct from "your token is
bad" (401) and "sid unknown" (410).
Cleanup:
- When backend.Done closes (child exited on its own — crash, OOM,
user-driven shutdown), a goroutine reaps the entry.
- Stop tears every session down on broker shutdown. The 30s idle
reaper and Forgejo token rotation come in 5c.
The Registry is decoupled from supervisor and bridge via SpawnFunc, so
tests don't need to fork real processes — they hand the registry a fake
that returns a controllable Backend. Also added oauth.ContextWithSession
so the session tests can inject an oauth.Session into request contexts
without standing up the full bearer-middleware chain.
Tests: 83.3% coverage. Cover spawn-on-initialize, sid reuse, unknown
sid, max-session cap with Retry-After, no-auth-context guard, sid
hijack defense (token mismatch → 403), Done-channel reaping, and
graceful Stop.
Closes forgejo-mcp-broker-t81.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9c8cf40501
commit
886092a600
4 changed files with 589 additions and 2 deletions
|
|
@ -42,6 +42,13 @@ func SessionFromContext(ctx context.Context) (*Session, bool) {
|
|||
return s, ok
|
||||
}
|
||||
|
||||
// ContextWithSession attaches a Session to ctx using the same key
|
||||
// RequireBearer uses. Primarily useful in tests that want to drive a
|
||||
// gated handler without standing up the full OAuth flow.
|
||||
func ContextWithSession(ctx context.Context, s *Session) context.Context {
|
||||
return context.WithValue(ctx, sessionCtxKey{}, s)
|
||||
}
|
||||
|
||||
// RequireBearer is HTTP middleware that:
|
||||
// 1. Demands an `Authorization: Bearer <token>` header.
|
||||
// 2. Looks the token up by SHA-256 hash in access_tokens.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue