Launch native Hermes without privileged sandbox setup

Use the same direct executable path for menu launches and URLs, require working user namespaces, and retain only the first-update relaunch gate backport. This avoids the upstream CLI fallback that makes a helper in the user runtime setuid-root.

Co-Authored-By: GPT-6 Codex (xhigh) <noreply@openai.com>
This commit is contained in:
Spencer BullandGPT-6 Codex committed 2026-09-07 03:17:42 -05:00
1 parent 9588b28cf6
commit 2fb9ab2ba9
4 files changed
+71 -130

No files matched your search

+59 -45
View File
@@ -1,71 +1,85 @@
"""Check the release backport against the actual pinned upstream source."""
import ast
"""Check native launch and the release updater without running Hermes or sudo."""
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
from typing import Optional
from unittest.mock import patch
source, patch_file = map(Path, sys.argv[1:])
source, patch_file, launcher = map(Path, sys.argv[1:])
with tempfile.TemporaryDirectory(prefix="hermes-runtime-check-") as temporary:
root = Path(temporary)
for filename in ("hermes_cli/main.py", "scripts/desktop-update/posix.sh"):
destination = root / filename
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source / filename, destination)
destination = root / "scripts/desktop-update/posix.sh"
destination.parent.mkdir(parents=True)
shutil.copyfile(source / "scripts/desktop-update/posix.sh", destination)
subprocess.run(["git", "apply", str(patch_file.resolve())], cwd=root, check=True)
text = (root / "hermes_cli/main.py").read_text()
functions = {node.name: node for node in ast.parse(text).body if isinstance(node, ast.FunctionDef)}
names = [
"_desktop_linux_userns_sandbox_available", "_sandbox_helper_lstat",
"_sandbox_helper_is_setuid_root", "_desktop_linux_needs_disable_setuid_sandbox",
"_desktop_linux_sandbox_fixup",
]
scope = dict(Path=Path, Optional=Optional, os=os, sys=sys, shutil=shutil,
subprocess=subprocess, stat=__import__("stat"))
for name in names:
exec(compile(ast.Module(body=[functions[name]], type_ignores=[]), str(source), "exec"), scope)
native = root / "apps/desktop/release/linux-unpacked"
home = root / "home with spaces"
runtime = home / ".hermes/hermes-agent"
native = runtime / "apps/desktop/release/linux-unpacked"
native.mkdir(parents=True)
executable = native / "Hermes"
executable.write_text(f"#!{sys.executable}\n" + '''import json, os, sys
from pathlib import Path
Path(os.environ["TEST_OUTPUT"]).write_text(json.dumps({
"args": sys.argv[1:], "home": os.environ["HERMES_HOME"],
"store": os.environ["HERMES_DESKTOP_PASSWORD_STORE"],
"gpu": os.environ.get("HERMES_DESKTOP_DISABLE_GPU"),
}))
''')
executable.chmod(0o755)
sandbox = native / "chrome-sandbox"
sandbox.write_text("fixture")
sandbox.chmod(0o755)
gui = ast.get_source_segment(text, functions["cmd_gui"])
start = gui.index(" launch_command = [str(packaged_executable)]")
end = gui.index(" launch_command.extend(config_electron_flags)", start)
exec("def launch(packaged_executable):\n" + gui[start:end] + " return launch_command\n", scope)
scope["_desktop_linux_needs_no_sandbox"] = lambda: False
with patch.object(shutil, "which", side_effect=lambda name: "/fixture/unshare" if name == "unshare" else None):
with patch.object(subprocess, "run", return_value=subprocess.CompletedProcess([], 0)) as run:
assert scope["launch"](executable) == [str(executable), "--disable-setuid-sandbox"]
assert all(call.args[0][0] == "/fixture/unshare" for call in run.call_args_list)
with patch.object(subprocess, "run", return_value=subprocess.CompletedProcess([], 1)):
assert not scope["_desktop_linux_sandbox_fixup"](executable)
sandbox.unlink()
sandbox.symlink_to(root / "unrelated")
(root / "unrelated").write_text("keep")
with patch.object(subprocess, "run") as run:
assert not scope["_desktop_linux_sandbox_fixup"](executable)
run.assert_not_called()
sandbox.unlink()
sandbox.write_text("fixture")
mock_bin = root / "bin"
mock_bin.mkdir()
unshare = mock_bin / "unshare"
unshare.write_text('#!/bin/bash\nexit "${TEST_NAMESPACE_RESULT:-0}"\n')
unshare.chmod(0o755)
env = {**os.environ, "PATH": f"{mock_bin}:/usr/bin:/bin"}
env.pop("ELECTRON_DISABLE_SANDBOX", None)
gate = ["bash", str(root / "scripts/desktop-update/posix.sh"), "--self-test-gate",
"--install-root", str(root), "--relaunch-target", str(executable)]
forbidden = '#!/bin/bash\ntouch "$TEST_FORBIDDEN"\nexit 99\n'
cli = runtime / "venv/bin/hermes"
cli.parent.mkdir(parents=True)
cli.write_text(forbidden)
cli.chmod(0o755)
for command in ("sudo", "omarchy-install-hermes-cli"):
target = mock_bin / command
target.write_text(forbidden if command == "sudo" else '#!/bin/bash\nexit 0\n')
target.chmod(0o755)
output = root / "launch.json"
forbidden_output = root / "forbidden"
env = {"HOME": str(home), "PATH": f"{mock_bin}:/usr/bin:/bin",
"TEST_OUTPUT": str(output), "TEST_FORBIDDEN": str(forbidden_output)}
launch = ["bash", str(launcher.resolve())]
for args, overrides, expected_args, expected_store in (
([], {"WAYLAND_DISPLAY": "wayland-1"}, ["--ozone-platform=wayland"], "gnome-libsecret"),
(["--ozone-platform=x11", "hermes://open?text=a%20b"],
{"WAYLAND_DISPLAY": "wayland-1", "HERMES_DESKTOP_PASSWORD_STORE": "kwallet6"},
["--ozone-platform=x11", "hermes://open?text=a%20b"], "kwallet6"),
([], {"HERMES_HOME": str(home / ".hermes/profiles/work"), "HERMES_DESKTOP_DISABLE_GPU": "1"},
[], "gnome-libsecret"),
):
subprocess.run(launch + args, env={**env, **overrides}, check=True)
result = json.loads(output.read_text())
assert result["args"] == ["--disable-setuid-sandbox", *expected_args], result
assert result["home"] == str(home / ".hermes"), result
assert result["store"] == expected_store, result
assert result["gpu"] == overrides.get("HERMES_DESKTOP_DISABLE_GPU"), result
assert not forbidden_output.exists(), "launcher invoked CLI or sudo"
output.unlink()
for code in ("1", "127"):
for args in ([], ["hermes://open"]):
result = subprocess.run(launch + args, env={**env, "TEST_NAMESPACE_RESULT": code},
capture_output=True, text=True)
assert result.returncode != 0 and "user namespaces" in result.stderr, result
assert not output.exists() and not forbidden_output.exists()
gate = ["bash", str(destination), "--self-test-gate", "--install-root", str(runtime),
"--relaunch-target", str(executable)]
assert subprocess.check_output(gate, env=env, text=True).strip() == "relaunch"
assert subprocess.check_output(gate, env={**env, "TEST_NAMESPACE_RESULT": "1"}, text=True).startswith("manual:")
gate[-1] = "/opt/hermes-desktop/Hermes"
assert subprocess.check_output(gate, env=env, text=True).startswith("skew:")
print("PASS: native launch and release update handoff retain the user-namespace sandbox")
print("PASS: direct native launches fail closed; release updater accepts the namespace sandbox")