You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 57568ed
Browse filesBrowse the repository at this point in the historyBrowse files
fix(agent): stop cross-user MCP credential leak in the Agent tab (#78)
The agent provisioner mirrored each freshly minted MCP token onto
`$HERMES_HOME/deepsql.token` AND every `profiles/*/deepsql.token`, which
made the agent credential globally last-writer-wins. Any user opening
the Agent tab overwrote every other user's token, so their agent then
authenticated as the newcomer. Two concurrent users was the whole
trigger; no impersonation required.
Verified end to end before the fix (analyst = DEVELOPER, admin = ADMIN,
`QA Vault Copy` admin-only):
analyst opens tab -> analyst file: dsql_mcp_v2nA7...
admin opens tab -> analyst file: dsql_mcp_p4nIo... (admin's token)
/api/auth/me with analyst's file -> username=admin, role=ADMIN
analyst session -> QA Vault Copy -> 403
analyst agent -> QA Vault Copy -> 200, 133 tables
SELECT via that token -> logged as user_id=8 (admin)
So one user read another's database and the audit row named the wrong
person. The schema read produced no audit event at all.
The mirror was introduced to fix a real View-as bug (#71): Hermes keeps
ONE MCP subprocess, `profile/switch` is `process_wide=False`, so the
live process kept whichever credential loaded first. That diagnosis
stands — only the remedy was too broad.
Fix, in two layers:
1. The provisioner writes only `$HERMES_HOME/deepsql.token`, the one
shared path the live process may have started from. Per-profile tokens
are no longer touched, so one user's open cannot destroy another's
credential.
2. That root file is still shared, so the durable guard is server-side:
`McpTokenAuthenticationFilter` refuses an MCP token whose owner differs
from the request's `DEEPSQL_MCP_USER_ID` claim (already sent as
`X-DeepSQL-Client-Agent`), answering 401 `mcp_identity_mismatch`. The
claim is only ever used to REFUSE, never to grant, so forging it cannot
widen access. A claim that is not a real DeepSQL username is ignored,
keeping editor/CLI MCP installs (`cursor`, `claude-desktop`, any
`--caller-agent`) working.
Also:
- The provisioner self-test asserted the fan-out was CORRECT — it
modelled only the View-as case, where overwriting is desired — so a
green suite guarded the bug and would have failed on this fix. It now
asserts the opposite: provisioning B leaves A's token intact.
- `probeMcpAuth` sends the same identity header so the boot health check
exercises the binding instead of bypassing it. Without that it would
report a token healthy that the live MCP process cannot use.
- `AgentChatPanel` re-bootstraps on an SSE error to surface the real
cause; EventSource exposes no status code, so session expiry, an agent
restart and a refused credential all arrived as "the agent run ended
early".
Verified after the fix: analyst keeps its own token when admin opens the
tab; the leaked-token request is 401 instead of 200/133-tables; and
every legitimate path still works (own agent 200, editor MCP 200,
no-header curl 200, analyst -> admin-only DB still 403).
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
0 commit comments