fix: a secondary profile no longer removes the default profile's bare-named gateway launchers (review follow-up)

The bare `Hermes_Gateway` Scheduled Task and Startup entry are the live
identity of the default ~/.hermes profile, yet any suffixed home classified
them as its own pre-suffix strays, so `hermes -p work gateway uninstall` /
`install --force` deleted the default profile's autostart. A bare-named
Startup entry or task now counts as legacy only when its action targets THIS
home's gateway-service dir (Startup file contents / `schtasks /Query /XML`);
files inside this home's gateway-service dir are ours by construction.
Invariant test (red before): sibling launchers survive uninstall with no
/Delete issued. The windows_only live test now registers a task whose action
targets the temp home so it still exercises the positive path.
This commit is contained in:
teknium1
2026-09-20 01:04:06 -07:00
committed by Teknium
parent 96ee0516c6
commit 5bdfc79821
3 changed files with 69 additions and 10 deletions

View File

@@ -20,9 +20,20 @@ def _w():
return gateway_windows
def _targets_this_home(text: str) -> bool:
"""A bare-named launcher is OURS only when its action points into THIS home's ``gateway-service``
dir. The bare name is also the live identity of the default ``~/.hermes`` profile, so a secondary
profile that classified every ``Hermes_Gateway`` object as its own stray would delete the default
profile's gateway autostart on ``uninstall`` / ``install --force``."""
w = _w()
marker = w._normalize_windows_path(str(w._hermes_home() / "gateway-service"))
return marker in w._normalize_windows_path(text)
def legacy_launcher_artifacts() -> list[tuple[str, str, Path | str]]:
"""``(kind, label, target)`` for every pre-suffix launcher still present; ``kind`` is ``"file"``
or ``"task"``. Empty when this home owns the bare name (its current objects ARE the bare ones)."""
"""``(kind, label, target)`` for every pre-suffix launcher of THIS home still present; ``kind`` is
``"file"`` or ``"task"``. Empty when this home owns the bare name (its current objects ARE the bare
ones). Bare-named objects belonging to another home (a sibling install) are left alone."""
w = _w()
bare = w._TASK_NAME_DEFAULT
if w.get_task_name() == bare:
@@ -33,13 +44,20 @@ def legacy_launcher_artifacts() -> list[tuple[str, str, Path | str]]:
for path, label in (
(startup / f"{bare}.vbs", "legacy pre-suffix Windows login item"),
(startup / f"{bare}.cmd", "legacy pre-suffix Windows login item"),
):
try:
if _targets_this_home(path.read_text(encoding="utf-8", errors="replace")):
found.append(("file", label, path))
except OSError:
continue
for path, label in (
(service_dir / f"{bare}.vbs", "legacy pre-suffix task launcher"),
(service_dir / f"{bare}.cmd", "legacy pre-suffix task script"),
):
if path.exists():
if path.exists(): # inside this home by construction
found.append(("file", label, path))
code, _out, _err = w._exec_schtasks(["/Query", "/TN", bare])
if code == 0:
code, out, _err = w._exec_schtasks(["/Query", "/TN", bare, "/XML"])
if code == 0 and _targets_this_home(out):
found.append(("task", "legacy pre-suffix Scheduled Task", bare))
return found

View File

