diff --git a/plugins/e2a-labs/.claude-plugin/plugin.json b/plugins/e2a-labs/.claude-plugin/plugin.json index 68258e2fd..be1a71a15 100644 --- a/plugins/e2a-labs/.claude-plugin/plugin.json +++ b/plugins/e2a-labs/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "e2a-labs", "displayName": "e2a Labs", - "version": "0.3.1", + "version": "0.3.2", "description": "Experimental autonomous workflows for the e2a agent email gateway. Requires the core e2a plugin for MCP tools and authentication.", "author": { "name": "TokenCanopy", diff --git a/plugins/e2a-labs/.codex-plugin/plugin.json b/plugins/e2a-labs/.codex-plugin/plugin.json index 9f9258f31..2a982e7ea 100644 --- a/plugins/e2a-labs/.codex-plugin/plugin.json +++ b/plugins/e2a-labs/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "e2a-labs", "displayName": "e2a Labs", - "version": "0.3.1", + "version": "0.3.2", "description": "Experimental autonomous workflows for e2a. Install the core e2a plugin first; it supplies the MCP connection and authentication.", "author": { "name": "TokenCanopy" diff --git a/plugins/e2a-labs/plugin.json b/plugins/e2a-labs/plugin.json index 7f55fe30a..230be0a33 100644 --- a/plugins/e2a-labs/plugin.json +++ b/plugins/e2a-labs/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "e2a-labs", - "version": "0.3.1", + "version": "0.3.2", "description": "Experimental autonomous workflows for the e2a agent email gateway. Requires the core e2a plugin for MCP tools and authentication.", "author": { "name": "TokenCanopy", diff --git a/plugins/e2a-labs/plugin.meta.json b/plugins/e2a-labs/plugin.meta.json index 946e3a67c..adedead9e 100644 --- a/plugins/e2a-labs/plugin.meta.json +++ b/plugins/e2a-labs/plugin.meta.json @@ -3,7 +3,7 @@ "name": "e2a-labs", "displayName": "e2a Labs", - "version": "0.3.1", + "version": "0.3.2", "license": "Apache-2.0", "homepage": "https://e2a.dev", "repository": "https://github.com/tokencanopy/e2a", diff --git a/plugins/e2a-labs/skills/tether/hooks/tether-notify.sh b/plugins/e2a-labs/skills/tether/hooks/tether-notify.sh index 3c7ff8b07..0850ab0b2 100755 --- a/plugins/e2a-labs/skills/tether/hooks/tether-notify.sh +++ b/plugins/e2a-labs/skills/tether/hooks/tether-notify.sh @@ -10,7 +10,7 @@ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" . "${here}/../lib.sh" payload="$(cat)" -message="$(printf '%s' "$payload" | python3 -c 'import json,sys +message="$(printf '%s' "$payload" | t_python -c 'import json,sys try:print(json.load(sys.stdin).get("message","") or "") except Exception:print("")')" [ -n "$message" ] || message="The agent needs your attention." diff --git a/plugins/e2a-labs/skills/tether/lib.sh b/plugins/e2a-labs/skills/tether/lib.sh index e0aa0a186..4eaef615b 100755 --- a/plugins/e2a-labs/skills/tether/lib.sh +++ b/plugins/e2a-labs/skills/tether/lib.sh @@ -10,7 +10,29 @@ # → `npx -y @e2a/cli@^MIN`. Node is always present where this skill runs # (Claude Code requires it), so existing users need to install nothing and # minor CLI upgrades flow automatically through npx. Python 3 is still needed -# for local state/JSON handling only. +# for local state/JSON handling only, resolved through t_python below. + +# t_python resolves and memoizes a Python 3 that actually EXECUTES, trying +# $E2A_PYTHON, then python3, then python. +T_PYTHON="" +t_python() { + if [ -z "$T_PYTHON" ]; then + local c + for c in "${E2A_PYTHON:-}" python3 python; do + [ -n "$c" ] || continue + if command -v "$c" >/dev/null 2>&1 \ + && "$c" -c 'import sys; sys.exit(0 if sys.version_info[0] == 3 else 1)' >/dev/null 2>&1; then + T_PYTHON="$(command -v "$c")" + break + fi + done + if [ -z "$T_PYTHON" ]; then + echo "tether: no working Python 3 found (tried \$E2A_PYTHON, python3, python), set E2A_PYTHON to a working interpreter" >&2 + return 1 + fi + fi + "$T_PYTHON" "$@" +} t_load_config() { # Explicit env vars win; each fallback source fills only the vars still missing @@ -34,7 +56,7 @@ t_load_config() { # already-resolved credential from ~/.e2a-tether.env is never clobbered by # a *different* agent the CLI happens to be logged in as. if { [ -z "${E2A_API_KEY:-}" ] || [ -z "${E2A_AGENT_EMAIL:-}" ] || [ -z "${E2A_URL:-}" ]; } && [ -f "${HOME}/.e2a/config.json" ]; then - eval "$(python3 -c 'import json,shlex,os + eval "$(t_python -c 'import json,shlex,os try: d=json.load(open(os.path.expanduser("~/.e2a/config.json"))) if not os.environ.get("E2A_API_KEY") and d.get("api_key"): @@ -83,7 +105,7 @@ except Exception:pass')" [ -n "${E2A_API_KEY:-}" ] && [ -n "${E2A_AGENT_EMAIL:-}" ] } -t_now_iso() { python3 -c 'import datetime;print(datetime.datetime.now(datetime.timezone.utc).isoformat())'; } +t_now_iso() { t_python -c 'import datetime;print(datetime.datetime.now(datetime.timezone.utc).isoformat())'; } # --- e2a CLI resolution -------------------------------------------------------- # The minimum CLI this skill's flags require (send --conversation-id, reply @@ -112,7 +134,7 @@ TETHER_MIN_CLI="2.0.0" # t_ver_ge "" "2.0.0" → 0 when the version (last token) >= min. t_ver_ge() { - python3 -c 'import sys + t_python -c 'import sys def v(s): s = s.strip().split()[-1] if s.strip() else "0" return [int(x) for x in s.lstrip("v").split(".")[:3] if x.isdigit()] or [0] @@ -121,7 +143,7 @@ sys.exit(0 if v(sys.argv[1]) >= v(sys.argv[2]) else 1)' "$1" "$2" 2>/dev/null # t_ver_major "" → 2 (0 when unparseable). t_ver_major() { - python3 -c 'import sys + t_python -c 'import sys s = sys.argv[1].strip() s = s.split()[-1] if s else "0" p = s.lstrip("v").split(".")[0] @@ -221,7 +243,7 @@ t_cli_desc() { t_state_key() { local root root="$(git rev-parse --show-toplevel 2>/dev/null)" || root="$PWD" - python3 -c 'import hashlib,sys;print(hashlib.sha1(sys.argv[1].encode()).hexdigest()[:12])' "$root" + t_python -c 'import hashlib,sys;print(hashlib.sha1(sys.argv[1].encode()).hexdigest()[:12])' "$root" } T_STATE_PATH="" # memoized: t_state_path runs in every poll tick @@ -240,7 +262,7 @@ t_state_path() { t_state_get() { local f; f="$(t_state_path)"; [ -f "$f" ] || return 0 - python3 -c 'import json,sys + t_python -c 'import json,sys try:print(json.load(open(sys.argv[1])).get(sys.argv[2],"") or "") except Exception:pass' "$f" "$1" } @@ -253,7 +275,7 @@ except Exception:pass' "$f" "$1" # entry re-executes an already-handled instruction). t_state_set() { # t_state_set k1 v1 [k2 v2 ...] local f; f="$(t_state_path)"; mkdir -p "$(dirname "$f")" - python3 -c 'import json,sys,os,fcntl + t_python -c 'import json,sys,os,fcntl f=sys.argv[1];kv=sys.argv[2:] lock=open(f+".lock","w"); fcntl.flock(lock,fcntl.LOCK_EX) d={} @@ -304,7 +326,7 @@ t_ask_active() { # treating a mistyped "1h30m"/"90 min" as an unbounded window). # accepts a SINGLE unit: 30m, 2h, 8h, 1d ; "" / forever / until-stop → empty t_duration_to_expiry() { - python3 -c 'import sys,datetime,re + t_python -c 'import sys,datetime,re d=(sys.argv[1] if len(sys.argv)>1 else "").strip().lower() if not d or d in ("forever","none","off","until-stop","stop"):print("");raise SystemExit m=re.fullmatch(r"(\d+)\s*([mhd])",d) @@ -314,7 +336,7 @@ print((datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(seconds=s } t_parse_until() { - python3 -c 'import datetime,sys + t_python -c 'import datetime,sys try: raw=sys.argv[1] value=datetime.datetime.fromisoformat(raw.replace("Z", "+00:00")) @@ -330,7 +352,7 @@ except Exception: t_remaining_seconds() { local f; f="$(t_state_path)" [ -f "$f" ] || { echo 2147483647; return; } - python3 -c 'import sys,datetime,json,os,fcntl + t_python -c 'import sys,datetime,json,os,fcntl try: f=sys.argv[1] lock=open(f+".lock","w"); fcntl.flock(lock,fcntl.LOCK_EX) @@ -401,7 +423,7 @@ T_ATTACH_MAX_BYTES=$((15 * 1024 * 1024)) # t_attach_check ... → 0 ok; 3 a file is missing; 4 over the total cap. # Validates BEFORE encoding so callers can fail fast with a clear message. t_attach_check() { - python3 -c 'import sys,os + t_python -c 'import sys,os maxb=int(sys.argv[1]); total=0 for f in sys.argv[2:]: if not os.path.isfile(f): @@ -492,7 +514,7 @@ t_ws_wait() { conv="$(t_state_get conversation_id)" # No conversation = stopped session or torn state — never wait unfiltered. if [ -z "$conv" ]; then sleep "${E2A_TETHER_POLL_INTERVAL:-20}"; return 0; fi - until="$(python3 -c 'import sys,datetime + until="$(t_python -c 'import sys,datetime print((datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(seconds=int(sys.argv[1]))).isoformat())' "$secs")" t0=$SECONDS # Raise the transport deadline past this wait's own window, else the @@ -517,14 +539,14 @@ print((datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(seconds=i t_seen_has() { # → 0 if already processed local f; f="$(t_state_path)"; [ -f "$f" ] || return 1 - python3 -c 'import json,sys + t_python -c 'import json,sys try:sys.exit(0 if sys.argv[2] in (json.load(open(sys.argv[1])).get("seen") or []) else 1) except Exception:sys.exit(1)' "$f" "$1" } t_seen_add() { # → record as processed (cap at last 500); flocked + atomic local f; f="$(t_state_path)"; mkdir -p "$(dirname "$f")" - python3 -c 'import json,sys,os,fcntl + t_python -c 'import json,sys,os,fcntl f,i=sys.argv[1],sys.argv[2] lock=open(f+".lock","w"); fcntl.flock(lock,fcntl.LOCK_EX) d={} diff --git a/plugins/e2a-labs/skills/tether/tether.sh b/plugins/e2a-labs/skills/tether/tether.sh index 14473cc6f..a2fe7718a 100755 --- a/plugins/e2a-labs/skills/tether/tether.sh +++ b/plugins/e2a-labs/skills/tether/tether.sh @@ -327,7 +327,7 @@ question or instruction; reply \"stop\" to end early. exit 1 fi rm -f "$errf" - scope="$(printf '%s' "$who" | python3 -c 'import json,sys + scope="$(printf '%s' "$who" | t_python -c 'import json,sys try:print(json.load(sys.stdin).get("scope","")) except Exception:print("")')" if [ "$scope" = "agent" ]; then @@ -335,7 +335,7 @@ except Exception:print("")')" # AccountView.agent_address → agent_email as part of the agent-reference # unification. Reading the old name silently yielded an empty string, so # this line reported "already holds an agent-scoped key ()". - bound="$(printf '%s' "$who" | python3 -c 'import json,sys + bound="$(printf '%s' "$who" | t_python -c 'import json,sys try:print(json.load(sys.stdin).get("agentEmail","")) except Exception:print("")')" echo "tether setup: the CLI already holds an agent-scoped key (${bound}) — nothing to mint." @@ -358,14 +358,14 @@ except Exception:print("")')" if [ -z "$inbox" ]; then sd="$(t_cli config get shared_domain 2>/dev/null)" [ -n "$sd" ] || { echo "tether setup: no shared domain on this deployment — pass --email you@yourdomain"; exit 1; } - inbox="tether-$(python3 -c 'import secrets;print(secrets.token_hex(3))')@${sd}" + inbox="tether-$(t_python -c 'import secrets;print(secrets.token_hex(3))')@${sd}" fi if ! t_cli agents get "$inbox" >/dev/null 2>&1; then echo "tether setup: creating ${inbox}…" t_cli agents create "$inbox" --name "tether" >/dev/null || { echo "tether setup: agent create failed (slug taken/invalid?)"; exit 1; } fi - kjson="$(t_cli keys create --agent "$inbox" --name "tether-$(python3 -c 'import secrets;print(secrets.token_hex(2))')" --json 2>/dev/null)" - agtkey="$(printf '%s' "$kjson" | python3 -c 'import json,sys + kjson="$(t_cli keys create --agent "$inbox" --name "tether-$(t_python -c 'import secrets;print(secrets.token_hex(2))')" --json 2>/dev/null)" + agtkey="$(printf '%s' "$kjson" | t_python -c 'import json,sys try:print(json.load(sys.stdin).get("key","") or "") except Exception:print("")')" [ -n "$agtkey" ] || { echo "tether setup: could not mint an agent-scoped key — aborting (NOT storing the broad account key)"; exit 1; } @@ -425,6 +425,34 @@ except Exception:print("")')" fail=0 ck() { if [ "$2" = "$3" ]; then echo "ok: $1"; else echo "FAIL: $1 (want [$3] got [$2])"; fail=1; fi; } + echo "# python interpreter resolution (Windows/Git Bash python3 shim):" + ( fakebin=/tmp/tether-selftest-fakepy; rm -rf "$fakebin"; mkdir -p "$fakebin" + real_py="$(command -v python3)" + # Mirrors the Microsoft Store App Installer redirector shim: on PATH + # (so `command -v python3` succeeds) but exits non-zero with no output + # on every invocation. + printf '#!/usr/bin/env bash\nexit 49\n' > "$fakebin/python3"; chmod +x "$fakebin/python3" + ln -s "$real_py" "$fakebin/python" + export PATH="$fakebin:$PATH" + T_PYTHON="" + ts="$(t_now_iso)" + case "$ts" in *T*:*:*) : ;; *) + echo "FAIL: t_now_iso silently failed when python3 is a broken shim but 'python' works (got [$ts])"; exit 1;; esac + echo "ok: t_now_iso falls back to a working 'python' when python3 is a non-functional shim" ) || fail=1 + ( fakebin=/tmp/tether-selftest-fakepy2; rm -rf "$fakebin"; mkdir -p "$fakebin" + # Shadow BOTH names with the broken shim so no fallback resolves, + # isolating the "nothing works" branch from the "python still works" + # branch tested above. + printf '#!/usr/bin/env bash\nexit 49\n' > "$fakebin/python3"; chmod +x "$fakebin/python3" + cp "$fakebin/python3" "$fakebin/python" + PATH="$fakebin" + export PATH + T_PYTHON="" + diag="$(t_now_iso 2>&1 1>/dev/null)" + [ -n "$diag" ] || { echo "FAIL: no working Python 3 anywhere fails completely silently (no diagnostic)"; exit 1; } + echo "ok: no working Python 3 anywhere fails loud with a diagnostic, not silently" ) || fail=1 + rm -rf /tmp/tether-selftest-fakepy /tmp/tether-selftest-fakepy2 + echo "# duration parser:" ck "2h parses" "$([ -n "$(t_duration_to_expiry 2h)" ] && echo good)" "good" ck "'' → until-stop" "$(t_duration_to_expiry '')" "" @@ -543,7 +571,7 @@ except Exception:print("")')" t_attach_check "$af" || { echo "FAIL: attach check on a real file"; fail=1; } t_attach_check /nonexistent-tether-file 2>/dev/null; ck "missing file → exit 3" "$?" "3" big=/tmp/tether-selftest-big.bin - python3 -c 'f=open("/tmp/tether-selftest-big.bin","wb");f.seek(16*1024*1024-1);f.write(b"\0")' + t_python -c 'f=open("/tmp/tether-selftest-big.bin","wb");f.seek(16*1024*1024-1);f.write(b"\0")' t_attach_check "$big" 2>/dev/null; ck "16 MB → exit 4 (over cap)" "$?" "4" rm -f "$af" "$big" diff --git a/scripts/plugin-packaging.test.mjs b/scripts/plugin-packaging.test.mjs index 473976bbb..000bca856 100644 --- a/scripts/plugin-packaging.test.mjs +++ b/scripts/plugin-packaging.test.mjs @@ -64,7 +64,7 @@ test("marketplaces expose the supported plugin set and release versions", async } for (const client of [".claude-plugin", ".codex-plugin"]) { const labs = JSON.parse(await readFile(`plugins/e2a-labs/${client}/plugin.json`, "utf8")); - assert.equal(labs.version, "0.3.1"); + assert.equal(labs.version, "0.3.2"); } });