mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
test(webapp): each host's launch-link path is tested on that host, without pretending to be macOS
The launch tests made the interpreter believe it was on another OS: they set sys.platform to
freebsd14, openbsd7, sunos5 or win32, and replaced the launch-link check and the browser peer check
with lambdas marked "macOS". The full-gateway fixture did the same for its macOS run. So the Windows
path of the real functions was never run on Windows (no file was on the Windows lane), and the macOS
detection only once, in one handoff test.
The two decisions take the platform as data now (a keyword defaulting to sys.platform), and
register_control_verbs takes whether launch links can be checked (None asks the host, as before).
Unmarked tests pass each platform in as data; platforms("windows") and platforms("macos") tests run
the real launch verb and handoff route on those hosts, and a platforms("linux") test runs the real
peer check on a real loopback connection, reading /proc/net/tcp. The fixture's macOS run tells the
gateway its links cannot be checked through that parameter, so the printed link is still exercised
on every host. The handoff test for printed links now asserts the peer check is never consulted.
Behaviour is unchanged.
This commit is contained in:
1 parent
facbbb2cd8
commit
a9f81666f4
5 files changed
+83
-33
No files matched your search
@@ -280,7 +280,8 @@ def _close_to_other_users(folder: Path) -> None:
|
||||
|
||||
|
||||
def _one_use_links_work(platform: str = sys.platform) -> bool:
|
||||
"""Whether a browser the server opens can redeem a one-use launch link here. Redemption needs
|
||||
"""Whether a browser the server opens can redeem a one-use launch link on *platform* (a
|
||||
``sys.platform`` value, this host's by default). Redemption needs
|
||||
proof that the browser's connection belongs to the gateway's OS user
|
||||
(``web_routers.webapp._peer_is_server_user``) on Linux, or a browser launcher whose arguments other
|
||||
users cannot read (Windows: no connection is checked there). macOS and the BSDs offer neither, and
|
||||
@@ -326,7 +327,7 @@ STARTING = {"state": "starting", "url": None, "reason": None}
|
||||
_STILL_STARTING = "The Webapp is still starting; try again in a moment."
|
||||
|
||||
|
||||
def register_control_verbs(runner, home, server=None) -> None:
|
||||
def register_control_verbs(runner, home, server=None, *, links_checked: bool | None = None) -> None:
|
||||
"""The private control-socket verbs ``hermes webapp`` uses on *server* (the runner's control
|
||||
server by default); only an authenticated same-user peer reaches them. The gateway registers them
|
||||
before its control socket answers, so they answer from its first moment: :data:`STARTING` until
|
||||
@@ -334,7 +335,11 @@ def register_control_verbs(runner, home, server=None) -> None:
|
||||
the credential crosses neither the socket nor a browser's argv; where nothing can check who
|
||||
redeems such a link (macOS, the BSDs, WSL) it is marked private, for the user to open by hand.
|
||||
``webapp-sign-out`` replaces the browser credential, which also ends every login bound to it
|
||||
behind the sign-in gate, closes the browser sockets those opened, and reports how many it closed."""
|
||||
behind the sign-in gate, closes the browser sockets those opened, and reports how many it closed.
|
||||
|
||||
*links_checked* is whether such a link can be checked here; None asks this host
|
||||
(:func:`_one_use_links_work`) at each launch. Tests pass False to take the printed-link path on
|
||||
any host, as the gateway does on macOS, the BSDs and WSL."""
|
||||
server = server if server is not None else getattr(runner, "session_control_server", None)
|
||||
if server is None:
|
||||
return
|
||||
@@ -391,7 +396,7 @@ def register_control_verbs(runner, home, server=None) -> None:
|
||||
# Where the browser's connection cannot be checked (macOS, the BSDs, WSL), the link is for the user
|
||||
# to open by hand ("private": printed, never passed to a browser launcher), and its ticket spends
|
||||
# without that check. Either way it works once, for 60 seconds, and carries no lasting credential.
|
||||
checked = _one_use_links_work()
|
||||
checked = _one_use_links_work() if links_checked is None else links_checked
|
||||
ticket = mint_launch_ticket(web.app.state) if checked else mint_printed_launch_ticket(web.app.state)
|
||||
fragment = urlencode({"ticket": ticket, **({"query": query} if query else {})})
|
||||
return {"url": f"{status['url']}/webapp/launch#{fragment}", "private": not checked,
|
||||
|
||||
@@ -131,7 +131,7 @@ def _loopback_peer_uid(client: tuple[str, int], server: tuple[str, int]) -> int
|
||||
|
||||
def _peer_is_server_user(request: Request, platform: str = sys.platform) -> bool:
|
||||
"""Whether a launch ticket that crossed a browser launcher's argv may be spent over this
|
||||
request's connection.
|
||||
request's connection on *platform* (a ``sys.platform`` value, this host's by default).
|
||||
|
||||
Linux checks that the connection belongs to the OS user running the server. Windows checks
|
||||
nothing and always says yes: the protection there is that the ticket travels only in the
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Start the gateway listener with the Webapp on or off; report what a browser and a native client get.
|
||||
|
||||
Run as ``python webapp_listener_peer.py on|off <port> <dist> [macos]`` with an isolated ``HERMES_HOME``;
|
||||
``macos`` takes the launch path of macOS and the BSDs on any host. Writes
|
||||
``macos`` takes the launch path of macOS and the BSDs (the printed link) on any host. Writes
|
||||
``receipt.json`` there (the gateway redirects stdout). No external inference: the model points at a closed
|
||||
port and no turn is submitted.
|
||||
"""
|
||||
@@ -15,15 +15,17 @@ import traceback
|
||||
SEEDED_TOKEN = "seeded-browser-credential-from-an-earlier-0" # 43 URL-safe characters
|
||||
|
||||
|
||||
def follow_launch_path(platform: str) -> None:
|
||||
"""Have this gateway take *platform*'s launch path: ``macos`` stands in for macOS and the BSDs,
|
||||
where nothing identifies who owns the browser's connection; anything else keeps the host's own."""
|
||||
if platform == "macos":
|
||||
from gateway import webapp_listener
|
||||
from hermes_cli.web_routers import webapp
|
||||
def follow_launch_path(runner, home, platform: str) -> bool:
|
||||
"""Have this gateway take *platform*'s launch path and say whether its launch links are checked.
|
||||
``macos`` takes the path of macOS and the BSDs, where nothing identifies who owns the browser's
|
||||
connection: the gateway is told so through ``register_control_verbs(links_checked=False)``, and its
|
||||
link is printed for the user to open by hand. Anything else keeps the host's own path."""
|
||||
from gateway import webapp_listener
|
||||
|
||||
webapp_listener._one_use_links_work = lambda: False
|
||||
webapp._peer_is_server_user = lambda _request: False
|
||||
if platform == "macos":
|
||||
webapp_listener.register_control_verbs(runner, home, links_checked=False)
|
||||
return False
|
||||
return webapp_listener._one_use_links_work()
|
||||
|
||||
|
||||
async def _upgrade_refusal(url, **kwargs):
|
||||
@@ -102,7 +104,6 @@ async def probe(mode, port, dist, platform):
|
||||
import httpx
|
||||
from websockets.asyncio.client import connect
|
||||
|
||||
from gateway import webapp_listener
|
||||
from gateway.control_socket import GatewayControlServer
|
||||
from gateway.run import GatewayRunner
|
||||
from gateway.run_api import stop_gateway_api
|
||||
@@ -112,7 +113,6 @@ async def probe(mode, port, dist, platform):
|
||||
import hermes_cli.webapp
|
||||
from tests.gateway.fixtures.local_recovery_probe import rpc
|
||||
|
||||
follow_launch_path(platform)
|
||||
home = Path(os.environ["HERMES_HOME"])
|
||||
webapp_block = f"webapp:\n enabled: true\n port: {port}\n" if mode == "on" else ""
|
||||
(home / "config.yaml").write_text(
|
||||
@@ -131,6 +131,7 @@ async def probe(mode, port, dist, platform):
|
||||
"served_profiles": []}
|
||||
await initialize_session_authority(runner, profile_id=str(home), instance_id="webapp-listener")
|
||||
await start_gateway_runtime_api(runner)
|
||||
links_checked = follow_launch_path(runner, home, platform)
|
||||
api = runner.session_api
|
||||
from hermes_cli import web_server
|
||||
|
||||
@@ -167,7 +168,7 @@ async def probe(mode, port, dist, platform):
|
||||
url = launched["result"]["url"]
|
||||
report["launch_page"] = url.split("#", 1)[0] == origin + "/webapp/launch"
|
||||
report["launch_private"] = launched["result"]["private"]
|
||||
report["launch_printed_for_the_user"] = not webapp_listener._one_use_links_work()
|
||||
report["launch_printed_for_the_user"] = not links_checked
|
||||
ticket = url.split("#ticket=", 1)[1]
|
||||
handoff = {"X-Hermes-Window-Ticket": ticket, "Origin": origin}
|
||||
redeemed = await client.post("/webapp/window-session", headers=handoff)
|
||||
|
||||
@@ -218,13 +218,13 @@ def test_webapp_verbs_answer_only_an_authenticated_same_user_peer(monkeypatch, t
|
||||
|
||||
|
||||
def test_where_one_use_links_cannot_be_checked_the_launcher_gets_one_to_open_by_hand(monkeypatch, tmp_path):
|
||||
"""Where the server cannot tell who redeems a launch link (macOS, the BSDs), the launcher still gets
|
||||
"""Where the server cannot tell who redeems a launch link (macOS, the BSDs, WSL), the launcher still gets
|
||||
a one-use link that lasts 60 seconds, never the long-lived browser credential: it spends without a
|
||||
peer check, as a window ticket does, and the launcher prints it rather than opening it."""
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
monkeypatch.setattr(webapp, "_one_use_links_work", lambda: False) # macOS
|
||||
runner, _, _ = _start_runtime_api(monkeypatch, tmp_path, "webapp:\n enabled: true\n port: 9555\n")
|
||||
runner, _, home = _start_runtime_api(monkeypatch, tmp_path, "webapp:\n enabled: true\n port: 9555\n")
|
||||
webapp.register_control_verbs(runner, home, links_checked=False) # as on macOS, the BSDs and WSL
|
||||
# The gateway mounts the dashboard app as the Webapp surface on a private (loopback) listener.
|
||||
web = SimpleNamespace(_SESSION_TOKEN="s" * 43, app=SimpleNamespace(state=SimpleNamespace(ui_surface="webapp")))
|
||||
runner.session_api.app = SimpleNamespace(web=web)
|
||||
@@ -332,8 +332,8 @@ def _serving_runner(monkeypatch, tmp_path):
|
||||
def test_a_launch_link_for_a_named_profile_opens_that_profile(monkeypatch, tmp_path):
|
||||
from urllib.parse import parse_qs
|
||||
runner = _serving_runner(monkeypatch, tmp_path)
|
||||
for checked in (True, False): # Linux and Windows; macOS and the BSDs
|
||||
monkeypatch.setattr(webapp, "_one_use_links_work", lambda checked=checked: checked)
|
||||
for checked in (True, False): # Linux and Windows; macOS, the BSDs and WSL
|
||||
webapp.register_control_verbs(runner, tmp_path / "home", links_checked=checked)
|
||||
url = _ask(runner, "webapp-launch", params={"profile": "work"})["result"]["url"]
|
||||
page, fragment = url.split("#", 1)
|
||||
assert page == "http://127.0.0.1:9555/webapp/launch"
|
||||
|
||||
@@ -71,9 +71,10 @@ def test_macos_launch_handoff_fails_closed_without_consuming_child_window_author
|
||||
|
||||
|
||||
def test_a_link_for_the_user_to_open_by_hand_spends_once_without_a_peer_check(monkeypatch):
|
||||
"""Where nothing identifies the connection's owner (macOS, the BSDs), ``hermes webapp`` prints a
|
||||
link for the user to open by hand. Its ticket spends without the peer check, once and within 60
|
||||
seconds, as a window ticket does; an argv launch ticket still never spends there."""
|
||||
"""Where nothing identifies the connection's owner (macOS, the BSDs, WSL), ``hermes webapp`` prints a
|
||||
link for the user to open by hand. On every host its ticket spends without consulting the peer
|
||||
check, once and within 60 seconds, as a window ticket does. (That an argv launch ticket never
|
||||
spends there is the platforms("macos") test above.)"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -85,7 +86,10 @@ def test_a_link_for_the_user_to_open_by_hand_spends_once_without_a_peer_check(mo
|
||||
monkeypatch.setattr(server.app.state, "bound_host", "127.0.0.1", raising=False)
|
||||
monkeypatch.setattr(server.app.state, "webapp_window_tickets", {}, raising=False)
|
||||
monkeypatch.setattr(server, "_SESSION_TOKEN", "a" * 43)
|
||||
monkeypatch.setattr(webapp, "_peer_is_server_user", lambda request: False) # macOS
|
||||
def peer_check(request):
|
||||
raise AssertionError("a printed launch ticket consulted the peer check")
|
||||
|
||||
monkeypatch.setattr(webapp, "_peer_is_server_user", peer_check)
|
||||
client = TestClient(server.app, base_url="http://127.0.0.1")
|
||||
|
||||
def redeem(ticket):
|
||||
@@ -95,7 +99,6 @@ def test_a_link_for_the_user_to_open_by_hand_spends_once_without_a_peer_check(mo
|
||||
printed = webapp.mint_printed_launch_ticket(server.app.state)
|
||||
assert redeem(printed).json() == {"token": "a" * 43}
|
||||
assert redeem(printed).status_code == 403
|
||||
assert redeem(webapp.mint_launch_ticket(server.app.state)).status_code == 403
|
||||
late = webapp.mint_printed_launch_ticket(server.app.state)
|
||||
now = webapp.time.monotonic()
|
||||
monkeypatch.setattr(webapp, "time", SimpleNamespace(monotonic=lambda: now + webapp.LAUNCH_TICKET_TTL_SECONDS + 1))
|
||||
@@ -110,12 +113,12 @@ def _loopback_request():
|
||||
scope={"server": ("127.0.0.1", 9120)})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", ["freebsd14", "openbsd7", "sunos5"])
|
||||
@pytest.mark.parametrize("platform", ["darwin", "freebsd14", "openbsd7", "sunos5"])
|
||||
def test_where_no_peer_check_exists_a_launch_ticket_is_never_spent(platform):
|
||||
"""Other Unixes show every process's argv to all local users by default and Hermes cannot tell
|
||||
who owns a loopback connection there. As on macOS, the launcher prints a link for the user to open
|
||||
by hand rather than passing a launch ticket to a browser launcher, and a launch ticket is never
|
||||
spent."""
|
||||
"""macOS and the other Unixes show every process's argv to all local users by default and Hermes
|
||||
cannot tell who owns a loopback connection there. So the launcher prints a link for the user to
|
||||
open by hand rather than passing a launch ticket to a browser launcher, and a launch ticket is
|
||||
never spent. The platform is passed in as data; the marked tests below run each host's own path."""
|
||||
from gateway import webapp_listener
|
||||
from hermes_cli.web_routers import webapp
|
||||
|
||||
@@ -126,7 +129,8 @@ def test_where_no_peer_check_exists_a_launch_ticket_is_never_spent(platform):
|
||||
def test_windows_spends_a_launch_ticket_without_a_peer_check():
|
||||
"""Windows does not show a process's command line to other standard users (reading it needs a
|
||||
handle the process's DACL grants only its own user, SYSTEM and elevated administrators), so the
|
||||
ticket in the browser launcher's argv stays the user's own, as the stored credential does."""
|
||||
ticket in the browser launcher's argv stays the user's own, as the stored credential does. The
|
||||
platform is passed in as data; the platforms("windows") test below runs the real path."""
|
||||
from gateway import webapp_listener
|
||||
from hermes_cli.web_routers import webapp
|
||||
|
||||
@@ -134,6 +138,46 @@ def test_windows_spends_a_launch_ticket_without_a_peer_check():
|
||||
assert webapp_listener._one_use_links_work(platform="win32") is True
|
||||
|
||||
|
||||
@pytest.mark.platforms("windows")
|
||||
def test_on_windows_the_launcher_opens_the_launch_link_and_its_browser_spends_it(tmp_path, monkeypatch):
|
||||
"""On Windows ``hermes webapp`` gets a launch link to open itself (not marked private), and the
|
||||
browser it opens spends the ticket without a peer check."""
|
||||
client, link = _launch_link(tmp_path, monkeypatch)
|
||||
assert link["private"] is False
|
||||
assert _spend(client, link).status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.platforms("macos")
|
||||
def test_on_macos_the_launch_link_is_printed_and_spends_without_a_peer_check(tmp_path, monkeypatch):
|
||||
"""On macOS nothing identifies who owns the browser's connection: ``hermes webapp`` gets a link
|
||||
marked private, to print for the user to open by hand, and it spends without the peer check."""
|
||||
client, link = _launch_link(tmp_path, monkeypatch)
|
||||
assert link["private"] is True
|
||||
assert _spend(client, link).status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.platforms("linux")
|
||||
def test_on_linux_the_peer_check_names_the_owner_of_a_real_loopback_connection():
|
||||
"""Linux reads who owns the browser's end of a loopback connection from ``/proc/net/tcp*``: a
|
||||
connection this process holds passes the peer check, and one to a port nobody listens on (no
|
||||
such connection) does not."""
|
||||
import socket
|
||||
from types import SimpleNamespace
|
||||
|
||||
from hermes_cli.web_routers import webapp
|
||||
|
||||
with socket.create_server(("127.0.0.1", 0)) as listener, \
|
||||
socket.create_connection(listener.getsockname()) as browser:
|
||||
served, _ = listener.accept()
|
||||
with served:
|
||||
host, port = browser.getsockname()
|
||||
own = SimpleNamespace(client=SimpleNamespace(host=host, port=port),
|
||||
scope={"server": listener.getsockname()})
|
||||
assert webapp._peer_is_server_user(own) is True
|
||||
unknown = SimpleNamespace(client=SimpleNamespace(host=host, port=port), scope={"server": (host, 1)})
|
||||
assert webapp._peer_is_server_user(unknown) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("page", ["/webapp/launch", "/webapp/window"])
|
||||
def test_a_handoff_page_keeps_the_credential_out_of_every_url(monkeypatch, page):
|
||||
"""The browser records every address a tab navigates to in its history (and syncs it), and the
|
||||
|
||||
Reference in new issue
Block a user