Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui

# Conflicts:
#	tui_gateway/server.py
This commit is contained in:
Brooklyn Nicholson
2026-05-30 13:19:27 -05:00
157 changed files with 10059 additions and 831 deletions
@@ -80,6 +80,25 @@ def loopback_app():
web_server.app.state.auth_required = prev_required
@pytest.fixture
def insecure_public_app():
"""web_server.app configured for all-interfaces insecure mode."""
_reset_for_tests()
clear_providers()
prev_host = getattr(web_server.app.state, "bound_host", None)
prev_port = getattr(web_server.app.state, "bound_port", None)
prev_required = getattr(web_server.app.state, "auth_required", None)
web_server.app.state.bound_host = "0.0.0.0"
web_server.app.state.bound_port = 9120
web_server.app.state.auth_required = False
client = TestClient(web_server.app, base_url="http://192.168.0.222:9120")
yield client
_reset_for_tests()
web_server.app.state.bound_host = prev_host
web_server.app.state.bound_port = prev_port
web_server.app.state.auth_required = prev_required
def _logged_in(client: TestClient) -> None:
"""Drive the stub OAuth round trip so the client holds session cookies."""
r1 = client.get("/auth/login?provider=stub", follow_redirects=False)
@@ -143,6 +162,30 @@ class TestWsTicketEndpoint:
# ---------------------------------------------------------------------------
@pytest.fixture
def insecure_explicit_host_app():
"""web_server.app bound to an explicit non-loopback host (--insecure).
Models `--host 100.64.0.10 --insecure` (e.g. a Tailscale IP behind
`tailscale serve`) — a specific address rather than the all-interfaces
0.0.0.0 wildcard.
"""
_reset_for_tests()
clear_providers()
prev_host = getattr(web_server.app.state, "bound_host", None)
prev_port = getattr(web_server.app.state, "bound_port", None)
prev_required = getattr(web_server.app.state, "auth_required", None)
web_server.app.state.bound_host = "100.64.0.10"
web_server.app.state.bound_port = 9119
web_server.app.state.auth_required = False
client = TestClient(web_server.app, base_url="http://100.64.0.10:9119")
yield client
_reset_for_tests()
web_server.app.state.bound_host = prev_host
web_server.app.state.bound_port = prev_port
web_server.app.state.auth_required = prev_required
def _fake_ws(*, query: dict, client_host: str = "127.0.0.1", path: str = "/api/pty"):
"""Build a stand-in for starlette.WebSocket good enough for _ws_auth_ok."""
@@ -281,6 +324,48 @@ class TestWsRequestIsAllowedGated:
ws.headers = {"host": "127.0.0.1:8080"}
assert web_server._ws_request_is_allowed(ws) is True
def test_non_loopback_peer_allowed_in_insecure_public_mode(self, insecure_public_app):
"""`--host 0.0.0.0 --insecure` is an explicit LAN/public opt-in.
Regression coverage for the dashboard `/chat` breakage where the
HTML shell loaded on 9120 but every WebSocket upgrade was rejected
with 403 because the loopback-only peer guard still ran even though
the operator intentionally exposed the dashboard on all interfaces.
"""
ws = _fake_ws(query={}, client_host="192.168.0.55")
ws.headers = {
"host": "192.168.0.222:9120",
"origin": "http://192.168.0.222:9120",
}
assert web_server._ws_request_is_allowed(ws) is True
def test_peer_allowed_on_explicit_non_loopback_bind(self, insecure_explicit_host_app):
"""`--host 100.64.0.10 --insecure` (Tailscale/LAN IP) is an explicit
non-loopback opt-in too — not just the 0.0.0.0 wildcard.
Regression coverage: the merged 0.0.0.0/:: fix did not cover binding
directly to a specific tailnet/LAN address, so `/chat` HTML loaded but
WS upgrades were still rejected by the loopback-only peer guard.
"""
ws = _fake_ws(query={}, client_host="100.64.0.99")
ws.headers = {
"host": "100.64.0.10:9119",
"origin": "http://100.64.0.10:9119",
}
assert web_server._ws_request_is_allowed(ws) is True
def test_rebinding_host_rejected_on_explicit_non_loopback_bind(
self, insecure_explicit_host_app
):
"""Lifting the peer-IP gate for an explicit bind must NOT lift the
DNS-rebinding Host guard: a mismatched Host header is still rejected,
because an explicit non-loopback bind requires an exact Host match in
`_is_accepted_host` (unlike the 0.0.0.0 wildcard, which accepts any).
"""
ws = _fake_ws(query={}, client_host="100.64.0.99")
ws.headers = {"host": "evil.example.com"}
assert web_server._ws_request_is_allowed(ws) is False
def test_host_origin_guard_still_runs_in_gated_mode(self, gated_app):
"""Bypassing the peer-IP check must not bypass the DNS-rebinding
Host header guard — that one still protects against attacker