@@ -448,11 +448,12 @@ def test_status_names_and_uninstall_removes_pre_suffix_launchers(monkeypatch, tm
(home / "gateway-service").mkdir(parents=True)
startup.mkdir()
legacy_vbs = startup / "Hermes_Gateway.vbs"
legacy_vbs.write_text("legacy", encoding="utf-8")
legacy_vbs.write_text(gateway_windows._build_startup_launcher(home / "gateway-service" / "Hermes_Gateway.cmd"), encoding="utf-8")
legacy_pair = home / "gateway-service" / "Hermes_Gateway.cmd"
legacy_pair.write_text("legacy", encoding="utf-8")
schtasks_calls = []
registered = {"Hermes_Gateway"}
task_xml = gateway_windows._build_scheduled_task_xml("Hermes_Gateway", home / "gateway-service" / "Hermes_Gateway.vbs", None)
def fake_schtasks(args):
schtasks_calls.append(args)
@@ -460,7 +461,7 @@ def test_status_names_and_uninstall_removes_pre_suffix_launchers(monkeypatch, tm
if args[0] == "/Delete":
registered.discard(name)
return (0, "SUCCESS", "")
return (0, "", "") if name in registered else (1, "", "ERROR: The system cannot find the file specified.")
return (0, task_xml, "") if name in registered else (1, "", "ERROR: The system cannot find the file specified.")
monkeypatch.setattr(gateway_windows, "_assert_windows", lambda: None)
monkeypatch.setattr(gateway_windows, "get_task_name", lambda: "Hermes_Gateway_alice")
@@ -488,6 +489,44 @@ def test_status_names_and_uninstall_removes_pre_suffix_launchers(monkeypatch, tm
assert "legacy pre-suffix" not in capsys.readouterr().out
def test_secondary_profile_leaves_default_profiles_bare_launchers_alone(monkeypatch, tmp_path, capsys):
"""The bare ``Hermes_Gateway`` task and Startup entry are the LIVE identity of the default ``~/.hermes``
profile. From a secondary profile they are a sibling install, not this home's pre-suffix stray:
``uninstall`` / ``install --force`` must issue no ``schtasks /Delete`` and unlink nothing."""
startup, home, default_home = tmp_path / "Startup", tmp_path / "profiles" / "work", tmp_path / "default"
(home / "gateway-service").mkdir(parents=True)
(default_home / "gateway-service").mkdir(parents=True)
startup.mkdir()
default_vbs = startup / "Hermes_Gateway.vbs"
default_vbs.write_text(gateway_windows._build_startup_launcher(default_home / "gateway-service" / "Hermes_Gateway.cmd"), encoding="utf-8")
task_xml = gateway_windows._build_scheduled_task_xml("Hermes_Gateway", default_home / "gateway-service" / "Hermes_Gateway.vbs", None)
schtasks_calls = []
def fake_schtasks(args):
schtasks_calls.append(args)
if args[0] == "/Query" and args[args.index("/TN") + 1] == "Hermes_Gateway":
return (0, task_xml, "")
return (1, "", "ERROR: The system cannot find the file specified.")
monkeypatch.setattr(gateway_windows, "_assert_windows", lambda: None)
monkeypatch.setattr(gateway_windows, "get_task_name", lambda: "Hermes_Gateway_work")
monkeypatch.setattr(gateway_windows, "get_task_script_path", lambda: home / "gateway-service" / "Hermes_Gateway_work.cmd")
monkeypatch.setattr(gateway_windows, "get_startup_entry_path", lambda: startup / "Hermes_Gateway_work.vbs")
monkeypatch.setattr(gateway_windows, "_legacy_startup_entry_path", lambda: startup / "Hermes_Gateway_work.cmd")
monkeypatch.setattr(gateway_windows, "_startup_dir", lambda: startup)
monkeypatch.setattr(gateway_windows, "_hermes_home", lambda: home)
monkeypatch.setattr(gateway_windows, "_exec_schtasks", fake_schtasks)
monkeypatch.setattr(gateway_windows, "_gateway_pids", lambda *a, **k: [])
monkeypatch.setattr(gateway_windows, "_print_start_attestation_warning", lambda: None)
gateway_windows.status()
assert "legacy pre-suffix" not in capsys.readouterr().out
gateway_windows.uninstall()
capsys.readouterr()
assert default_vbs.exists()
assert not any(call[0] == "/Delete" and "Hermes_Gateway" in call for call in schtasks_calls)
# Reporter's `Export-ScheduledTask` of a task registered before the hardened template (#113670).
_PRE_HARDENING_TASK_XML = """<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">

View File

@@ -28,14 +28,16 @@ def test_status_warns_and_uninstall_removes_pre_suffix_launchers(tmp_path, monke
current = gateway_windows.get_task_name()
assert current != "Hermes_Gateway", "a temp home must carry a profile suffix"
legacy_vbs = startup / "Hermes_Gateway.vbs"
legacy_vbs.write_text("' pre-suffix login item\r\n", encoding="utf-8")
legacy_pair = home / "gateway-service" / "Hermes_Gateway.vbs"
legacy_pair.write_text("' pre-suffix launcher\r\n", encoding="utf-8")
# Both objects must point INTO this home: a bare-named task/entry targeting another home is a
# sibling install (the default profile's live gateway) and is deliberately left alone.
legacy_vbs = startup / "Hermes_Gateway.vbs"
legacy_vbs.write_text(gateway_windows._build_startup_launcher(legacy_pair.with_suffix(".cmd")), encoding="utf-8")
schtasks = shutil.which("schtasks") or "schtasks"
create = subprocess.run(
[schtasks, "/Create", "/F", "/TN", "Hermes_Gateway", "/SC", "ONLOGON", "/TR", "cmd.exe /c exit 0"],
[schtasks, "/Create", "/F", "/TN", "Hermes_Gateway", "/SC", "ONLOGON", "/TR", f"wscript.exe //B {legacy_pair}"],
capture_output=True, text=True, timeout=60,
)
assert create.returncode == 0, create.stderr