Files
hermes-agent/tests/tools/test_mcp_oauth_stdin_console.py
T
Teknium 4beb7e29a2 fix(mcp): unattended paths never open browser OAuth; gateway follows mcp_servers edits
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.
2026-09-13 06:29:12 -07:00

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