Fix Hermes desktop launch settings and sandbox consistency
Release 2026.8.31-3 preserves upstream desktop settings and explicit launch environment without invoking the CLI sandbox setup. Keep incomplete runtimes from blocking the packaged fallback, and use the namespace sandbox consistently in both locations. Co-Authored-By: Codex XHigh <noreply@openai.com>
This commit is contained in:
1 parent
bf842f5a6f
commit
f69f6ce364
3 files changed
+158
-67
No files matched your search
@@ -1,4 +1,5 @@
|
||||
"""Check native launch and the release updater without running Hermes or sudo."""
|
||||
"""Check launch settings and sandbox gates without running Hermes or sudo."""
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -26,6 +27,9 @@ 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"),
|
||||
"ozone": os.environ.get("ELECTRON_OZONE_PLATFORM_HINT"),
|
||||
"cwd": os.environ.get("HERMES_DESKTOP_CWD"),
|
||||
"inherited": [name for name in ("ELECTRON_RUN_AS_NODE", "PYTHONPATH", "PYTHONHOME") if name in os.environ],
|
||||
}))
|
||||
''')
|
||||
executable.chmod(0o755)
|
||||
@@ -33,6 +37,30 @@ Path(os.environ["TEST_OUTPUT"]).write_text(json.dumps({
|
||||
sandbox.write_text("fixture")
|
||||
sandbox.chmod(0o755)
|
||||
|
||||
# Execute the release's real option parser, isolating config I/O and avoiding
|
||||
# unrelated CLI imports, startup hooks and third-party dependencies.
|
||||
module = runtime / "hermes_cli"
|
||||
module.mkdir()
|
||||
(module / "__init__.py").touch()
|
||||
upstream = ast.parse((source / "hermes_cli/main.py").read_text())
|
||||
option_parser = next(node for node in upstream.body
|
||||
if isinstance(node, ast.FunctionDef) and node.name == "_desktop_launch_options")
|
||||
stores = next(node for node in upstream.body if isinstance(node, ast.Assign)
|
||||
and any(isinstance(target, ast.Name) and target.id == "_LINUX_PASSWORD_STORES"
|
||||
for target in node.targets))
|
||||
helper = "import os, shlex\n" + ast.unparse(stores) + "\n" + ast.unparse(option_parser) + "\n"
|
||||
(module / "main.py").write_text(helper)
|
||||
(module / "config.py").write_text('''import json, os
|
||||
from pathlib import Path
|
||||
def load_config():
|
||||
path = Path(os.environ["HERMES_HOME"], "config.yaml")
|
||||
return json.loads(path.read_text()) if path.exists() else {}
|
||||
''')
|
||||
(runtime / "hermes_constants.py").write_text('''import os
|
||||
def with_hermes_node_path(env=None):
|
||||
return (os.environ if env is None else env).copy()
|
||||
''')
|
||||
|
||||
mock_bin = root / "bin"
|
||||
mock_bin.mkdir()
|
||||
unshare = mock_bin / "unshare"
|
||||
@@ -43,6 +71,7 @@ Path(os.environ["TEST_OUTPUT"]).write_text(json.dumps({
|
||||
cli.parent.mkdir(parents=True)
|
||||
cli.write_text(forbidden)
|
||||
cli.chmod(0o755)
|
||||
(cli.parent / "python").symlink_to(sys.executable)
|
||||
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')
|
||||
@@ -53,33 +82,81 @@ Path(os.environ["TEST_OUTPUT"]).write_text(json.dumps({
|
||||
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)
|
||||
|
||||
def check_launch(args=(), overrides=None, expected_args=(), store="gnome-libsecret", gpu=None, ozone=None):
|
||||
subprocess.run(launch + list(args), env={**env, **(overrides or {})}, cwd=home, 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 result == {"args": ["--disable-setuid-sandbox", *expected_args],
|
||||
"home": str(home / ".hermes"), "store": store, "gpu": gpu,
|
||||
"ozone": ozone, "cwd": str(home), "inherited": []}, 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()
|
||||
|
||||
wayland = {"WAYLAND_DISPLAY": "wayland-1"}
|
||||
check_launch(overrides=wayland, expected_args=["--ozone-platform=wayland"])
|
||||
url = "hermes://open?text=a%20b"
|
||||
check_launch(["--ozone-platform=x11", url], {**wayland, "HERMES_DESKTOP_PASSWORD_STORE": "kwallet6"},
|
||||
["--ozone-platform=x11", url], store="kwallet6")
|
||||
check_launch(overrides={"HERMES_HOME": str(home / ".hermes/profiles/work"),
|
||||
"HERMES_DESKTOP_DISABLE_GPU": "1"}, gpu="1")
|
||||
check_launch(overrides={"ELECTRON_RUN_AS_NODE": "1", "PYTHONPATH": "/invalid", "PYTHONHOME": "/invalid"})
|
||||
|
||||
config = home / ".hermes/config.yaml"
|
||||
config.write_text(json.dumps({"desktop": {"disable_gpu": True, "password_store": "kwallet5",
|
||||
"ozone_platform_hint": "x11",
|
||||
"electron_flags": '--force-device-scale-factor=1.5 "--test=a b"'}}))
|
||||
config_flags = ["--force-device-scale-factor=1.5", "--test=a b"]
|
||||
for args in ([], [url]):
|
||||
check_launch(args, wayland, config_flags + args, store="kwallet5", gpu="1", ozone="x11")
|
||||
check_launch(overrides={**wayland, "HERMES_DESKTOP_DISABLE_GPU": "0",
|
||||
"HERMES_DESKTOP_PASSWORD_STORE": "basic", "ELECTRON_OZONE_PLATFORM_HINT": "wayland"},
|
||||
expected_args=config_flags, store="basic", gpu="0", ozone="wayland")
|
||||
config.write_text(json.dumps({"desktop": {"electron_flags": ["--ozone-platform=x11"], "disable_gpu": False}}))
|
||||
check_launch(overrides=wayland, expected_args=["--ozone-platform=x11"], gpu="0")
|
||||
# New upstream revisions expose the same helper from main_desktop instead.
|
||||
(module / "main_desktop.py").write_text(helper)
|
||||
(module / "main.py").write_text('raise AssertionError("old helper import after update")\n')
|
||||
check_launch([url], wayland, ["--ozone-platform=x11", url], gpu="0")
|
||||
config.unlink()
|
||||
(module / "main_desktop.py").write_text(helper + '\nimport os\nos.environ.update(' + repr({
|
||||
"ELECTRON_RUN_AS_NODE": "1", "PYTHONPATH": "/invalid", "PYTHONHOME": "/invalid",
|
||||
"HERMES_HOME": "/invalid", "HERMES_DESKTOP_DISABLE_GPU": "1",
|
||||
"HERMES_DESKTOP_PASSWORD_STORE": "basic", "ELECTRON_OZONE_PLATFORM_HINT": "x11",
|
||||
}) + ')\n')
|
||||
check_launch(overrides={**wayland, "HERMES_DESKTOP_DISABLE_GPU": "0"},
|
||||
expected_args=["--ozone-platform=wayland"], gpu="0")
|
||||
(module / "main_desktop.py").write_text(helper)
|
||||
|
||||
def check_namespace_failure():
|
||||
for code in ("1", "127"):
|
||||
for args in ([], [url]):
|
||||
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()
|
||||
|
||||
check_namespace_failure()
|
||||
# Exercise the real fallback branch with only its absolute app path redirected.
|
||||
fallback = root / "packaged-Hermes"
|
||||
shutil.copyfile(executable, fallback)
|
||||
fallback.chmod(0o755)
|
||||
fallback_launcher = root / "fallback-launcher"
|
||||
fallback_launcher.write_text(launcher.read_text().replace("/opt/hermes-desktop/Hermes", str(fallback)))
|
||||
launch = ["bash", str(fallback_launcher)]
|
||||
executable.unlink()
|
||||
check_launch([url], wayland, ["--ozone-platform=wayland", url])
|
||||
(module / "main_desktop.py").write_text('raise ImportError("incomplete Python dependencies")\n')
|
||||
check_launch([url], wayland, ["--ozone-platform=wayland", url])
|
||||
shutil.rmtree(runtime / "venv")
|
||||
check_launch(overrides={"ELECTRON_RUN_AS_NODE": "1", "PYTHONPATH": "/invalid", "PYTHONHOME": "/invalid"})
|
||||
check_namespace_failure()
|
||||
|
||||
gate = ["bash", str(destination), "--self-test-gate", "--install-root", str(runtime),
|
||||
"--relaunch-target", str(executable)]
|
||||
executable.touch()
|
||||
executable.chmod(0o755)
|
||||
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: direct native launches fail closed; release updater accepts the namespace sandbox")
|
||||
print("PASS: desktop settings, environment, native/fallback namespace sandbox and updater gate")
|
||||
Reference in new issue
Block a user