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
2 changes: 1 addition & 1 deletion plugins/e2a-labs/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/e2a-labs/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion plugins/e2a-labs/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/e2a-labs/plugin.meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/e2a-labs/skills/tether/hooks/tether-notify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
52 changes: 37 additions & 15 deletions plugins/e2a-labs/skills/tether/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -112,7 +134,7 @@ TETHER_MIN_CLI="2.0.0"

# t_ver_ge "<e2a 2.0.1>" "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]
Expand All @@ -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 "<e2a 2.0.1>" → 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]
Expand Down Expand Up @@ -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
Expand All @@ -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"
}
Expand All @@ -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={}
Expand Down Expand Up @@ -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)
Expand All @@ -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"))
Expand All @@ -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)
Expand Down Expand Up @@ -401,7 +423,7 @@ T_ATTACH_MAX_BYTES=$((15 * 1024 * 1024))
# t_attach_check <file>... → 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):
Expand Down Expand Up @@ -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
Expand All @@ -517,14 +539,14 @@ print((datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(seconds=i

t_seen_has() { # <id> → 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() { # <id> → 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={}
Expand Down
40 changes: 34 additions & 6 deletions plugins/e2a-labs/skills/tether/tether.sh
Original file line number Diff line number Diff line change
Expand Up @@ -327,15 +327,15 @@ 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
# `agentEmail`, not the pre-2.0.0 `agentAddress`: CLI 2.0.0 renamed
# 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."
Expand All @@ -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; }
Expand Down Expand Up @@ -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 '')" ""
Expand Down Expand Up @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion scripts/plugin-packaging.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
});

Expand Down
Loading