From 2b8803f77723cb909b7aca69f4e2e2ed0cc49e86 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Tue, 20 Oct 2026 23:19:35 -0400 Subject: [PATCH] fix(installer): disable dependency hooks explicitly across Bun versions --- tests/test_install_portal_hooks.py | 94 ++++++++++++++++++++++++++++++ tools/install_portal_clis.py | 5 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tests/test_install_portal_hooks.py diff --git a/tests/test_install_portal_hooks.py b/tests/test_install_portal_hooks.py new file mode 100644 index 00000000..6888c541 --- /dev/null +++ b/tests/test_install_portal_hooks.py @@ -0,0 +1,94 @@ +"""Real Bun install against an offline npm registry: dependency hooks stay off. + +The fixture has the same ordinary npm dependency shape as esbuild, whose +postinstall Bun 1.2 runs by default. It writes only a local marker. No public +registry, credentials or lifecycle-script runner is mocked. +""" +import base64 +import hashlib +import http.server +import io +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tarfile +from tempfile import TemporaryDirectory +import threading +import unittest + +ROOT = Path(__file__).resolve().parent.parent +BUN = os.environ.get("TEST_BUN") or shutil.which("bun") + + +@unittest.skipUnless(BUN, "requires Bun for the offline package-manager integration") +class DependencyHooks(unittest.TestCase): + def test_installing_dependency_does_not_execute_its_hook(self): + package = {"name": "esbuild", "version": "0.25.0", "scripts": {"postinstall": "bun run hook.js"}} + archive = io.BytesIO() + with tarfile.open(fileobj=archive, mode="w:gz") as tar: + for name, data in { + "package.json": json.dumps(package).encode(), + "hook.js": b'await Bun.write("hook-ran.txt", "ran");', + }.items(): + entry = tarfile.TarInfo("package/" + name) + entry.size = len(data) + tar.addfile(entry, io.BytesIO(data)) + blob = archive.getvalue() + + class Registry(http.server.BaseHTTPRequestHandler): + def do_GET(self): + if self.path.endswith(".tgz"): + body = blob + else: + metadata = {**package, "dist": { + "tarball": f"http://127.0.0.1:{self.server.server_port}/esbuild.tgz", + "integrity": "sha512-" + base64.b64encode(hashlib.sha512(blob).digest()).decode(), + }} + body = json.dumps({"name": "esbuild", "dist-tags": {"latest": "0.25.0"}, + "versions": {"0.25.0": metadata}}).encode() + self.send_response(200) + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, *args): + pass + + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Registry) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + with TemporaryDirectory() as directory: + root = Path(directory) + tools = root / "tools" + tools.mkdir() + for name in ("install_portal_clis.py", "security_guards.py"): + shutil.copyfile(ROOT / "tools" / name, tools / name) + cli = root / ".agents/skills/example-search/cli" + cli.mkdir(parents=True) + (cli / "package.json").write_text(json.dumps({ + "name": "example-search", "private": True, "dependencies": {"esbuild": "0.25.0"}, + }), encoding="utf-8") + (cli / ".npmrc").write_text(f"registry=http://127.0.0.1:{server.server_port}/\n", encoding="utf-8") + env = {"PATH": str(Path(BUN).absolute().parent) + os.pathsep + os.defpath, + "HOME": str(root), "TMPDIR": str(root), "TEMP": str(root), "TMP": str(root), + "BUN_INSTALL_CACHE_DIR": str(root / "cache")} + if "SYSTEMROOT" in os.environ: + env["SYSTEMROOT"] = os.environ["SYSTEMROOT"] + proc = subprocess.run([sys.executable, str(tools / "install_portal_clis.py")], + env=env, capture_output=True, text=True, timeout=30) + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + dependency = cli / "node_modules/esbuild" + self.assertTrue((dependency / "hook.js").is_file(), "the dependency was actually installed") + self.assertFalse((dependency / "hook-ran.txt").exists(), "the dependency install hook ran") + finally: + server.shutdown() + server.server_close() + thread.join() + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/install_portal_clis.py b/tools/install_portal_clis.py index bb3a329a..732b989c 100644 --- a/tools/install_portal_clis.py +++ b/tools/install_portal_clis.py @@ -18,6 +18,9 @@ skipped and reported instead of installed: those are the hooks that run code during `bun install`, and tools/security_guards.py forbids them in-tree. A borrowed portal that carries one gets read by a human before it runs. +Every install also passes --ignore-scripts, including on older Bun releases +that run a built-in list of dependency hooks without trustedDependencies. + Each CLI installs independently; one failure does not stop the rest. Exit 0 when every CLI installed, 1 when bun is missing, a manifest was skipped, or an install failed. @@ -72,7 +75,7 @@ def install(cli_dirs: list[Path], bun: str, dry_run: bool, root: Path = ROOT, ru print(f"would install {name}") continue print(f"installing {name}", flush=True) - if run([bun, "install"], cwd=cli_dir).returncode != 0: + if run([bun, "install", "--ignore-scripts"], cwd=cli_dir).returncode != 0: print(f"FAIL {name}: bun install exited non-zero") failures.append(name) if failures: