From dee6a5981c80cb345e5d0eca949e4d62876d6a99 Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Tue, 8 Sep 2026 22:58:35 +0300 Subject: [PATCH] fix(launchd): pin the nightly backup template to the keg interpreter (XS) Found by the PR #815 pair review. #790 pinned every hook to the keg python; the launchd template for the nightly JSONL backup was missed and still shipped `/usr/bin/env python3`. That matters here for the reason AGENTS.md already documents: bare `python3` fronts the framework python, whose `_brainlayer.pth` injects `~/Gits/brainlayer/src`. So a `git checkout` at the repo root silently re-aims the job -- and the job in question is the one that PREVENTS data loss. The installed plist on this machine happens to be safe: it goes through brainlayer-env-run.sh with BRAINLAYER_PYTHON set to the keg. The template is what a reinstall would render, so the hazard is latent rather than active. That is exactly the shape that put a checkout's install.sh in front of every LaunchAgent on 09-05. The test reuses the gate #790 already built -- hook_python.is_pinned_interpreter -- rather than inventing a second definition of "pinned", so the two cannot drift. It walks every launchd template, judges only direct python invocations (a wrapper script makes no interpreter claim), and fails on anything it cannot vouch for. Verified RED against origin/main's plist and green with the pin. Co-Authored-By: brainlayerClaude running claude-opus-5 --- launchd/com.brainlayer.jsonl-backup.plist | 7 +++-- tests/test_hook_python.py | 37 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/launchd/com.brainlayer.jsonl-backup.plist b/launchd/com.brainlayer.jsonl-backup.plist index 8ca8d008..63d161cf 100644 --- a/launchd/com.brainlayer.jsonl-backup.plist +++ b/launchd/com.brainlayer.jsonl-backup.plist @@ -10,8 +10,11 @@ ProgramArguments - /usr/bin/env - python3 + + /opt/homebrew/opt/brainlayer/libexec/venv/bin/python -m brainlayer.jsonl_backup diff --git a/tests/test_hook_python.py b/tests/test_hook_python.py index 9463a2cc..2c9477fb 100644 --- a/tests/test_hook_python.py +++ b/tests/test_hook_python.py @@ -506,3 +506,40 @@ def test_installed_brainlayer_hooks_are_pinned(): assert findings == [], "BrainLayer hooks still resolve their interpreter through PATH: " + "; ".join( f"{f.event}: {f.command}" for f in findings ) + + +def test_launchd_plist_templates_pin_their_interpreter(): + """launchd templates must not let PATH choose the interpreter. + + #790 pinned the hooks. The nightly backup template was missed and still shipped + `/usr/bin/env python3`, which fronts the framework python whose `_brainlayer.pth` + injects `~/Gits/brainlayer/src` -- so a `git checkout` at the repo root would have + silently re-aimed the job that PREVENTS data loss at a working tree. + + Found by the PR #815 pair review. Applying the same affirmative gate the hooks use: + anything this cannot vouch for is a finding, not a pass. + """ + import plistlib + from pathlib import Path + + from brainlayer.hook_python import is_pinned_interpreter + + repo_root = Path(__file__).resolve().parents[1] + plists = sorted((repo_root / "launchd").glob("*.plist")) + assert plists, "expected launchd templates to exist" + + unpinned: list[str] = [] + for path in plists: + args = plistlib.loads(path.read_bytes()).get("ProgramArguments") or [] + if not args: + continue + interpreter = args[0] + # A wrapper script is not an interpreter claim; only judge direct python invocations. + if "python" not in interpreter and not interpreter.endswith("/env"): + continue + if interpreter.endswith("/env"): + interpreter = f"{interpreter} {args[1] if len(args) > 1 else ''}".strip() + if not is_pinned_interpreter(interpreter): + unpinned.append(f"{path.name}: {interpreter}") + + assert not unpinned, "launchd templates must name a pinned interpreter, not PATH: " + "; ".join(unpinned)