4beb7e29a2
A gateway with an OAuth MCP server whose refresh token expired opened a new authorize tab every 300s, all night (92 tabs). Four defects stacked: - The parked-server self-probe re-entered the SDK's authorization-code flow with interactive OAuth enabled. The timed wake is unattended by definition: `_wait_for_reconnect_or_shutdown` now distinguishes "self-probe" from an explicit "reconnect", and `_park` flips the task-local `_oauth_interactive_enabled` off before a self-probe revival. - Gateway MCP discovery (startup, `/reload-mcp`, hot-added multiplex profiles) ran interactive, unlike the CLI's background discovery. All three now run under `suppress_interactive_oauth()`; an expired token parks with the `hermes mcp login` hint instead of a browser. - `_is_interactive()` trusted `sys.stdin.isatty()`, which the Windows CRT reports True for a DEVNULL/detached stdin. `_stdin_is_console()` confirms with `GetConsoleMode` on Windows. - Removing an `mcp_servers` entry (or `enabled: false`) never reached a running gateway; the parked server probed forever. New `reconcile_mcp_servers_with_config()` tears down dropped/disabled servers (via `shutdown_mcp_servers(names=...)`) and connects new ones; a housekeeping chore runs it when config.yaml's (mtime, size) changes. `_select_new_servers` also stops nudging disabled parked servers. Fixes #81830. Fixes the browser-storm item of #96320.
28 lines
1.1 KiB
Python
28 lines
1.1 KiB
Python
"""``_is_interactive()`` must not trust ``isatty()`` alone on Windows: the CRT reports a DEVNULL /
|
|
detached stdin as a TTY, so the background gateway looked interactive and launched browser OAuth."""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.windows_only
|
|
def test_devnull_stdin_is_not_a_console_on_windows():
|
|
code = ("import sys, os; sys.path.insert(0, os.getcwd()); "
|
|
"from tools.mcp_oauth import _stdin_is_console; print(_stdin_is_console(), sys.stdin.isatty())")
|
|
proc = subprocess.run([sys.executable, "-c", code], stdin=subprocess.DEVNULL,
|
|
capture_output=True, text=True, cwd=os.getcwd(), timeout=60)
|
|
console, isatty = proc.stdout.split()
|
|
assert isatty == "True", "premise: the Windows CRT calls DEVNULL a tty (else this guard is moot)"
|
|
assert console == "False"
|
|
|
|
|
|
def test_non_tty_stdin_is_not_interactive(monkeypatch):
|
|
import io
|
|
from tools import mcp_oauth
|
|
monkeypatch.setattr(mcp_oauth.sys, "stdin", io.StringIO())
|
|
assert mcp_oauth._stdin_is_console() is False
|
|
assert mcp_oauth._is_interactive() is False
|