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
23 changes: 20 additions & 3 deletions plugin/scripts/te
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,27 @@ if [ "${OS:-}" = Windows_NT ]; then
exec "$py" "$dir/te_bootstrap.py" cli "$@"
done
else
for py in "${TE_PYTHON:-}" python3 python; do
[ -n "$py" ] && command -v "$py" >/dev/null 2>&1 || continue
exec "$py" "$dir/te_bootstrap.py" cli "$@"
# 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 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
probe='import sys; sys.exit(sys.version_info < (3, 10))'
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
"$py" -E -S -c "$probe" </dev/null >/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
echo "te: no python3 on PATH (set TE_PYTHON)" >&2
exit 1
26 changes: 22 additions & 4 deletions plugin/scripts/te-hook
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -35,10 +36,27 @@ if [ "${OS:-}" = Windows_NT ]; then
exec "$py" "$dir/te_bootstrap.py" hook "$@"
done
else
for py in "${TE_PYTHON:-}" python3 python; do
[ -n "$py" ] && command -v "$py" >/dev/null 2>&1 || continue
exec "$py" "$dir/te_bootstrap.py" hook "$@"
# 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 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
probe='import sys; sys.exit(sys.version_info < (3, 10))'
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
"$py" -E -S -c "$probe" </dev/null >/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
# Drain stdin with a builtin (no `cat` when PATH is empty), then answer "no change".
while IFS= read -r _line; do :; done
Expand Down
61 changes: 61 additions & 0 deletions tests/test_plugin_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,67 @@ 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 (`-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")
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": 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_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
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:
Expand Down
Loading