Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions launchd/com.brainlayer.jsonl-backup.plist
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
</array>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/env</string>
<string>python3</string>
<!-- Pinned to the keg interpreter, never PATH. Bare `python3` fronts the framework
python, whose _brainlayer.pth injects ~/Gits/brainlayer/src - so a `git checkout`
at the root would silently re-aim the nightly backup at a working tree. Same
reasoning as #790 for hooks; the `opt/` symlink outlives any single keg. -->
<string>/opt/homebrew/opt/brainlayer/libexec/venv/bin/python</string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High launchd/com.brainlayer.jsonl-backup.plist:17

On Intel Homebrew installations, the scheduled backup cannot start because /opt/homebrew/opt/brainlayer/libexec/venv/bin/python does not point to the installed keg at /usr/local/opt/brainlayer/libexec/venv/bin/python. Render the actual Homebrew prefix/interpreter or invoke a prefix-aware wrapper instead of hard-coding /opt/homebrew.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @launchd/com.brainlayer.jsonl-backup.plist around line 17:

On Intel Homebrew installations, the scheduled backup cannot start because `/opt/homebrew/opt/brainlayer/libexec/venv/bin/python` does not point to the installed keg at `/usr/local/opt/brainlayer/libexec/venv/bin/python`. Render the actual Homebrew prefix/interpreter or invoke a prefix-aware wrapper instead of hard-coding `/opt/homebrew`.

<string>-m</string>
<string>brainlayer.jsonl_backup</string>
</array>
Expand Down
37 changes: 37 additions & 0 deletions tests/test_hook_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading