Skip to content

feat(bridle,mcpServer): per-user MCP OAuth tokens on a shared channel (CLEAN-80) - #106

Merged
maksymhryzodub-prog merged 2 commits into
feat/CLEAN-78-mcp-probefrom
feat/CLEAN-80-per-user-mcp-tokens
Sep 24, 2026
Merged

maksymhryzodub-prog merged 2 commits into
feat/CLEAN-78-mcp-probefrom
feat/CLEAN-80-per-user-mcp-tokens

Conversation

@maksymhryzodub-prog

@maksymhryzodub-prog maksymhryzodub-prog commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Jira: CLEAN-80

Stacked on #105 (feat/CLEAN-78-mcp-probe). Merge #105 first, then retarget this one to main. Runtime half: CleanSlice/runtime PR for CLEAN-79 / CLEAN-81.

Why

Owners and admins chat as one admin conversation on purpose: shared history, shared context. The MCP OAuth token from CLEAN-75 was stored per agent, so everyone on that channel used the Silpo account of whoever logged in first. The identity for tokens has to travel separately from the channel.

What

Hub — a console login's JWT sub (+ email) is kept on its socket record and attached to every message that socket sends, as user next to clientId (socket and HTTP sync paths alike). Two admins still share admin; the runtime now knows which of them is typing. Share visitors and anonymous embeds carry no user — their clientId is already theirs alone.

OAuth

  • start takes a subject (the user id the hub forwarded, or a share/anon client id; the PKCE state remembers it, with the email for display). The callback stores the bundle under mcpOauth:<serverId>:<subject> and pushes mcp_connected { server, serverId, subject } so the runtime brings up the client for exactly that person. Without a subject the agent-wide mcpOauth:<serverId> bundle is written as before — what start_mcp_oauth from the Rancher chat still creates (it gained an optional subject).
  • status answers per subject with the agent-wide bundle as fallback and says which it found, plus the login email for "connected as …".
  • Sweep: bundles of per-browser subjects (anon-*, share-*) unused for 30 days are revoked (RFC 7009 when the provider advertises revocation_endpoint) and dropped every six hours; a real user's bundle is never swept. Bundles carry connectedAt / lastUsedAt; the runtime bumps the latter.
  • Migration 20260923120000_mcp_oauth_subject: McpOauthState.subject / subjectEmail, nullable, additive.

Verification

  • api: jest on bridle + mcpServer → 24 suites, 358 tests green (new: identity follows the sending socket, admin sub kept next to clientId: admin, per-subject storage, status order, sweep rules); tsc --noEmit and eslint clean.
  • Needs bun run migrate on deploy (Prisma client regenerated locally).
  • Smoke test after the runtime PR is deployed: two admins open the same agent, each runs the Silpo connect from the chat, each sees their own cart.

Live test (2026-09-23, local stack, real Silpo)

  • Hub: two sockets with different JWTs on clientId: admin — the throwaway agent received both messages with distinct user { id, email }.
  • start with a subject: PKCE state keeps subject + email, DCR at Silpo succeeded, callback stored mcpOauth:<silpo>:<sub> with refresh token, connectedAt, lastUsedAt, revocationEndpoint.
  • status per subject answers scope: subject with the email; another subject gets connected: false.
  • Found live and fixed here: the S3-backed secret gateway lists names as <agentId>/<key>, so status/sweep never matched a stored bundle (ae466ad).

🤖 Generated with Claude Code

https://claude.ai/code/session_01KtJu4QscRKbDdj8WLL9aVL

@maksymhryzodub-prog
maksymhryzodub-prog added this pull request to stack #107 September 23, 2026 12:16
maksymhryzodub-prog and others added 2 commits September 24, 2026 14:44
… (CLEAN-80)

Owners and admins chat as one `admin` conversation on purpose — shared
history, shared context. The MCP OAuth token from CLEAN-75 was stored per
agent, so everyone on that channel used the Silpo account of whoever logged
in first. The identity for tokens now travels separately from the channel.

Hub: a console login's JWT `sub` (+ email) is kept on its socket record and
attached to every message that socket sends, as `user` next to `clientId`
(socket and HTTP sync paths alike). Two admins still share `admin`; the
runtime now knows which of them is typing. Share visitors and anonymous
embeds carry no user — their clientId is already theirs alone.

OAuth: start takes a subject (the user id, or a share/anon client id; the
PKCE state remembers it), the callback stores the bundle under
mcpOauth:<serverId>:<subject> and pushes mcp_connected { server, serverId,
subject } so the runtime brings up the client for exactly that person;
without a subject the agent-wide mcpOauth:<serverId> bundle is written as
before, which is what start_mcp_oauth from the Rancher chat still creates.
status answers per subject with the agent-wide bundle as fallback and says
which one it found, plus the login email for "connected as …".

Sweep: bundles of per-browser subjects (anon-*, share-*) unused for 30
days are revoked (RFC 7009, when the provider advertises an endpoint) and
dropped every six hours; a real user's bundle is never swept. The bundle
carries connectedAt/lastUsedAt for that; the runtime bumps lastUsedAt.

Migration adds McpOauthState.subject / subjectEmail, nullable, additive.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtJu4QscRKbDdj8WLL9aVL
…ndle (CLEAN-80)

The S3-backed secret gateway lists entries as `<agentId>/<key>`, the AWS
one as the bare key. `status` compared against the bare key only, so on the
file provider a bundle stored a minute earlier answered "not connected" —
seen live against Silpo. The same comparison would have kept the sweep from
ever matching a per-browser bundle. Both now strip the agent's own scope
before comparing; deletion keeps the listed name, which either gateway
resolves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtJu4QscRKbDdj8WLL9aVL
@maksymhryzodub-prog
maksymhryzodub-prog force-pushed the feat/CLEAN-80-per-user-mcp-tokens branch from ae466ad to 2a146bb Compare September 24, 2026 11:44
@maksymhryzodub-prog
maksymhryzodub-prog merged commit 6f05c90 into main Sep 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant