From 75b1b43ec19f156070aa297e7ebbcbbb97220f4f Mon Sep 17 00:00:00 2001 From: KoNit-K <124019182+KoNit-K@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:20:58 +0800 Subject: [PATCH] fix(tui-gateway): reject sessionless config.set model with 4001 Sessionless model switches (including legacy --global) could persist profile defaults before session.create; fail closed unless --once. Co-authored-by: Cursor (cherry picked from commit 1c4a29757bbde1304404f26b45b6becb841c069f) --- tests/test_tui_gateway_server.py | 55 +++++++++++++++++++++++++++++++ tui_gateway/methods_config_set.py | 8 ++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 4a15d7b257..54e29465fb 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -10316,6 +10316,61 @@ def test_config_set_model_once_requires_live_session(monkeypatch): assert "/model --once requires a live session" in resp["error"]["message"] +def test_config_set_model_sessionless_rejected(monkeypatch): + """Sessionless config.set model must 4001 before _apply_model_switch. + + Missing session_id and a stale session_id miss both take the sessionless + branch; unscoped values and legacy --global must be rejected the same way + so a Desktop client cannot persist model.default before session.create. + """ + called = {"n": 0} + + def boom(*a, **k): + called["n"] += 1 + raise AssertionError("_apply_model_switch must not run") + + monkeypatch.setattr(server, "_apply_model_switch", boom) + for value in ["some-model", "some-model --provider openai-codex --global"]: + resp = server.handle_request({ + "id": "1", "method": "config.set", + "params": {"key": "model", "value": value}, + }) + assert resp["error"]["code"] == 4001 + assert called["n"] == 0 + + resp = server.handle_request({ + "id": "1", "method": "config.set", + "params": {"session_id": "missing-sid", "key": "model", "value": "some-model --global"}, + }) + assert resp["error"]["code"] == 4001 + assert called["n"] == 0 + + +def test_config_set_model_live_session_still_applies_switch(monkeypatch): + """CONTROL: a live session still reaches _apply_model_switch, including --global.""" + called = {"raw": []} + + def fake_apply(sid, session, raw, **_kwargs): + called["raw"].append(raw) + return {"value": "some-model", "warning": "", "scope": "global"} + + server._sessions["sid"] = _session() + monkeypatch.setattr(server, "_apply_model_switch", fake_apply) + try: + resp = server.handle_request({ + "id": "1", "method": "config.set", + "params": { + "session_id": "sid", + "key": "model", + "value": "some-model --provider openai-codex --global", + }, + }) + assert "error" not in resp + assert called["raw"] == ["some-model --provider openai-codex --global"] + finally: + server._sessions.pop("sid", None) + + def test_config_set_model_session_switch_clears_pending_once_restore(monkeypatch): class Agent: model = "temp/model" diff --git a/tui_gateway/methods_config_set.py b/tui_gateway/methods_config_set.py index cd16baaed3..cd749a7353 100644 --- a/tui_gateway/methods_config_set.py +++ b/tui_gateway/methods_config_set.py @@ -140,7 +140,13 @@ def _set_model(rid, params, key, value, session): with _session_profile_runtime_scope(session): _persist_live_session_runtime(session) else: - result = _apply_model_switch("", {"agent": None}, value, confirm_expensive_model=confirmed) + # --once keeps its specific 5001; other sessionless model sets 4001 so + # --global cannot persist profile defaults before session.create. + from hermes_cli.model_switch import parse_model_switch_args + if parse_model_switch_args(str(value)).is_once: + result = _apply_model_switch("", {"agent": None}, value, confirm_expensive_model=confirmed) + else: + return _err(rid, 4001, "config.set model requires a live session") return _kv(rid, key, result["value"], warning=result["warning"], confirm_required=result.get("confirm_required", False), confirm_message=result.get("confirm_message", ""), scope=result.get("scope", "session"))