From e6b4e219bea573359d7949d78f7963e80f5cc8a9 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Sat, 19 Sep 2026 15:58:13 +1000 Subject: [PATCH] fix(install): Codex cleanup drops hand-wired twins; pip3 fallback explains an out-of-range Python Two more things the same incident showed. The user's ~/.codex/config.toml held a second table, `treg-to_mcp`, pointing at our MCP url with the token in the env-var-NAME field; the installer now removes any `mcp_servers.*` table whose url is ours before writing the one correct table, so Codex is not left showing a dead server beside the working one. install.sh's pip3 fallback ran on a machine whose pip3 belonged to a Python outside the supported range and died on pip's "Ignored the following versions" wall. pip cannot pick another interpreter, so the script now says which Python pip3 belongs to and prints the uv one-liner instead. Fragment: docs/context/interface/landing-sandbox.md. Co-Authored-By: Claude Fable 5.1 --- docs/context/interface/landing-sandbox.md | 5 ++++- src/treg/mcp_install.py | 16 +++++++++++++--- src/treg/web/install.sh | 8 ++++++++ tests/test_mcp_install.py | 3 +++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/context/interface/landing-sandbox.md b/docs/context/interface/landing-sandbox.md index fdfa7021..3a270e9d 100644 --- a/docs/context/interface/landing-sandbox.md +++ b/docs/context/interface/landing-sandbox.md @@ -144,7 +144,10 @@ files a skill folder is — `SKILL.md` (agent recipe: call the treg proxy, key i supported interpreter range (`PYREQ`, kept in sync with `requires-python` in `pyproject.toml`): without it uv resolves against its *default* interpreter — its own managed Python first — and a machine whose default falls outside the range fails resolution instead of picking (or auto-downloading) a compatible -one. It also installs the official +one. The pip3 path cannot pick an interpreter at all, so it checks `pip3 --version` against the range +first and, outside it, prints which Python pip3 belongs to plus the uv one-liner instead of letting +pip fail with its "Ignored the following versions" wall (a real user hit exactly that). It also +installs the official **tools-registry skill** into every detected agent via `treg skill bootstrap` (Claude Code, Cursor, Codex, Gemini, Copilot, OpenCode, Windsurf …), falling back on older CLIs to a Claude-only drop that curls `{BASE}/skill.md` into `~/.claude/skills/treg`. Because the package is public on PyPI, diff --git a/src/treg/mcp_install.py b/src/treg/mcp_install.py index 810077d8..70d0331b 100644 --- a/src/treg/mcp_install.py +++ b/src/treg/mcp_install.py @@ -155,12 +155,22 @@ def _write_toml_agent(meta: dict, name: str, url: str, token: str) -> tuple[str, old = path.read_text() if path.exists() else "" tomllib.loads(old) # a config Codex can't read is not ours to edit header = f"[mcp_servers.{name}]" - kept = re.sub(rf"(?ms)^\[mcp_servers\.{re.escape(name)}\]\n.*?(?=^\[|\Z)", "", old).rstrip() + # Cut our table, and any other table pointing at this same MCP url under another name — an + # agent wiring Codex by hand tends to leave a `treg-to_mcp` twin with the token in the wrong + # field, which Codex then shows as a dead server beside the working one. + stale = {name} | {k for k, v in tomllib.loads(old).get("mcp_servers", {}).items() + if isinstance(v, dict) and v.get("url") == url} + kept = old + for k in stale: + kept = re.sub(rf"(?ms)^\[mcp_servers\.{re.escape(k)}\]\n.*?(?=^\[|\Z)", "", kept) + kept = kept.rstrip() block = (f"{header}\nurl = {json.dumps(url)}\n" f"http_headers = {{ \"Authorization\" = {json.dumps('Bearer ' + token)} }}\n") new = (kept + "\n\n" if kept else "") + block - entry = tomllib.loads(new)["mcp_servers"][name] - if entry != {"url": url, "http_headers": {"Authorization": f"Bearer {token}"}}: + servers = tomllib.loads(new)["mcp_servers"] + if (servers[name] != {"url": url, "http_headers": {"Authorization": f"Bearer {token}"}} + or any(k != name and isinstance(v, dict) and v.get("url") == url + for k, v in servers.items())): return "error", f"{path}: a sub-table of {header} survived; edit it by hand" _write_private(path, new) return "ok", str(path) diff --git a/src/treg/web/install.sh b/src/treg/web/install.sh index 492e48e9..20d0b350 100644 --- a/src/treg/web/install.sh +++ b/src/treg/web/install.sh @@ -52,6 +52,14 @@ elif command -v pipx >/dev/null 2>&1; then pipx install --force "$SRC" fi elif command -v pip3 >/dev/null 2>&1; then + # pip can only install into the interpreter it belongs to. Outside PYREQ it prints a wall of + # "Ignored the following versions" and dies — say what is wrong and how to fix it instead. + if ! pip3 --version 2>/dev/null | grep -qE '\(python 3\.1[23]\)'; then + echo "pip3 belongs to $(pip3 --version 2>/dev/null | sed -n 's/.*(python \([0-9.]*\)).*/Python \1/p'), but treg needs Python 3.12 or 3.13." >&2 + echo "Easiest fix — install uv (it fetches a matching Python by itself), then rerun this command:" >&2 + echo " curl -LsSf https://astral.sh/uv/install.sh | sh" >&2 + exit 1 + fi pip3 install --user --upgrade "$SRC" else echo "Need Python 3.12 or 3.13 and one of: uv (recommended), pipx, or pip3." >&2 diff --git a/tests/test_mcp_install.py b/tests/test_mcp_install.py index e536f724..04ef4b30 100644 --- a/tests/test_mcp_install.py +++ b/tests/test_mcp_install.py @@ -403,6 +403,8 @@ def test_codex_replaces_a_stale_env_var_table_and_keeps_the_rest(tmp_path, monke import tomllib before = ('model = "gpt-5"\n\n[mcp_servers.figma]\nurl = "https://mcp.figma.com/mcp"\n\n' '[mcp_servers.treg]\nurl = "https://treg.to/mcp/"\nbearer_token_env_var = "TREG_TOKEN"\n\n' + # a hand-wired twin under another name, token in the env-var-NAME field (seen in the wild) + '[mcp_servers.treg-to_mcp]\nurl = "https://treg.to/mcp/"\nbearer_token_env_var = "eyJ.x"\n\n' '[mcp_servers.node_repl]\ncommand = "node_repl"\n\n[mcp_servers.node_repl.env]\nX = "1"\n') out, cfg = _codex_install(tmp_path, monkeypatch, existing=before, token="NEWKEY") assert out["results"][0][1] == "ok", out @@ -413,6 +415,7 @@ def test_codex_replaces_a_stale_env_var_table_and_keeps_the_rest(tmp_path, monke assert data["mcp_servers"]["treg"] == { "url": "https://treg.to/mcp/", "http_headers": {"Authorization": "Bearer NEWKEY"}} assert cfg.read_text().count("[mcp_servers.treg]") == 1 + assert "treg-to_mcp" not in cfg.read_text() and "eyJ.x" not in cfg.read_text() # idempotent mcp_install.install_mcp(base_url="https://treg.to", token="NEWKEY", only=["codex"]) assert cfg.read_text().count("[mcp_servers.treg]") == 1