feat(tui-gateway): build the agent with the authenticated dashboard user as user_id

the dashboard login already stamps {user_id, provider} on the websocket
at upgrade time and tui_gateway keeps it as WSTransport.auth_identity,
but _make_agent never read it. every dashboard and desktop session was
built with user_id=None, so memory providers saw no runtime user and fell
back to the configured peer, mixing all logins together (#89794).

_make_agent now reads the session transport's auth_identity and passes
its user_id to AIAgent, the kwarg gateway platforms already use. the
legacy ?token= path, stdio, and the server-internal credential the PTY
child connects with carry no human and pass None, so they keep resolving
to the configured peer. the change is provider-neutral: honcho and any
other memory provider receive the id through the existing initialize()
kwargs.
This commit is contained in:
Erosika
2026-09-04 15:31:18 -04:00
committed by kshitij
parent 3da6a80d50
commit c5973cd540
3 changed files with 68 additions and 3 deletions
+52
View File
@@ -4549,6 +4549,58 @@ def test_make_agent_passes_configured_fallback_chain(monkeypatch):
assert captured["platform"] == "tui"
def _capture_make_agent_kwargs(monkeypatch) -> dict:
"""Stub AIAgent so ``server._make_agent`` records the kwargs it was built with."""
captured = {}
def fake_agent(**kwargs):
captured.update(kwargs)
return types.SimpleNamespace(model=kwargs.get("model"))
_setup_make_agent_mocks(monkeypatch, {})
monkeypatch.setattr("run_agent.AIAgent", fake_agent)
return captured
def test_make_agent_passes_the_authenticated_dashboard_user_as_user_id(monkeypatch):
"""The identity stamped at WS-upgrade auth (WSTransport.auth_identity) reaches the agent as
``user_id``, the same kwarg gateways pass, so memory providers scope memory to the login (#89794)."""
captured = _capture_make_agent_kwargs(monkeypatch)
transport = types.SimpleNamespace(auth_identity={"user_id": "oidc|abc123", "provider": "oidc"})
monkeypatch.setitem(server._sessions, "sid-auth", {"session_key": "k", "transport": transport})
server._make_agent("sid-auth", "k")
assert captured["user_id"] == "oidc|abc123"
@pytest.mark.parametrize("identity", [
None,
{"user_id": "server-internal", "provider": "server-internal"},
{"user_id": "", "provider": "oidc"},
{"user_id": "abc", "provider": ""},
])
def test_make_agent_passes_no_user_id_without_an_authenticated_human(monkeypatch, identity):
"""Legacy token, stdio and the PTY child's server-internal credential name no human; the
agent must not receive a user id memory would treat as a person."""
captured = _capture_make_agent_kwargs(monkeypatch)
transport = types.SimpleNamespace(auth_identity=identity)
monkeypatch.setitem(server._sessions, "sid-anon", {"session_key": "k", "transport": transport})
server._make_agent("sid-anon", "k")
assert captured["user_id"] is None
def test_make_agent_passes_no_user_id_for_an_unknown_session(monkeypatch):
captured = _capture_make_agent_kwargs(monkeypatch)
server._sessions.pop("sid-missing", None)
server._make_agent("sid-missing", "k")
assert captured["user_id"] is None
def test_background_agent_kwargs_preserves_full_fallback_chain(monkeypatch):
chain = [
{"provider": "openrouter", "model": "openai/gpt-5.5"},
+14 -2
View File
@@ -2255,6 +2255,15 @@ def _startup_system_prompt(cfg: dict, task_id: str) -> str:
return system_prompt
def _session_auth_user_id(session: dict | None) -> str | None:
"""User id the WS-upgrade credential authenticated for ``session``'s transport, or None for the legacy
token, stdio, and the server-internal credential the PTY child connects with."""
identity = getattr((session or {}).get("transport"), "auth_identity", None)
if _methods_browser_control._is_authenticated_identity(identity):
return str(identity["user_id"]).strip()
return None
def _make_agent(
sid: str, key: str, session_id: str | None = None, session_db=None,
model_override: dict | str | None = None, provider_override: str | None = None,
@@ -2280,6 +2289,8 @@ def _make_agent(
_pr = _load_provider_routing()
platform = _resolve_agent_platform(platform_override)
ignore_rules = is_truthy_value(os.environ.get("HERMES_IGNORE_RULES"))
with _sessions_lock:
session = _sessions.get(sid)
agent = AIAgent(
model=model, max_iterations=_cfg_max_turns(cfg, 500), provider=runtime.get("provider"),
base_url=runtime.get("base_url"), api_key=runtime.get("api_key"), api_mode=runtime.get("api_mode"),
@@ -2294,14 +2305,15 @@ def _make_agent(
providers_allowed=_pr.get("only"), providers_ignored=_pr.get("ignore"), providers_order=_pr.get("order"),
provider_sort=_pr.get("sort"), provider_require_parameters=_pr.get("require_parameters", False),
provider_data_collection=_pr.get("data_collection"), platform=platform, session_id=session_id or key,
# The dashboard login identity reaches memory providers as the runtime user, like a gateway user id.
user_id=_session_auth_user_id(session),
session_db=session_db if session_db is not None else _get_db(), ephemeral_system_prompt=system_prompt or None,
checkpoints_enabled=is_truthy_value(os.environ.get("HERMES_TUI_CHECKPOINTS")),
pass_session_id=is_truthy_value(os.environ.get("HERMES_TUI_PASS_SESSION_ID")),
skip_context_files=ignore_rules, skip_memory=ignore_rules, fallback_model=_load_fallback_model(),
**_agent_cbs(sid))
if context_cwd_is_launch_artifact is None:
with _sessions_lock:
context_cwd_is_launch_artifact = _context_cwd_is_launch_artifact(_sessions.get(sid))
context_cwd_is_launch_artifact = _context_cwd_is_launch_artifact(session)
agent._context_cwd_is_launch_artifact = bool(context_cwd_is_launch_artifact)
return agent
+2 -1
View File
@@ -93,7 +93,8 @@ class WSTransport:
self._loop = loop
self._peer = peer
#: Server-verified identity from the WS-upgrade credential, stamped by ``web_server._ws_auth_reason``; None
#: for legacy-token/stdio. RPC params can never populate it: sole identity authority for browser controllers.
#: for legacy-token/stdio. RPC params can never populate it: sole identity authority for browser controllers
#: and for the ``user_id`` the agent is built with (``server._session_auth_user_id``).
self.auth_identity = auth_identity
self._closed = False
# Token-coalescing buffer. The lock guards the buffer + "armed" flag against worker threads