fix(dashboard): require Desktop spawn credential for host bypass
This commit is contained in:
@@ -797,6 +797,7 @@ from hermes_cli.main_dashboard import (
|
||||
_finalize_update_output,
|
||||
_find_stale_dashboard_pids,
|
||||
_install_hangup_protection,
|
||||
_is_desktop_owned_backend,
|
||||
_is_electron_packaged_web_dist,
|
||||
_maybe_setup_dashboard_auth_interactively,
|
||||
_read_ssh_session_token_file,
|
||||
@@ -2607,10 +2608,7 @@ def _dashboard_sanitize_desktop_env(headless_backend) -> None:
|
||||
HERMES_DASHBOARD_SESSION_TOKEN, which the terminal pane never receives and
|
||||
the terminal tool's env policy strips from agent children.
|
||||
"""
|
||||
desktop_owned_child = (
|
||||
os.environ.get("HERMES_DESKTOP") == "1"
|
||||
and bool(os.environ.get("HERMES_DASHBOARD_SESSION_TOKEN"))
|
||||
)
|
||||
desktop_owned_child = _is_desktop_owned_backend()
|
||||
if (
|
||||
not headless_backend
|
||||
and not desktop_owned_child
|
||||
@@ -2689,7 +2687,7 @@ def _dashboard_prepare_runtime(args, headless_backend) -> bool:
|
||||
# wait_for_mcp_discovery covers a server still connecting at first turn.
|
||||
# A standalone (non-Desktop) dashboard may sit idle and unvisited for days
|
||||
# (#58733): it arms discovery instead and the first /api/ws client fires it.
|
||||
desktop = os.environ.get("HERMES_DESKTOP") == "1"
|
||||
desktop = _is_desktop_owned_backend()
|
||||
if headless_backend and desktop:
|
||||
return True
|
||||
try:
|
||||
|
||||
@@ -751,6 +751,20 @@ def _is_electron_packaged_web_dist(path: str) -> bool:
|
||||
return "app.asar" in path.replace("\\", "/")
|
||||
|
||||
|
||||
def _is_desktop_owned_backend() -> bool:
|
||||
"""Whether this process is the backend that Desktop spawned and owns.
|
||||
|
||||
``HERMES_DESKTOP`` is a marker inherited by shells launched from Desktop;
|
||||
it is not ownership proof. Desktop gives only its backend a fresh session
|
||||
credential, so requiring both values keeps inherited terminal commands on
|
||||
the normal one-host-backend path.
|
||||
"""
|
||||
return (
|
||||
os.environ.get("HERMES_DESKTOP") == "1"
|
||||
and bool(os.environ.get("HERMES_DASHBOARD_SESSION_TOKEN"))
|
||||
)
|
||||
|
||||
|
||||
def _host_backend_attachment():
|
||||
"""Live host serve/dashboard record to attach to, or ``None``.
|
||||
|
||||
@@ -826,7 +840,7 @@ def _attach_to_host_backend(args, headless_backend: bool) -> None:
|
||||
|
||||
Returns normally — leaving the caller to BIND — when no owner answers.
|
||||
"""
|
||||
if getattr(args, "isolated", False) or os.environ.get("HERMES_DESKTOP") == "1":
|
||||
if getattr(args, "isolated", False) or _is_desktop_owned_backend():
|
||||
return
|
||||
record = _host_backend_attachment()
|
||||
if record is None:
|
||||
@@ -893,7 +907,7 @@ def _route_named_profile_dashboard(
|
||||
_launch_profile in ("default", "custom")
|
||||
or getattr(args, "isolated", False)
|
||||
or getattr(args, "open_profile", "")
|
||||
or os.environ.get("HERMES_DESKTOP") == "1"
|
||||
or _is_desktop_owned_backend()
|
||||
):
|
||||
return
|
||||
|
||||
|
||||
@@ -62,12 +62,38 @@ class TestUnifiedDashboardRouting:
|
||||
assert env.get("HERMES_HOME") == str(get_default_hermes_root())
|
||||
|
||||
|
||||
def test_inherited_desktop_marker_routes_named_profile_to_machine_dashboard(
|
||||
self, main_mod, monkeypatch
|
||||
):
|
||||
"""A Desktop shell is not its credential-bearing backend child."""
|
||||
monkeypatch.setenv("HERMES_DESKTOP", "1")
|
||||
monkeypatch.delenv("HERMES_DASHBOARD_SESSION_TOKEN", raising=False)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.profiles.get_active_profile_name", lambda: "worker_x"
|
||||
)
|
||||
monkeypatch.setattr(main_dashboard, "_dashboard_listening", lambda host, port: False)
|
||||
execs = []
|
||||
|
||||
def fake_exec(exe, argv, env):
|
||||
execs.append((exe, argv, env))
|
||||
raise SystemExit(0)
|
||||
|
||||
monkeypatch.setattr(main_mod.os, "execvpe", fake_exec)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
main_mod.cmd_dashboard(_args())
|
||||
|
||||
assert len(execs) == 1
|
||||
assert "-p" in execs[0][1] and execs[0][1][execs[0][1].index("-p") + 1] == "default"
|
||||
|
||||
|
||||
def test_desktop_profile_backend_skips_machine_dashboard_reroute(self, main_mod, monkeypatch):
|
||||
"""A desktop-spawned named-profile backend (HERMES_DESKTOP=1) must NOT
|
||||
reroute into the machine dashboard. The reroute re-execs as the default
|
||||
profile and exits, so the desktop never sees a ready backend → boot
|
||||
loop. The guard keeps desktop pool backends per-profile."""
|
||||
monkeypatch.setenv("HERMES_DESKTOP", "1")
|
||||
monkeypatch.setenv("HERMES_DASHBOARD_SESSION_TOKEN", "desktop-spawn-token")
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.profiles.get_active_profile_name", lambda: "worker_x"
|
||||
)
|
||||
@@ -110,5 +136,3 @@ class TestInteractiveDashboardAuthSetup:
|
||||
assert "configured external dashboard.public_url" in output
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -91,6 +91,31 @@ def test_second_serve_attaches_to_the_live_host_backend(host_dir, owner, capsys)
|
||||
assert f"port {owner.port}" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_inherited_desktop_flag_without_spawn_credential_still_attaches(host_dir, owner, monkeypatch):
|
||||
"""A terminal spawned by Desktop inherits its marker, not Desktop ownership.
|
||||
|
||||
Only the Desktop backend receives the per-spawn session credential. A bare
|
||||
marker must therefore preserve the one-host-backend attach invariant.
|
||||
"""
|
||||
monkeypatch.setenv("HERMES_DESKTOP", "1")
|
||||
monkeypatch.delenv("HERMES_DASHBOARD_SESSION_TOKEN", raising=False)
|
||||
_publish(hr.process_create_time(), port=owner.port)
|
||||
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
_attach_to_host_backend(_args(), headless_backend=True)
|
||||
|
||||
assert exc.value.code == 0
|
||||
|
||||
|
||||
def test_desktop_owned_backend_keeps_its_separate_lifecycle(host_dir, owner, monkeypatch):
|
||||
"""Desktop's credential-bearing backend does not attach to the host owner."""
|
||||
monkeypatch.setenv("HERMES_DESKTOP", "1")
|
||||
monkeypatch.setenv("HERMES_DASHBOARD_SESSION_TOKEN", "desktop-spawn-token")
|
||||
_publish(hr.process_create_time(), port=owner.port)
|
||||
|
||||
assert _attach_to_host_backend(_args(), headless_backend=True) is None
|
||||
|
||||
|
||||
def test_stale_record_is_ignored_and_the_launch_proceeds(host_dir):
|
||||
"""A record whose creation time does not match the live PID is a recycled PID, not a
|
||||
backend: the launch must fall through and bind, never attach."""
|
||||
|
||||
Reference in New Issue
Block a user