From ad42bfb8bdeb7b624a15c4ed43994414769ef7b0 Mon Sep 17 00:00:00 2001 From: serverleader <34089+serverleader@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:23:47 -0600 Subject: [PATCH 1/3] Launchers: skip a python3 older than the bootstrap needs On macOS the first python3 on a hook's PATH is often /usr/bin/python3 (3.9): agents run hooks without a login shell, so Homebrew or pyenv Pythons come later or only under a versioned name. The POSIX launchers exec'd that first python3, the bootstrap failed open on its version check, and the plugin silently did nothing although a usable Python was installed. Probe each candidate (TE_PYTHON, python3, python3.14 .. python3.10, python) and exec the first one >= 3.10. With none, hand the hook to the first Python found so the bootstrap still fails open, and `te` still says which version it needs. Windows paths are unchanged. --- plugin/scripts/te | 13 +++++++++++-- plugin/scripts/te-hook | 13 +++++++++++-- tests/test_plugin_package.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/plugin/scripts/te b/plugin/scripts/te index 53c416a..1ddce48 100755 --- a/plugin/scripts/te +++ b/plugin/scripts/te @@ -32,10 +32,19 @@ if [ "${OS:-}" = Windows_NT ]; then exec "$py" "$dir/te_bootstrap.py" cli "$@" done else - for py in "${TE_PYTHON:-}" python3 python; do + # The first python3 on PATH can be older than the bootstrap's minimum (macOS /usr/bin/python3 + # is 3.9, and an agent's hook PATH is often not a login shell's), which makes the plugin a + # silent no-op even when a newer Python is installed. Take the first one new enough; with + # none, run the first one found so the bootstrap still fails open / says what it needs. + old= + for py in "${TE_PYTHON:-}" python3 python3.14 python3.13 python3.12 python3.11 python3.10 python; do [ -n "$py" ] && command -v "$py" >/dev/null 2>&1 || continue - exec "$py" "$dir/te_bootstrap.py" cli "$@" + if "$py" -c 'import sys; sys.exit(sys.version_info < (3, 10))' /dev/null 2>&1; then + exec "$py" "$dir/te_bootstrap.py" cli "$@" + fi + [ -n "$old" ] || old=$py done + [ -n "$old" ] && exec "$old" "$dir/te_bootstrap.py" cli "$@" fi echo "te: no python3 on PATH (set TE_PYTHON)" >&2 exit 1 diff --git a/plugin/scripts/te-hook b/plugin/scripts/te-hook index da387ea..af9ab21 100755 --- a/plugin/scripts/te-hook +++ b/plugin/scripts/te-hook @@ -35,10 +35,19 @@ if [ "${OS:-}" = Windows_NT ]; then exec "$py" "$dir/te_bootstrap.py" hook "$@" done else - for py in "${TE_PYTHON:-}" python3 python; do + # The first python3 on PATH can be older than the bootstrap's minimum (macOS /usr/bin/python3 + # is 3.9, and an agent's hook PATH is often not a login shell's), which makes the plugin a + # silent no-op even when a newer Python is installed. Take the first one new enough; with + # none, run the first one found so the bootstrap still fails open / says what it needs. + old= + for py in "${TE_PYTHON:-}" python3 python3.14 python3.13 python3.12 python3.11 python3.10 python; do [ -n "$py" ] && command -v "$py" >/dev/null 2>&1 || continue - exec "$py" "$dir/te_bootstrap.py" hook "$@" + if "$py" -c 'import sys; sys.exit(sys.version_info < (3, 10))' /dev/null 2>&1; then + exec "$py" "$dir/te_bootstrap.py" hook "$@" + fi + [ -n "$old" ] || old=$py done + [ -n "$old" ] && exec "$old" "$dir/te_bootstrap.py" hook "$@" fi # Drain stdin with a builtin (no `cat` when PATH is empty), then answer "no change". while IFS= read -r _line; do :; done diff --git a/tests/test_plugin_package.py b/tests/test_plugin_package.py index 011f962..959ab07 100644 --- a/tests/test_plugin_package.py +++ b/tests/test_plugin_package.py @@ -198,6 +198,38 @@ def test_the_launcher_fails_open_without_python(tmp_path): assert code == 0 and json.loads(out) == {} +def _old_python(path, marker): + # Fails the launcher's version probe (`-c ...`); anything else it is asked to run, it records. + _script(path, f'case "$1" in -c) exit 1 ;; esac\n: > "{marker}"\nwhile IFS= read -r _l; do :; done\necho "{{}}"') + + +@pytest.mark.skipif(os.name == "nt", reason="the POSIX launcher path") +def test_the_launcher_skips_a_python3_older_than_the_bootstrap_needs(tmp_path): + old, new = tmp_path / "old" / "python3", tmp_path / "new" / "python3.12" + _old_python(old, tmp_path / "old_ran") + _script(new, f'exec "{sys.executable}" "$@"') + work = tmp_path / "work" + work.mkdir() + code, out, _ = _run_hook(os.path.join(PLUGIN, "scripts", "te-hook"), + {"hook_event_name": "SessionStart", "session_id": "s", + "cwd": str(work), "source": "startup"}, + _hook_env(tmp_path, PATH=f"{old.parent}:{new.parent}:/usr/bin:/bin")) + assert code == 0 and json.loads(out) == {} + assert os.path.isdir(tmp_path / "home" / "repos") # the new Python ran the dispatcher + assert not (tmp_path / "old_ran").exists() + + +@pytest.mark.skipif(os.name == "nt", reason="the POSIX launcher path") +def test_with_only_an_old_python_the_launcher_still_hands_it_the_hook(tmp_path): + old = tmp_path / "old" / "python3" + _old_python(old, tmp_path / "old_ran") + code, out, _ = _run_hook(os.path.join(PLUGIN, "scripts", "te-hook"), + {"hook_event_name": "SessionStart"}, + {"PATH": f"{old.parent}:/bin", "TOKENECONOMIZER_HOME": str(tmp_path / "home")}) + assert code == 0 and json.loads(out) == {} + assert (tmp_path / "old_ran").exists() # so the bootstrap can fail open itself + + def test_the_bootstrap_answers_no_change_on_an_old_python(tmp_path, monkeypatch): sys.path.insert(0, os.path.join(PLUGIN, "scripts")) try: From f60e19ab77d7e157138071d43e0e3b91633fb2c6 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 10 Nov 2026 10:25:32 -0800 Subject: [PATCH 2/3] Launchers: run TE_PYTHON as named, probe with -E -S, add python3.15 TE_PYTHON is the person's choice and runs without a version probe, as before. The probe skips site setup. The fallback test's PATH no longer includes /bin, whose real python3.x made it fail on Linux. --- plugin/scripts/te | 19 ++++++++++++------- plugin/scripts/te-hook | 22 ++++++++++++++-------- tests/test_plugin_package.py | 24 +++++++++++++++++++++--- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/plugin/scripts/te b/plugin/scripts/te index 1ddce48..c3320b4 100755 --- a/plugin/scripts/te +++ b/plugin/scripts/te @@ -32,14 +32,19 @@ if [ "${OS:-}" = Windows_NT ]; then exec "$py" "$dir/te_bootstrap.py" cli "$@" done else - # The first python3 on PATH can be older than the bootstrap's minimum (macOS /usr/bin/python3 - # is 3.9, and an agent's hook PATH is often not a login shell's), which makes the plugin a - # silent no-op even when a newer Python is installed. Take the first one new enough; with - # none, run the first one found so the bootstrap still fails open / says what it needs. + # TE_PYTHON is the person's own choice: it runs as named. Otherwise the first python3 on PATH + # can be older than the bootstrap's minimum (macOS /usr/bin/python3 is 3.9, and an agent's + # hook PATH is often not a login shell's), which would make the plugin a silent no-op with a + # newer Python installed: take the first one new enough. With none, run the first one found, + # so the bootstrap still fails open / says what it needs. The probe skips `site` (-E -S). + if [ -n "${TE_PYTHON:-}" ] && command -v "$TE_PYTHON" >/dev/null 2>&1; then + exec "$TE_PYTHON" "$dir/te_bootstrap.py" cli "$@" + fi + probe='import sys; sys.exit(sys.version_info < (3, 10))' old= - for py in "${TE_PYTHON:-}" python3 python3.14 python3.13 python3.12 python3.11 python3.10 python; do - [ -n "$py" ] && command -v "$py" >/dev/null 2>&1 || continue - if "$py" -c 'import sys; sys.exit(sys.version_info < (3, 10))' /dev/null 2>&1; then + for py in python3 python3.15 python3.14 python3.13 python3.12 python3.11 python3.10 python; do + command -v "$py" >/dev/null 2>&1 || continue + if "$py" -E -S -c "$probe" /dev/null 2>&1; then exec "$py" "$dir/te_bootstrap.py" cli "$@" fi [ -n "$old" ] || old=$py diff --git a/plugin/scripts/te-hook b/plugin/scripts/te-hook index af9ab21..dab0128 100755 --- a/plugin/scripts/te-hook +++ b/plugin/scripts/te-hook @@ -1,5 +1,6 @@ #!/bin/sh -# TokenEconomizer hook launcher: run the hook dispatcher under the first Python 3 on PATH. +# TokenEconomizer hook launcher: run the hook dispatcher under TE_PYTHON, else the first Python +# 3.10+ on PATH. # Fails open -- with no usable Python the hook prints an empty object, which changes nothing. # On Windows (Git Bash), $0 can arrive with backslashes. The Microsoft Store alias under WindowsApps # runs nothing, and every probe of it starts AppXSvc (python/pymanager#396), so a Python outside @@ -35,14 +36,19 @@ if [ "${OS:-}" = Windows_NT ]; then exec "$py" "$dir/te_bootstrap.py" hook "$@" done else - # The first python3 on PATH can be older than the bootstrap's minimum (macOS /usr/bin/python3 - # is 3.9, and an agent's hook PATH is often not a login shell's), which makes the plugin a - # silent no-op even when a newer Python is installed. Take the first one new enough; with - # none, run the first one found so the bootstrap still fails open / says what it needs. + # TE_PYTHON is the person's own choice: it runs as named. Otherwise the first python3 on PATH + # can be older than the bootstrap's minimum (macOS /usr/bin/python3 is 3.9, and an agent's + # hook PATH is often not a login shell's), which would make the plugin a silent no-op with a + # newer Python installed: take the first one new enough. With none, run the first one found, + # so the bootstrap still fails open / says what it needs. The probe skips `site` (-E -S). + if [ -n "${TE_PYTHON:-}" ] && command -v "$TE_PYTHON" >/dev/null 2>&1; then + exec "$TE_PYTHON" "$dir/te_bootstrap.py" hook "$@" + fi + probe='import sys; sys.exit(sys.version_info < (3, 10))' old= - for py in "${TE_PYTHON:-}" python3 python3.14 python3.13 python3.12 python3.11 python3.10 python; do - [ -n "$py" ] && command -v "$py" >/dev/null 2>&1 || continue - if "$py" -c 'import sys; sys.exit(sys.version_info < (3, 10))' /dev/null 2>&1; then + for py in python3 python3.15 python3.14 python3.13 python3.12 python3.11 python3.10 python; do + command -v "$py" >/dev/null 2>&1 || continue + if "$py" -E -S -c "$probe" /dev/null 2>&1; then exec "$py" "$dir/te_bootstrap.py" hook "$@" fi [ -n "$old" ] || old=$py diff --git a/tests/test_plugin_package.py b/tests/test_plugin_package.py index 959ab07..ded47e5 100644 --- a/tests/test_plugin_package.py +++ b/tests/test_plugin_package.py @@ -199,8 +199,10 @@ def test_the_launcher_fails_open_without_python(tmp_path): def _old_python(path, marker): - # Fails the launcher's version probe (`-c ...`); anything else it is asked to run, it records. - _script(path, f'case "$1" in -c) exit 1 ;; esac\n: > "{marker}"\nwhile IFS= read -r _l; do :; done\necho "{{}}"') + # Fails the launcher's version probe (`-E -S -c ...`); anything else it is asked to run, it + # records. + _script(path, f'for a in "$@"; do [ "$a" = -c ] && exit 1; done\n: > "{marker}"\n' + 'while IFS= read -r _l; do :; done\necho "{}"') @pytest.mark.skipif(os.name == "nt", reason="the POSIX launcher path") @@ -225,11 +227,27 @@ def test_with_only_an_old_python_the_launcher_still_hands_it_the_hook(tmp_path): _old_python(old, tmp_path / "old_ran") code, out, _ = _run_hook(os.path.join(PLUGIN, "scripts", "te-hook"), {"hook_event_name": "SessionStart"}, - {"PATH": f"{old.parent}:/bin", "TOKENECONOMIZER_HOME": str(tmp_path / "home")}) + {"PATH": str(old.parent), + "TOKENECONOMIZER_HOME": str(tmp_path / "home")}) assert code == 0 and json.loads(out) == {} assert (tmp_path / "old_ran").exists() # so the bootstrap can fail open itself +@pytest.mark.skipif(os.name == "nt", reason="the POSIX launcher path") +def test_te_python_runs_as_named_even_beside_a_newer_python(tmp_path): + """`TE_PYTHON` is the person's own choice: the launcher runs it without a probe, and the + bootstrap says what it needs if it is too old.""" + old, new = tmp_path / "old" / "python3", tmp_path / "new" / "python3.12" + _old_python(old, tmp_path / "old_ran") + _script(new, f'exec "{sys.executable}" "$@"') + code, out, _ = _run_hook(os.path.join(PLUGIN, "scripts", "te-hook"), + {"hook_event_name": "SessionStart"}, + {"PATH": str(new.parent), "TE_PYTHON": str(old), + "TOKENECONOMIZER_HOME": str(tmp_path / "home")}) + assert code == 0 and json.loads(out) == {} + assert (tmp_path / "old_ran").exists() + + def test_the_bootstrap_answers_no_change_on_an_old_python(tmp_path, monkeypatch): sys.path.insert(0, os.path.join(PLUGIN, "scripts")) try: From ee0a73275729abf30842879c7525fc5405d5d2e4 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 10 Nov 2026 10:34:12 -0800 Subject: [PATCH 3/3] Launchers: pass over a name that runs no Python A pyenv shim with no version set exits 127; it is no longer kept as the too-old fallback, so with no other Python the hook still answers {}. Raised in review. Co-Authored-By: Claude Opus 5.5 --- plugin/scripts/te | 15 +++++++++------ plugin/scripts/te-hook | 15 +++++++++------ tests/test_plugin_package.py | 11 +++++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/plugin/scripts/te b/plugin/scripts/te index c3320b4..ecc0f17 100755 --- a/plugin/scripts/te +++ b/plugin/scripts/te @@ -35,8 +35,10 @@ else # TE_PYTHON is the person's own choice: it runs as named. Otherwise the first python3 on PATH # can be older than the bootstrap's minimum (macOS /usr/bin/python3 is 3.9, and an agent's # hook PATH is often not a login shell's), which would make the plugin a silent no-op with a - # newer Python installed: take the first one new enough. With none, run the first one found, - # so the bootstrap still fails open / says what it needs. The probe skips `site` (-E -S). + # newer Python installed: take the first one new enough. With none, run the first one too old + # (the probe exits 1), so the bootstrap still fails open / says what it needs; a name that + # runs no Python (a pyenv shim with no version set exits 127) is passed over. The probe + # skips `site` (-E -S). if [ -n "${TE_PYTHON:-}" ] && command -v "$TE_PYTHON" >/dev/null 2>&1; then exec "$TE_PYTHON" "$dir/te_bootstrap.py" cli "$@" fi @@ -44,10 +46,11 @@ else old= for py in python3 python3.15 python3.14 python3.13 python3.12 python3.11 python3.10 python; do command -v "$py" >/dev/null 2>&1 || continue - if "$py" -E -S -c "$probe" /dev/null 2>&1; then - exec "$py" "$dir/te_bootstrap.py" cli "$@" - fi - [ -n "$old" ] || old=$py + "$py" -E -S -c "$probe" /dev/null 2>&1 + case $? in + 0) exec "$py" "$dir/te_bootstrap.py" cli "$@" ;; + 1) [ -n "$old" ] || old=$py ;; + esac done [ -n "$old" ] && exec "$old" "$dir/te_bootstrap.py" cli "$@" fi diff --git a/plugin/scripts/te-hook b/plugin/scripts/te-hook index dab0128..94da312 100755 --- a/plugin/scripts/te-hook +++ b/plugin/scripts/te-hook @@ -39,8 +39,10 @@ else # TE_PYTHON is the person's own choice: it runs as named. Otherwise the first python3 on PATH # can be older than the bootstrap's minimum (macOS /usr/bin/python3 is 3.9, and an agent's # hook PATH is often not a login shell's), which would make the plugin a silent no-op with a - # newer Python installed: take the first one new enough. With none, run the first one found, - # so the bootstrap still fails open / says what it needs. The probe skips `site` (-E -S). + # newer Python installed: take the first one new enough. With none, run the first one too old + # (the probe exits 1), so the bootstrap still fails open / says what it needs; a name that + # runs no Python (a pyenv shim with no version set exits 127) is passed over. The probe + # skips `site` (-E -S). if [ -n "${TE_PYTHON:-}" ] && command -v "$TE_PYTHON" >/dev/null 2>&1; then exec "$TE_PYTHON" "$dir/te_bootstrap.py" hook "$@" fi @@ -48,10 +50,11 @@ else old= for py in python3 python3.15 python3.14 python3.13 python3.12 python3.11 python3.10 python; do command -v "$py" >/dev/null 2>&1 || continue - if "$py" -E -S -c "$probe" /dev/null 2>&1; then - exec "$py" "$dir/te_bootstrap.py" hook "$@" - fi - [ -n "$old" ] || old=$py + "$py" -E -S -c "$probe" /dev/null 2>&1 + case $? in + 0) exec "$py" "$dir/te_bootstrap.py" hook "$@" ;; + 1) [ -n "$old" ] || old=$py ;; + esac done [ -n "$old" ] && exec "$old" "$dir/te_bootstrap.py" hook "$@" fi diff --git a/tests/test_plugin_package.py b/tests/test_plugin_package.py index ded47e5..bc99e86 100644 --- a/tests/test_plugin_package.py +++ b/tests/test_plugin_package.py @@ -233,6 +233,17 @@ def test_with_only_an_old_python_the_launcher_still_hands_it_the_hook(tmp_path): assert (tmp_path / "old_ran").exists() # so the bootstrap can fail open itself +@pytest.mark.skipif(os.name == "nt", reason="the POSIX launcher path") +def test_a_shim_that_runs_no_python_is_passed_over(tmp_path): + """A pyenv shim with no version set exits 127 and runs nothing: with no other Python on + PATH, the launcher answers no change itself.""" + shim = tmp_path / "shims" / "python3" + _script(shim, 'echo "pyenv: python3: command not found" >&2\nexit 127') + code, out, _ = _run_hook(os.path.join(PLUGIN, "scripts", "te-hook"), + {"hook_event_name": "SessionStart"}, {"PATH": str(shim.parent)}) + assert code == 0 and json.loads(out) == {} + + @pytest.mark.skipif(os.name == "nt", reason="the POSIX launcher path") def test_te_python_runs_as_named_even_beside_a_newer_python(tmp_path): """`TE_PYTHON` is the person's own choice: the launcher runs it without a probe, and the