feat(server): bind each member their own session-scoped token, release 0.0.5 - #48
Merged
Conversation
Members of one slot shared a single JWT. A session was bound to the grant of the token that created it, at creation time only, so the queue had to make the token that created the session the token every member attached with. That token could not be refreshed either, since a re-mint started an empty grant, so it was issued to outlive the whole turn and tokenTtlSeconds degraded into a floor. The Coordinator now binds sessions that already exist at mint (resources.sessions.bind), on the sole condition that the session is open and owned by the minting API key. The queue is that key's holder, so the server goes back to creating sessions and minting connections with its own JWT, and each member receives a token bound to the one session they were seated on. Members sharing a slot hold distinct tokens, and a spilled member's token follows them to the session they landed on. Binding works against a live session, so a member's token is minted fresh whenever it is needed: tokenTtlSeconds is a real lifetime again (60s by default) and request_token re-mints rather than re-delivering. The limit is left to resolve to the bound count, which makes a member's grant full on arrival — it drives the session it was given and cannot open another. A member in the admission grace has no session to bind to and receives their token at claim. The slot no longer carries a token, so a slot persisted by an earlier version binds like any other. Deployments with an acquireSession override keep unscoped tokens: the session they supply need not belong to this server's API key. Signed-off-by: Dere-Wah <derexcontact@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
Author
This stack of pull requests is managed by Graphite. Learn more about stacking. |
An acquireSession override still handed members fully unscoped JWTs: account-wide tokens able to create sessions for any model and reach anything else the API key can. That path predates binding, when a session the queue had not created could not be scoped to at all. Binding only asks that the session be open and owned by the same user as the minting API key, which an override that customises how the session is made — extra arguments, a pinned release, its own retry — already satisfies. So the queue scopes every member token and keeps no unscoped path at all. An override that sources a session from a different user now fails rather than degrading: the Coordinator refuses the mint, the member receives token_mint_failed, and the admin log carries the 403. Falling back to an unscoped token would leave an operator believing members were scoped while they were not, which is worse than a loud failure. Same account is not sufficient, and the README now says so. Signed-off-by: Dere-Wah <derexcontact@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Patch release for session-bound member tokens. Signed-off-by: Dere-Wah <derexcontact@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Dere-Wah
force-pushed
the
dere/per-member-bound-tokens
branch
from
July 28, 2026 07:26
a6297e0 to
d2e2e30
Compare
Dere-Wah
requested review from
cruhl,
tempusfrangit,
todd-elvers and
willemhelmet
July 28, 2026 07:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Why
Every member of a slot holds the same JWT. PR #46 chose that design because the platform bound a session to the token that created it, at creation time only. A token minted later could not reach a session that already existed.
Three costs follow:
usersPerSession > 1) gives several browsers one identical credential.tokenTtlSecondsbecame a floor, not a lifetime.acquireSessionoverride receives no scope at all. Members of those deployments hold an unscoped, account-wide JWT.POST /tokensnow acceptsresources.sessions.bind, which binds a token to a session that already exists. The session must be open. Its owner must be the user that owns the API key. The queue server holds that key, so this change removes all three costs.What this changes
The slot no longer owns a token.
mintConnectionOnSlotcreates the session and the connection with the server JWT.CoordinatorClient.createSessionandcreateConnectiondrop their per-calljwtparameter.mintMemberToken(sessionId)mints one token per member. Each token names the configured model and binds to the one session assigned to that member. It omitsmax_sessions, so the limit resolves to the bound count. The grant arrives full, and the token cannot open a session of its own.A token is no longer single-use, because the bind targets a live session.
sendMemberTokenmints a fresh token on every call.request_tokenand the clientgetJwtresolver now refresh for real.tokenTtlSecondsis a true 60 second lifetime again.A token cannot exist before its session. A member who reserves a new slot receives the token at claim, just before
session_ready. A member who joins a running slot receives it at once. Each token binds to the session that the member lands on, so a member moved to another slot needs no second delivery.No unscoped path remains. The
scopedTokensswitch is gone, so anacquireSessionoverride is scoped like every other deployment. Its session must come fromRQ_REACTOR_API_KEY, or from another key of the same user. The platform checks ownership per user, not per account. The README said "same Reactor account", which is too weak, and this PR corrects it.A session from a different user now fails visibly. The Coordinator refuses the mint. The member receives
{ type: "error", message: "token_mint_failed" }, and the admin log records the 403. A silent fallback would let an operator believe that members are scoped when they are not.API surface
Nothing changes for clients. Members still receive
{ type: "token", jwt, expiresAt }and attach withconnect({ sessionId, connectionId }). Only the request that the server sends is new:POST /tokens { "expires_after": 60, "authorization_details": [ { "type": "session", "resources": { "models": { "match": ["helios"] }, "sessions": { "bind": ["<the member's session id>"] } } } ] }RQ_TOKEN_TTL_SECONDSrecovers its documented meaning. Set it back to a small value if you raised it because of the old floor.Verification
pnpm typecheck,pnpm test(133 tests),pnpm build,pnpm format:check, and./scripts/check-license.shpass.New tests cover:
session_readyrequest_tokenre-mintsacquireSessionsession, and a refused bind that raises an errorTwo invariants also hold. No member token authorizes a session or connection call, and no member holds an unscoped token.
A live deployment confirmed the behaviour. Several tokens each took their own connection on one shared session, and the Coordinator applied a short
expires_afterto a bound mint. The refusals held too. A full grant cannot create a session, cannot reach another session, and cannot call a non-session API.