{"repo":"NousResearch/hermes-agent","public_id":"35408","display_label":"Propagating dashboard auth user identity to agent sessions","summary":"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.","subsystem":"dashboard","counts":{"members":8,"open_issues":1,"open_issues_distinct":1,"open_prs":1,"closed_issues":1,"merged_prs":1,"attempt_prs":5},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":60745,"merged_at":"2026-07-16T23:48:48+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-07-16T23:48:48+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.22714285714285715},"fix_edges":[{"issue_number":61284,"pr_number":60745,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-16T23:48:48+00:00"},{"issue_number":64154,"pr_number":60745,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-16T23:48:48+00:00"}],"members":[{"kind":"issue","number":35408,"title":"[Feature]: Dashboard authenticated user identity should propagate to the agent session","url":null,"state":"closed"},{"kind":"issue","number":62549,"title":"[Bug]: Dashboard auth identity (`Session.user_id`) never reaches agent construction — memory providers silently fall back to the default user, breaking multi-user isolation","url":null,"state":"open"},{"kind":"pr","number":60458,"title":"Propagate dashboard auth identity to TUI agents","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":60745,"title":"fix(dashboard): scope chat attach tokens by session","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/web_server.py","tests/test_pty_keepalive_ws.py"],"fix_hunks":[{"file":"hermes_cli/web_server.py","hunks":["@@ -15149,7 +15149,8 @@ async def pty_ws(ws: WebSocket) -> None:\n         return\n \n     # --- spawn PTY ------------------------------------------------------\n-    resume = ws.query_params.get(\"resume\") or None\n+    raw_resume = ws.query_params.get(\"resume\") or None\n+    resume = raw_resume\n     profile = ws.query_params.get(\"profile\") or None\n     channel = _channel_or_close_code(ws)\n     sidecar_url = _build_sidecar_url(channel) if channel else None","@@ -15192,6 +15193,12 @@ async def pty_ws(ws: WebSocket) -> None:\n \n \n     attach_token = ws.query_params.get(\"attach\") or None\n+    registry_resume = raw_resume\n+    if raw_resume and env:\n+        registry_resume = env.get(\"HERMES_TUI_RESUME\") or raw_resume\n+    if attach_token is not None and (registry_resume or profile):\n+        # Key explicit resumes on their canonical target, never the active-session fallback.\n+        attach_token = f\"{attach_token}\\0{profile or ''}\\0{registry_resume or ''}\"\n \n     def _spawn():\n         return PtyBridge.spawn(argv, cwd=cwd, env=env)"]},{"file":"tests/test_pty_keepalive_ws.py","hunks":["@@ -1,53 +1,136 @@\n+import json\n+\n import pytest\n \n from hermes_cli import web_server\n \n \n[redacted:email]\n-async def test_attach_token_reuses_same_session(monkeypatch):\n-    \"\"\"Two connects with the same ?attach= token hit one spawned bridge.\"\"\"\n-    spawned = []\n+class FakeBridge:\n+    def __init__(self):\n+        self.alive = True\n+\n+    def read(self, timeout):\n+        return b\"\"        # idle forever\n \n-    class FakeBridge:\n-        def __init__(self):\n-            self.alive = True\n+    def write(self, data):\n+        pass\n \n-        def read(self, timeout):\n-            return b\"\"        # idle forever\n+    def resize(self, cols, rows):\n+        pass\n \n-        def write(self, data):\n-            pass\n+    def close(self):\n+        self.alive = False\n \n-        def resize(self, cols, rows):\n-            pass\n \n-        def close(self):\n-            self.alive = False\n[redacted:email]\n+def pty_keepalive_harness(monkeypatch):\n+    spawned = []\n \n     def fake_spawn(argv, cwd=None, env=None):\n         b = FakeBridge()\n-        spawned.append(b)\n+        spawned.append(argv)\n         return b\n \n     monkeypatch.setattr(web_server.PtyBridge, \"spawn\", staticmethod(fake_spawn))\n-    # bypass auth + argv resolution for the test\n     monkeypatch.setattr(web_server, \"_ws_auth_reason\", lambda ws: (None, \"test\"))\n     monkeypatch.setattr(web_server, \"_ws_host_origin_reason\", lambda ws: None)\n     monkeypatch.setattr(web_server, \"_ws_client_reason\", lambda ws: None)\n \n     async def fake_argv(**kw):\n-        return ([\"x\"], \"/tmp\", {})\n+        resume = \"child\" if kw.get(\"resume\") == \"parent\" else kw.get(\"resume\")\n+        env = {\"HERMES_TUI_RESUME\": resume} if resume else {}\n+        return ([\"x\", resume or \"fresh\"], \"/tmp\", env)\n \n     monkeypatch.setattr(web_server, \"_resolve_chat_argv_async\", fake_argv)\n \n-    from starlette.testclient import TestClient\n-\n     try:\n-        client = TestClient(web_server.app)\n-        with client.websocket_connect(\"/api/pty?attach=TOK1\") as ws1:\n-            ws1.send_bytes(b\"hi\")\n-        with client.websocket_connect(\"/api/pty?attach=TOK1\") as ws2:\n-            ws2.send_bytes(b\"again\")\n-        assert len(spawned) == 1                # reattached, did not respawn\n+        yield spawned\n     finally:\n         web_server.PTY_REGISTRY._sessions.clear()\n+\n+\n[redacted:email]\n+async def test_attach_token_reuses_same_session(pty_keepalive_harness):\n+    \"\"\"Two connects with the same ?attach= token hit one spawned bridge.\"\"\"\n+    from starlette.testclient import TestClient\n+\n+    client = TestClient(web_server.app)\n+    with client.websocket_connect(\"/api/pty?attach=TOK1\") as ws1:\n+        ws1.send_bytes(b\"hi\")\n+    with client.websocket_connect(\"/api/pty?attach=TOK1\") as ws2:\n+        ws2.send_bytes(b\"again\")\n+    assert len(pty_keepalive_harness) == 1                # reattached, did not respawn\n+\n+\n[redacted:email]\n+async def test_attach_token_reuses_same_resume(pty_keepalive_harness):\n+    from starlette.testclient import TestClient\n+\n+    client = TestClient(web_server.app)\n+    with client.websocket_connect(\"/api/pty?attach=TOK1&resume=same\") as ws1:\n+        ws1.send_bytes(b\"hi\")\n+    with client.websocket_connect(\"/api/pty?attach=TOK1&resume=same\") as ws2:\n+        ws2.send_bytes(b\"again\")\n+    assert pty_keepalive_harness == [[\"x\", \"same\"]]\n+\n+\n[redacted:email]\n+async def test_attach_token_does_not_reuse_different_resume(pty_keepalive_harness):\n+    \"\"\"A keep-alive token must not pin /chat to an old selected session.\"\"\"\n+    from starlette.testclient import TestClient\n+\n+    client = TestClient(web_server.app)\n+    with client.websocket_connect(\"/api/pty?attach=TOK1&resume=old\") as ws1:\n+        ws1.send_bytes(b\"hi\")\n+    with client.websocket_connect(\"/api/pty?attach=TOK1&resume=new\") as ws2:\n+        ws2.send_bytes(b\"again\")\n+    assert pty_keepalive_harness == [[\"x\", \"old\"], [\"x\", \"new\"]]\n+\n+\n[redacted:email]\n+async def test_attach_token_reuses_canonical_resume(pty_keepalive_harness):\n+    from starlette.testclient import TestClient\n+\n+    client = TestClient(web_server.app)\n+    with client.websocket_connect(\"/api/pty?attach=TOK1&resume=parent\") as ws1:\n+        ws1.send_bytes(b\"hi\")\n+    with client.websocket_connect(\"/api/pty?attach=TOK1&resume=child\") as ws2:\n+        ws2.send_bytes(b\"again\")\n+    assert pty_keepalive_harness == [[\"x\", \"child\"]]\n+\n+\n[redacted:email]\n+async def test_attach_token_does_not_reuse_different_profile(pty_keepalive_harness):\n+    from starlette.testclient import TestClient\n+\n+    client = TestClient(web_server.app)\n+    with client.websocket_connect(\"/api/pty?attach=TOK1&profile=alpha\") as ws1:\n+        ws1.send_bytes(b\"hi\")\n+    with client.websocket_connect(\"/api/pty?attach=TOK1&profile=beta\") as ws2:\n+        ws2.send_bytes(b\"again\")\n+    assert len(pty_keepalive_harness) == 2\n+\n+\n[redacted:email]\n+async def test_attach_token_reuses_default_chat_after_active_session_fallback(\n+    pty_keepalive_harness, tmp_path, monkeypatch\n+):\n+    from starlette.testclient import TestClient\n+\n+    active_session_file = tmp_path / \"active-session.json\"\n+    monkeypatch.setattr(\n+        web_server,\n+        \"_active_session_file_for_channel\",\n+        lambda app, channel: active_session_file,\n+    )\n+\n+    client = TestClient(web_server.app)\n+    with client.websocket_connect(\"/api/pty?attach=TOK1&channel=CHAT\") as ws1:\n+        ws1.send_bytes(b\"hi\")\n+\n+    active_session_file.write_text(json.dumps({\"session_id\": \"existing\"}))\n+\n+    with client.websocket_connect(\"/api/pty?attach=TOK1&channel=CHAT\") as ws2:\n+        ws2.send_bytes(b\"again\")\n+\n+    assert pty_keepalive_harness == [[\"x\", \"fresh\"]]\n"]}]},{"kind":"pr","number":62563,"title":"fix: propagate dashboard auth user identity to agent construction (#62549)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#62549","number":62549},{"kind":"pr","value":"#60458","number":60458},{"kind":"pr","value":"#62563","number":62563}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":62629,"title":"fix(dashboard): propagate authenticated user_id through PTY spawn to agent (#62549)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#62563","number":62563},{"kind":"pr","value":"#62549","number":62549},{"kind":"pr","value":"#60458","number":60458}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":62877,"title":"fix(dashboard): propagate auth identity to agent construction","url":null,"state":"closed","close_reason":{"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":63217,"title":"fix(dashboard): propagate auth user identity to agent construction (#62549)","url":null,"state":"closed","close_reason":{"category":"duplicate_of","implication":"duplicate"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}