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:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user