mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
fix: fall back to no-sandbox for desktop launch on restricted Linux hosts
This commit is contained in:
parent
97640fd9ad
commit
54ea059919
3 changed files with 84 additions and 3 deletions
|
|
@ -5442,6 +5442,46 @@ def _force_adhoc_macos_signing(env: dict, *, source_mode: bool) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def _desktop_linux_needs_no_sandbox() -> bool:
|
||||
"""Return True when Chromium/Electron should bypass the Linux sandbox.
|
||||
|
||||
Ubuntu 23.10+ can enable AppArmor's
|
||||
``apparmor_restrict_unprivileged_userns`` hardening, which breaks
|
||||
Chromium/Electron's user-namespace sandbox for normal users unless the app
|
||||
ships a working root-owned 4755 ``chrome-sandbox`` helper. In headless or
|
||||
non-interactive CLI contexts we may be unable to ``sudo chown/chmod`` that
|
||||
helper, so detect the host restriction and fall back to ``--no-sandbox``
|
||||
rather than hard-failing the launcher.
|
||||
|
||||
We intentionally do NOT return True for root users here: running Electron as
|
||||
root without a sandbox is a qualitatively riskier path than launching as an
|
||||
unprivileged desktop user on an AppArmor-restricted host. The root case
|
||||
should remain an explicit user choice.
|
||||
"""
|
||||
if sys.platform != "linux":
|
||||
return False
|
||||
if hasattr(os, "geteuid") and os.geteuid() == 0:
|
||||
return False
|
||||
try:
|
||||
with open("/proc/sys/kernel/apparmor_restrict_unprivileged_userns", encoding="utf-8") as f:
|
||||
return f.read().strip() == "1"
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _desktop_linux_sandbox_helper_is_regular_file(packaged_executable: Path) -> bool:
|
||||
"""Return True when ``chrome-sandbox`` exists as a regular file."""
|
||||
if sys.platform != "linux":
|
||||
return False
|
||||
sandbox = packaged_executable.parent / "chrome-sandbox"
|
||||
try:
|
||||
sandbox_lstat = sandbox.lstat()
|
||||
except OSError:
|
||||
return False
|
||||
return stat.S_ISREG(sandbox_lstat.st_mode)
|
||||
|
||||
|
||||
|
||||
def _desktop_linux_sandbox_fixup(packaged_executable: Path) -> bool:
|
||||
"""Configure Electron's Linux SUID sandbox helper when required."""
|
||||
if sys.platform != "linux":
|
||||
|
|
@ -5678,11 +5718,16 @@ def cmd_gui(args: argparse.Namespace):
|
|||
print(" Expected an unpacked Electron app for the current OS.")
|
||||
sys.exit(1)
|
||||
|
||||
launch_command = [str(packaged_executable)]
|
||||
if not _desktop_linux_sandbox_fixup(packaged_executable):
|
||||
sys.exit(1)
|
||||
if _desktop_linux_needs_no_sandbox() and _desktop_linux_sandbox_helper_is_regular_file(packaged_executable):
|
||||
print("⚠ Falling back to --no-sandbox because this Linux host restricts unprivileged user namespaces and the Electron sandbox helper could not be configured.")
|
||||
launch_command.append("--no-sandbox")
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
print(f"→ Launching packaged Hermes Desktop: {packaged_executable}")
|
||||
launch_result = subprocess.run([str(packaged_executable)], cwd=desktop_dir, env=env, check=False)
|
||||
print(f"→ Launching packaged Hermes Desktop: {' '.join(launch_command)}")
|
||||
launch_result = subprocess.run(launch_command, cwd=desktop_dir, env=env, check=False)
|
||||
sys.exit(launch_result.returncode)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ AUTHOR_MAP = {
|
|||
"277269729+yusekiotacode@users.noreply.github.com": "yusekiotacode", # PR #48706 salvage (anthropic OAuth login token endpoint → platform.claude.com; #45250/#49821)
|
||||
"minz0721@outlook.com": "s010mn", # PR #29221 salvage (ollama-cloud reasoning_effort xhigh→max)
|
||||
"128256017+chriswesley4@users.noreply.github.com": "chriswesley4", # PR #53185 salvage (re-enable titleBarOverlay on plain Linux; missing min/max/close regression)
|
||||
"rafael.millan@gmail.com": "RafaelMiMi", # PR #42229 salvage (no-sandbox fallback for AppArmor-restricted Linux desktop launch)
|
||||
"jeevesassistant00@gmail.com": "jeeves-assistant", # PR #50771 (computer-use CuaDriver vision capture routing)
|
||||
"21178861+ScotterMonk@users.noreply.github.com": "ScotterMonk", # PR #50145 salvage (cron output truncation: adapter-aware chunking, #50126)
|
||||
"rrandqua@gmail.com": "TutkuEroglu", # PR #50481 salvage (AGENTS.md stale token-lock adapter path)
|
||||
|
|
|
|||
|
|
@ -222,6 +222,41 @@ def test_gui_linux_skips_fixup_when_already_configured(tmp_path, monkeypatch):
|
|||
assert mock_run.call_args.args[0] == [str(packaged_exe)]
|
||||
|
||||
|
||||
def test_gui_linux_falls_back_to_no_sandbox_when_userns_is_restricted(tmp_path, monkeypatch):
|
||||
root = _make_desktop_tree(tmp_path)
|
||||
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
|
||||
packaged_exe = _make_packaged_executable(root, monkeypatch, platform="linux")
|
||||
sandbox = packaged_exe.parent / "chrome-sandbox"
|
||||
sandbox.write_text("", encoding="utf-8")
|
||||
|
||||
launch_ok = subprocess.CompletedProcess([str(packaged_exe), "--no-sandbox"], 0)
|
||||
|
||||
with patch("hermes_cli.main._desktop_linux_sandbox_fixup", return_value=False), \
|
||||
patch("hermes_cli.main._desktop_linux_needs_no_sandbox", return_value=True), \
|
||||
patch("hermes_cli.main.subprocess.run", return_value=launch_ok) as mock_run, \
|
||||
pytest.raises(SystemExit) as exc:
|
||||
cli_main.cmd_gui(_ns(skip_build=True))
|
||||
|
||||
assert exc.value.code == 0
|
||||
mock_run.assert_called_once()
|
||||
assert mock_run.call_args.args[0] == [str(packaged_exe), "--no-sandbox"]
|
||||
|
||||
|
||||
def test_gui_linux_exits_when_sandbox_fixup_fails_without_safe_fallback(tmp_path, monkeypatch):
|
||||
root = _make_desktop_tree(tmp_path)
|
||||
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
|
||||
_make_packaged_executable(root, monkeypatch, platform="linux")
|
||||
|
||||
with patch("hermes_cli.main._desktop_linux_sandbox_fixup", return_value=False), \
|
||||
patch("hermes_cli.main._desktop_linux_needs_no_sandbox", return_value=False), \
|
||||
patch("hermes_cli.main.subprocess.run") as mock_run, \
|
||||
pytest.raises(SystemExit) as exc:
|
||||
cli_main.cmd_gui(_ns(skip_build=True))
|
||||
|
||||
assert exc.value.code == 1
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
def test_gui_source_mode_uses_renderer_build_and_electron(tmp_path, monkeypatch):
|
||||
root = _make_desktop_tree(tmp_path)
|
||||
desktop_dir = root / "apps" / "desktop"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue