Findings
Browser affinity comes from the bearer, not from the agent
Date: 2026-09-17
What we found#
Since the pod-affinity fix (2026-09-10) every browser_* tool took a session_id that was HMAC(SESSION_SECRET, userId), and the /mcp handler dug it out of executions[].args to set X-Browser-Session on the hop to the internal service. But that handler had already resolved the bearer to a userId before it looked — the agent was carrying a value the server could compute from what it already had.
Two consequences:
session_idwas spent on routing, so there was no way to name a second tab. One user, one page.POST /rest/:integrationruns the same tools through the same engine but never forwarded at all, so underCLUSTER_ENABLEDaPOST /rest/browsercould spawn a second Chromium on the shared profile and fight the first forSingletonLock.
What changed#
- One helper (
auth/affinity-forward.ts) setsX-Browser-Session: mintSessionKey(userId)when a/mcptools/callor aPOST /rest/browsertouches abrowser_*tool. The agent's arguments are never consulted. session_idnow names a tab (a ChromiumtargetId) inside the caller's own session.browser_startopens one;browser_tabslists them;browser_closecloses one. Lookup is scoped to the caller'sWarmSession, so no HMAC is needed on the id.- The key stays an HMAC rather than the raw user id: the portal live view already hashes on it, and a user id does not belong in mesh headers or proxy logs.
What did not change#
The portal live view still sends the header from the client: its requests meet the proxy before any server code runs, so the server cannot add it. attach mints the key exactly as before. Both paths hash on the same value.