# NousResearch/hermes-agent · Propagating dashboard auth user identity to agent sessions

## Open now

- 1 issue, 1 PR open on GitHub.
- #62549: \[Bug\]: Dashboard auth identity (\`Session.user_id\`) never reaches agent construction — memory providers silently fall back to the default user, breaking multi-user isolation
- PR #60458: Propagate dashboard auth identity to TUI agents

Most members concern passing the authenticated dashboard user identity (Session.user_id) through to agent construction, including PTY/TUI spawn paths, with issue reports about multi-user isolation and memory providers. One member, about scoping chat attach tokens by session, is related to dashboard auth but is a somewhat different concern.

## TL;DR

- 8 member(s): 1 open issue(s), 1 closed issue(s), 1 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #60745. Verify it addresses your case.
- **Recommended:** read PR #60745 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.23`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #62549: \[Bug\]: Dashboard auth identity (\`Session.user_id\`) never reaches agent construction — memory providers silently fall back to the default user, breaking multi-user isolation

## Closed / duplicate issues

- #35408: \[Feature\]: Dashboard authenticated user identity should propagate to the agent session

## Prior & competing PRs

- **#60745: fix(dashboard): scope chat attach tokens by session**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/web_server.py`, `tests/test_pty_keepalive_ws.py`
  - fix hunks:
  - `hermes_cli/web_server.py`

    ```diff
    @@ -15149,7 +15149,8 @@ async def pty_ws(ws: WebSocket) -> None:
             return
     
         # --- spawn PTY ------------------------------------------------------
    -    resume = ws.query_params.get("resume") or None
    +    raw_resume = ws.query_params.get("resume") or None
    +    resume = raw_resume
         profile = ws.query_params.get("profile") or None
         channel = _channel_or_close_code(ws)
         sidecar_url = _build_sidecar_url(channel) if channel else None
    ```

    ```diff
    @@ -15192,6 +15193,12 @@ async def pty_ws(ws: WebSocket) -> None:
     
     
         attach_token = ws.query_params.get("attach") or None
    +    registry_resume = raw_resume
    +    if raw_resume and env:
    +        registry_resume = env.get("HERMES_TUI_RESUME") or raw_resume
    +    if attach_token is not None and (registry_resume or profile):
    +        # Key explicit resumes on their canonical target, never the active-session fallback.
    +        attach_token = f"{attach_token}\0{profile or ''}\0{registry_resume or ''}"
     
         def _spawn():
             return PtyBridge.spawn(argv, cwd=cwd, env=env)
    ```
  - `tests/test_pty_keepalive_ws.py`

    ```diff
    @@ -1,53 +1,136 @@
    +import json
    +
     import pytest
     
     from hermes_cli import web_server
     
     
    [redacted:email]
    -async def test_attach_token_reuses_same_session(monkeypatch):
    -    """Two connects with the same ?attach= token hit one spawned bridge."""
    -    spawned = []
    +class FakeBridge:
    +    def __init__(self):
    +        self.alive = True
    +
    +    def read(self, timeout):
    +        return b""        # idle forever
     
    -    class FakeBridge:
    -        def __init__(self):
    -            self.alive = True
    +    def write(self, data):
    +        pass
     
    -        def read(self, timeout):
    -            return b""        # idle forever
    +    def resize(self, cols, rows):
    +        pass
     
    -        def write(self, data):
    -            pass
    +    def close(self):
    +        self.alive = False
     
    -        def resize(self, cols, rows):
    -            pass
     
    -        def close(self):
    -            self.alive = False
    [redacted:email]
    +def pty_keepalive_harness(monkeypatch):
    +    spawned = []
     
         def fake_spawn(argv, cwd=None, env=None):
             b = FakeBridge()
    -        spawned.append(b)
    +        spawned.append(argv)
             return b
     
         monkeypatch.setattr(web_server.PtyBridge, "spawn", staticmethod(fake_spawn))
    -    # bypass auth + argv resolution for the test
         monkeypatch.setattr(web_server, "_ws_auth_reason", lambda ws: (None, "test"))
         monkeypatch.setattr(web_server, "_ws_host_origin_reason", lambda ws: None)
         monkeypatch.setattr(web_server, "_ws_client_reason", lambda ws: None)
     
         async def fake_argv(**kw):
    -        return (["x"], "/tmp", {})
    +        resume = "child" if kw.get("resume") == "parent" else kw.get("resume")
    +        env = {"HERMES_TUI_RESUME": resume} if resume else {}
    +        return (["x", resume or "fresh"], "/tmp", env)
     
         monkeypatch.setattr(web_server, "_resolve_chat_argv_async", fake_argv)
     
    -    from starlette.testclient import TestClient
    -
         try:
    -        client = TestClient(web_server.app)
    -        with client.websocket_connect("/api/pty?attach=TOK1") as ws1:
    -            ws1.send_bytes(b"hi")
    -        with client.websocket_connect("/api/pty?attach=TOK1") as ws2:
    -            ws2.send_bytes(b"again")
    -        assert len(spawned) == 1                # reattached, did not respawn
    +        yield spawned
         finally:
             web_server.PTY_REGISTRY._sessions.clear()
    +
    +
    [redacted:email]
    +async def test_attach_token_reuses_same_session(pty_keepalive_harness):
    +    """Two connects with the same ?attach= token hit one spawned bridge."""
    +    from starlette.testclient import TestClient
    +
    +    client = TestClient(web_server.app)
    +    with client.websocket_connect("/api/pty?attach=TOK1") as ws1:
    +        ws1.send_bytes(b"hi")
    +    with client.websocket_connect("/api/pty?attach=TOK1") as ws2:
    +        ws2.send_bytes(b"again")
    +    assert len(pty_keepalive_harness) == 1                # reattached, did not respawn
    +
    +
    [redacted:email]
    +async def test_attach_token_reuses_same_resume(pty_keepalive_harness):
    +    from starlette.testclient import TestClient
    +
    +    client = TestClient(web_server.app)
    +    with client.websocket_connect("/api/pty?attach=TOK1&resume=same") as ws1:
    +        ws1.send_bytes(b"hi")
    +    with client.websocket_connect("/api/pty?attach=TOK1&resume=same") as ws2:
    +        ws2.send_bytes(b"again")
    +    assert pty_keepalive_harness == [["x", "same"]]
    +
    +
    [redacted:email]
    +async def test_attach_token_does_not_reuse_different_resume(pty_keepalive_harness):
    +    """A keep-alive token must not pin /chat to an old selected session."""
    +    from starlette.testclient import TestClient
    +
    +    client = TestClient(web_server.app)
    +    with client.websocket_connect("/api/pty?attach=TOK1&resume=old") as ws1:
    +        ws1.send_bytes(b"hi")
    +    with client.websocket_connect("/api/pty?attach=TOK1&resume=new") as ws2:
    +        ws2.send_bytes(b"again")
    +    assert pty_keepalive_harness == [["x", "old"], ["x", "new"]]
    +
    +
    [redacted:email]
    +async def test_attach_token_reuses_canonical_resume(pty_keepalive_harness):
    +    from starlette.testclient import TestClient
    +
    +    client = TestClient(web_server.app)
    +    with client.websocket_connect("/api/pty?attach=TOK1&resume=parent") as ws1:
    +        ws1.send_bytes(b"hi")
    +    with client.websocket_connect("/api/pty?attach=TOK1&resume=child") as ws2:
    +        ws2.send_bytes(b"again")
    +    assert pty_keepalive_harness == [["x", "child"]]
    +
    +
    [redacted:email]
    +async def test_attach_token_does_not_reuse_different_profile(pty_keepalive_harness):
    +    from starlette.testclient import TestClient
    +
    +    client = TestClient(web_server.app)
    +    with client.websocket_connect("/api/pty?attach=TOK1&profile=alpha") as ws1:
    +        ws1.send_bytes(b"hi")
    +    with client.websocket_connect("/api/pty?attach=TOK1&profile=beta") as ws2:
    +        ws2.send_bytes(b"again")
    +    assert len(pty_keepalive_harness) == 2
    +
    +
    [redacted:email]
    +async def test_attach_token_reuses_default_chat_after_active_session_fallback(
    +    pty_keepalive_harness, tmp_path, monkeypatch
    +):
    +    from starlette.testclient import TestClient
    +
    +    active_session_file = tmp_path / "active-session.json"
    +    monkeypatch.setattr(
    +        web_server,
    +        "_active_session_file_for_channel",
    +        lambda app, channel: active_session_file,
    +    )
    +
    +    client = TestClient(web_server.app)
    +    with client.websocket_connect("/api/pty?attach=TOK1&channel=CHAT") as ws1:
    +        ws1.send_bytes(b"hi")
    +
    +    active_session_file.write_text(json.dumps({"session_id": "existing"}))
    +
    +    with client.websocket_connect("/api/pty?attach=TOK1&channel=CHAT") as ws2:
    +        ws2.send_bytes(b"again")
    +
    +    assert pty_keepalive_harness == [["x", "fresh"]]
    ```
- **#60458: Propagate dashboard auth identity to TUI agents**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `agent/agent_init.py`, `agent/conversation_loop.py`, `hermes_cli/dashboard_auth/routes.py`, `hermes_cli/dashboard_auth/ws_tickets.py`, `hermes_cli/web_server.py`, `tests/agent/test_agent_init_dashboard_identity.py`, `tests/agent/test_conversation_loop_identity.py`, `tests/hermes_cli/test_dashboard_auth_ws_auth.py`, `tests/hermes_cli/test_dashboard_auth_ws_tickets.py`, `tests/hermes_cli/test_web_server.py`, `tests/test_tui_gateway_server.py`, `tui_gateway/server.py`, `tui_gateway/ws.py`
- **#62563: fix: propagate dashboard auth user identity to agent construction (#62549)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#62549`, `#60458`, `#62563`
- **#62629: fix(dashboard): propagate authenticated user_id through PTY spawn to agent (#62549)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#62563`, `#62549`, `#60458`
- **#62877: fix(dashboard): propagate auth identity to agent construction**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`
- **#63217: fix(dashboard): propagate auth user identity to agent construction (#62549)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`

## Fix status

- Merged to main in PR #60745 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-16`.
- Subsystem: `dashboard`
