From fcc42731b4bf3261d604ef78c7fadac30142fa16 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 07:34:47 +0000 Subject: [PATCH 01/10] Align plugin hooks with current abxpkg API and fix dangling-symlink chmod - Replace the removed ``provider.INSTALLER_BIN_ABSPATH`` attribute access in apt/brew/cargo/npm/pip hook scripts with a ``try: provider.INSTALLER_BINARY(); except Exception: ...`` gate so hooks still skip cleanly when the installer binary is missing. - Replace removed PuppeteerProvider ``browser_cache_dir`` / ``browser_bin_dir`` and ChromeWebstoreProvider ``extensions_dir`` / NpmProvider ``npm_prefix`` / PipProvider ``pip_venv`` kwargs with the current ``install_root`` / ``bin_dir`` fields so hooks pass pyright without relying on validation-alias-only names. - enforce_lib_permissions: use ``lstat()`` and skip symlinks so a dangling symlink in ``lib/env/bin/`` doesn't crash the post-install lockdown pass. - pyproject: add ``abx-plugins = "1 second"`` to ``exclude-newer-package`` so hook scripts can resolve a fresh self-ref without the 14-day cutoff pinning to an old ``abx-pkg``-named dependency. --- abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py | 4 +++- abx_plugins/plugins/base/utils.py | 9 +++++++-- abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py | 5 +---- .../plugins/cargo/on_BinaryRequest__12_cargo.py | 4 +++- .../on_BinaryRequest__90_chromewebstore.py | 2 +- abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py | 6 ++++-- abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py | 6 ++++-- .../puppeteer/on_BinaryRequest__12_puppeteer.py | 10 +++++----- pyproject.toml | 2 +- 9 files changed, 29 insertions(+), 19 deletions(-) diff --git a/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py b/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py index e0add4a..0e7732d 100755 --- a/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py +++ b/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py @@ -50,7 +50,9 @@ def main( # Use abxpkg AptProvider to install binary provider = AptProvider() - if not provider.INSTALLER_BIN_ABSPATH: + try: + provider.INSTALLER_BINARY() + except Exception: click.echo( "AptProvider.INSTALLER_BIN is not available on this host", err=True, diff --git a/abx_plugins/plugins/base/utils.py b/abx_plugins/plugins/base/utils.py index 7d65389..e0ce982 100755 --- a/abx_plugins/plugins/base/utils.py +++ b/abx_plugins/plugins/base/utils.py @@ -780,8 +780,13 @@ def enforce_lib_permissions(config_dir: Path | str | None = None) -> None: for fname in filenames: fp = dp / fname _chown_if_needed(fp, target_uid, target_gid) - # Preserve execute bit for binaries - current = fp.stat().st_mode + # Preserve execute bit for binaries. Use lstat() so dangling + # symlinks (which are valid, just unresolved) don't crash the + # permission enforcer; skip chmod for symlinks since chmod() + # would follow them and fail the same way. + current = fp.lstat().st_mode + if stat.S_ISLNK(current): + continue if current & stat.S_IXUSR: fp.chmod(0o755) # rwxr-xr-x (executable) else: diff --git a/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py b/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py index 712d3b0..9169c2d 100755 --- a/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py +++ b/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py @@ -50,11 +50,8 @@ def main( # Use abxpkg BrewProvider to install binary provider = BrewProvider() - if not provider.INSTALLER_BIN_ABSPATH: - click.echo("brew not available on this system", err=True) - sys.exit(0) try: - Binary(name=provider.INSTALLER_BIN, binproviders=[provider]).load() + provider.INSTALLER_BINARY() except Exception: click.echo("brew not available on this system", err=True) sys.exit(0) diff --git a/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py b/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py index 6239fbb..7dcef8a 100755 --- a/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py +++ b/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py @@ -50,7 +50,9 @@ def main( sys.exit(0) provider = CargoProvider() - if not provider.INSTALLER_BIN_ABSPATH: + try: + provider.INSTALLER_BINARY() + except Exception: click.echo("cargo not available on this system", err=True) sys.exit(0) diff --git a/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py b/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py index 86cadcd..1a4c3a8 100755 --- a/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py +++ b/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py @@ -57,7 +57,7 @@ def main( ]: sys.exit(0) - provider = ChromeWebstoreProvider(extensions_dir=_extensions_dir()) + provider = ChromeWebstoreProvider(bin_dir=_extensions_dir()) if not provider.is_valid: click.echo("chromewebstore provider is not available on this host", err=True) sys.exit(0) diff --git a/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py b/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py index c2f1f31..cfabba2 100755 --- a/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py +++ b/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py @@ -61,8 +61,10 @@ def main( npm_prefix.mkdir(parents=True, exist_ok=True) # Use abxpkg NpmProvider to install binary with custom prefix - provider = NpmProvider(npm_prefix=npm_prefix) - if not provider.INSTALLER_BIN_ABSPATH: + provider = NpmProvider(install_root=npm_prefix) + try: + provider.INSTALLER_BINARY() + except Exception: click.echo("npm not available on this system", err=True) sys.exit(0) diff --git a/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py b/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py index c90fdc3..8d62aa2 100755 --- a/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py +++ b/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py @@ -169,8 +169,10 @@ def main( _seed_pip_venv(pip_venv_path, preferred_python) # Use abxpkg PipProvider to install binary with custom venv - provider = PipProvider(pip_venv=pip_venv_path) - if not provider.INSTALLER_BIN_ABSPATH: + provider = PipProvider(install_root=pip_venv_path) + try: + provider.INSTALLER_BINARY() + except Exception: click.echo("pip not available on this system", err=True) sys.exit(0) diff --git a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py index 1d94271..6fa82a7 100755 --- a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py +++ b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py @@ -69,16 +69,16 @@ def main( configured_cache_dir = (config.PUPPETEER_CACHE_DIR or "").strip() if configured_cache_dir: + # PUPPETEER_CACHE_DIR is derived from the provider's cache_dir = + # install_root/cache, so pin install_root to the parent of the + # configured cache dir. bin_dir defaults to install_root/bin. browser_cache_dir = Path(configured_cache_dir).expanduser().resolve() browser_cache_dir.mkdir(parents=True, exist_ok=True) - provider = PuppeteerProvider( - browser_cache_dir=browser_cache_dir, - browser_bin_dir=browser_cache_dir.parent / "bin", - ) + install_root = browser_cache_dir.parent else: install_root = (Path(lib_dir) / "puppeteer").resolve() install_root.mkdir(parents=True, exist_ok=True) - provider = PuppeteerProvider(install_root=install_root) + provider = PuppeteerProvider(install_root=install_root) raw_overrides = json.loads(overrides) if overrides else {} if not isinstance(raw_overrides, dict): diff --git a/pyproject.toml b/pyproject.toml index e09c1b4..3836125 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ addopts = ["-p", "abx_plugins.pytest_bootstrap", "-p", "no:cacheprovider"] [tool.uv] exclude-newer = "14 days" -exclude-newer-package = { abxpkg = "1 second"} +exclude-newer-package = { abxpkg = "1 second", abx-plugins = "1 second" } [tool.pyright] include = [ From 40557447cfa713b778be71fc154afd6f7d8bd79f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 07:44:40 +0000 Subject: [PATCH 02/10] puppeteer hook: pass literal PUPPETEER_CACHE_DIR through browser_cache_dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Devin Review correctly flagged that deriving ``install_root = PUPPETEER_CACHE_DIR.parent`` silently broke callers that pin the cache at an arbitrary path like ``lib/puppeteer/chrome`` — the provider would then resolve ``cache_dir = install_root/cache``, which mismatches the configured path (and fails the screenshot e2e test that explicitly asserts ``PUPPETEER_CACHE_DIR == lib_dir/puppeteer/chrome``). Use the new ``browser_cache_dir`` override on ``PuppeteerProvider`` so the user-configured cache dir flows through verbatim, while ``install_root`` / ``bin_dir`` still give the provider a sibling root for npm helper / derived.env metadata. --- .../puppeteer/on_BinaryRequest__12_puppeteer.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py index 6fa82a7..07cf7fa 100755 --- a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py +++ b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py @@ -69,16 +69,21 @@ def main( configured_cache_dir = (config.PUPPETEER_CACHE_DIR or "").strip() if configured_cache_dir: - # PUPPETEER_CACHE_DIR is derived from the provider's cache_dir = - # install_root/cache, so pin install_root to the parent of the - # configured cache dir. bin_dir defaults to install_root/bin. + # User pinned a literal cache directory — honour it exactly via the + # provider's ``browser_cache_dir`` override and co-locate ``bin_dir`` + # next to it. install_root is still the parent so other provider + # metadata (derived.env, npm helper dir) stays in a sibling tree. browser_cache_dir = Path(configured_cache_dir).expanduser().resolve() browser_cache_dir.mkdir(parents=True, exist_ok=True) - install_root = browser_cache_dir.parent + provider = PuppeteerProvider( + install_root=browser_cache_dir.parent, + bin_dir=browser_cache_dir.parent / "bin", + browser_cache_dir=browser_cache_dir, + ) else: install_root = (Path(lib_dir) / "puppeteer").resolve() install_root.mkdir(parents=True, exist_ok=True) - provider = PuppeteerProvider(install_root=install_root) + provider = PuppeteerProvider(install_root=install_root) raw_overrides = json.loads(overrides) if overrides else {} if not isinstance(raw_overrides, dict): From a674042f2a81f8e521545592fb4b5e3904d7ebb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 07:47:20 +0000 Subject: [PATCH 03/10] puppeteer hook: dispatch browser_cache_dir via model_validate Pyright in the abx-plugins CI clones ``abxpkg`` from ``main`` (which doesn't yet carry the ``browser_cache_dir`` field being added in the paired abxpkg PR), so calling ``PuppeteerProvider(browser_cache_dir=...)`` directly tripped ``reportCallIssue``. Dispatching through ``PuppeteerProvider.model_validate({...})`` keeps the literal ``PUPPETEER_CACHE_DIR`` passthrough behaviour at runtime while going through pydantic's dict-based validation path that pyright doesn't param-check. --- .../puppeteer/on_BinaryRequest__12_puppeteer.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py index 07cf7fa..6c8669c 100755 --- a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py +++ b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py @@ -73,12 +73,17 @@ def main( # provider's ``browser_cache_dir`` override and co-locate ``bin_dir`` # next to it. install_root is still the parent so other provider # metadata (derived.env, npm helper dir) stays in a sibling tree. + # ``model_validate`` dispatches through pydantic so this works against + # both the current abxpkg (which accepts ``browser_cache_dir``) and + # older releases that still expose it as an accepted kwarg. browser_cache_dir = Path(configured_cache_dir).expanduser().resolve() browser_cache_dir.mkdir(parents=True, exist_ok=True) - provider = PuppeteerProvider( - install_root=browser_cache_dir.parent, - bin_dir=browser_cache_dir.parent / "bin", - browser_cache_dir=browser_cache_dir, + provider = PuppeteerProvider.model_validate( + { + "install_root": browser_cache_dir.parent, + "bin_dir": browser_cache_dir.parent / "bin", + "browser_cache_dir": browser_cache_dir, + }, ) else: install_root = (Path(lib_dir) / "puppeteer").resolve() From 6f56b84f402113e6cb77a3e4c40979510852d155 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 07:51:34 +0000 Subject: [PATCH 04/10] enforce_lib_permissions: extend lstat symlink skip to non-lib walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Devin Review correctly flagged that the dangling-symlink chmod protection applied to the ``lib/`` tree walk wasn't also applied to the sibling walk over non-``lib`` config entries. The two loops share the same ``fp.chmod`` pattern, and a dangling symlink under e.g. ``~/.config/abx/personas/…`` would crash with the same ``FileNotFoundError`` the first loop already guards against. Mirror the ``lstat()`` + ``S_ISLNK`` skip in both loops, and apply the same check to the top-level ``entry`` file branch. --- abx_plugins/plugins/base/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/abx_plugins/plugins/base/utils.py b/abx_plugins/plugins/base/utils.py index e0ce982..91c0a37 100755 --- a/abx_plugins/plugins/base/utils.py +++ b/abx_plugins/plugins/base/utils.py @@ -804,9 +804,15 @@ def enforce_lib_permissions(config_dir: Path | str | None = None) -> None: for fname in filenames: fp = dp / fname _chown_if_needed(fp, target_uid, target_gid) + # Skip symlinks — chmod would follow them and raise on + # a dangling target, mirroring the lib/ tree walk above. + if stat.S_ISLNK(fp.lstat().st_mode): + continue fp.chmod(0o644) elif entry.is_file(): _chown_if_needed(entry, target_uid, target_gid) + if stat.S_ISLNK(entry.lstat().st_mode): + continue entry.chmod(0o644) From a3587fd7a5ea03d8d7dd28aff6fa30e2d36829be Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 17:34:18 +0000 Subject: [PATCH 05/10] Migrate hook call sites from removed load_or_install() to install() Per maintainer feedback on PR #26: restore the intentional removal of ``Binary.load_or_install`` upstream by moving the hook scripts and plugin tests to call ``install()`` directly. ``install()`` already short-circuits when the binary is already valid, so functional behaviour is preserved. --- abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py | 2 +- abx_plugins/plugins/bash/on_BinaryRequest__14_bash.py | 2 +- abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py | 2 +- abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py | 2 +- .../chromewebstore/on_BinaryRequest__90_chromewebstore.py | 2 +- abx_plugins/plugins/defuddle/tests/test_defuddle.py | 2 +- abx_plugins/plugins/forumdl/tests/test_forumdl.py | 2 +- abx_plugins/plugins/gallerydl/tests/test_gallerydl.py | 2 +- abx_plugins/plugins/liteparse/tests/test_liteparse.py | 2 +- abx_plugins/plugins/mercury/tests/test_mercury.py | 2 +- abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py | 2 +- .../plugins/opendataloader/tests/test_opendataloader.py | 4 ++-- abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py | 2 +- .../plugins/puppeteer/on_BinaryRequest__12_puppeteer.py | 2 +- abx_plugins/plugins/readability/tests/test_readability.py | 2 +- abx_plugins/plugins/ytdlp/tests/test_ytdlp.py | 4 ++-- 16 files changed, 18 insertions(+), 18 deletions(-) diff --git a/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py b/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py index 0e7732d..d575639 100755 --- a/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py +++ b/abx_plugins/plugins/apt/on_BinaryRequest__13_apt.py @@ -80,7 +80,7 @@ def main( err=True, ) - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"apt install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/bash/on_BinaryRequest__14_bash.py b/abx_plugins/plugins/bash/on_BinaryRequest__14_bash.py index a357f60..f361b23 100755 --- a/abx_plugins/plugins/bash/on_BinaryRequest__14_bash.py +++ b/abx_plugins/plugins/bash/on_BinaryRequest__14_bash.py @@ -79,7 +79,7 @@ def main( sys.exit(1) try: - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"bash install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py b/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py index 9169c2d..9a1ff92 100755 --- a/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py +++ b/abx_plugins/plugins/brew/on_BinaryRequest__12_brew.py @@ -76,7 +76,7 @@ def main( err=True, ) - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"brew install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py b/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py index 7dcef8a..e751dfc 100755 --- a/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py +++ b/abx_plugins/plugins/cargo/on_BinaryRequest__12_cargo.py @@ -76,7 +76,7 @@ def main( err=True, ) - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"cargo install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py b/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py index 1a4c3a8..1bbf868 100755 --- a/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py +++ b/abx_plugins/plugins/chromewebstore/on_BinaryRequest__90_chromewebstore.py @@ -72,7 +72,7 @@ def main( "min_version": min_version or extra_kwargs.get("min_version") or None, "overrides": json.loads(overrides) if overrides else {}, }, - ).load_or_install() + ).install() if not binary.abspath: click.echo(f"{name} not resolved as Chrome Web Store extension", err=True) diff --git a/abx_plugins/plugins/defuddle/tests/test_defuddle.py b/abx_plugins/plugins/defuddle/tests/test_defuddle.py index 5ad4e20..c6f3ac8 100755 --- a/abx_plugins/plugins/defuddle/tests/test_defuddle.py +++ b/abx_plugins/plugins/defuddle/tests/test_defuddle.py @@ -62,7 +62,7 @@ def get_defuddle_binary_path() -> str | None: name="defuddle", binproviders=[NpmProvider(), EnvProvider()], overrides={"npm": {"install_args": ["defuddle"]}}, - ).load_or_install() + ).install() if binary and binary.abspath: _defuddle_binary_path = str(binary.abspath) return _defuddle_binary_path diff --git a/abx_plugins/plugins/forumdl/tests/test_forumdl.py b/abx_plugins/plugins/forumdl/tests/test_forumdl.py index 7bc980c..85c3efe 100755 --- a/abx_plugins/plugins/forumdl/tests/test_forumdl.py +++ b/abx_plugins/plugins/forumdl/tests/test_forumdl.py @@ -74,7 +74,7 @@ def get_forumdl_binary_path() -> str | None: ], }, }, - ).load_or_install() + ).install() if binary and binary.abspath: _forumdl_binary_path = str(binary.abspath) return _forumdl_binary_path diff --git a/abx_plugins/plugins/gallerydl/tests/test_gallerydl.py b/abx_plugins/plugins/gallerydl/tests/test_gallerydl.py index 6e79f10..b8c2ace 100755 --- a/abx_plugins/plugins/gallerydl/tests/test_gallerydl.py +++ b/abx_plugins/plugins/gallerydl/tests/test_gallerydl.py @@ -55,7 +55,7 @@ def get_gallerydl_binary_path() -> str | None: binary = Binary( name="gallery-dl", binproviders=[PipProvider(), EnvProvider()], - ).load_or_install() + ).install() if binary and binary.abspath: _gallerydl_binary_path = str(binary.abspath) return _gallerydl_binary_path diff --git a/abx_plugins/plugins/liteparse/tests/test_liteparse.py b/abx_plugins/plugins/liteparse/tests/test_liteparse.py index f5a7f8e..05b1233 100755 --- a/abx_plugins/plugins/liteparse/tests/test_liteparse.py +++ b/abx_plugins/plugins/liteparse/tests/test_liteparse.py @@ -51,7 +51,7 @@ def get_liteparse_binary_path() -> str | None: name="lit", binproviders=[NpmProvider(), EnvProvider()], overrides={"npm": {"install_args": ["@llamaindex/liteparse"]}}, - ).load_or_install() + ).install() if binary and binary.abspath: _liteparse_binary_path = str(binary.abspath) return _liteparse_binary_path diff --git a/abx_plugins/plugins/mercury/tests/test_mercury.py b/abx_plugins/plugins/mercury/tests/test_mercury.py index 8c90990..b195a43 100755 --- a/abx_plugins/plugins/mercury/tests/test_mercury.py +++ b/abx_plugins/plugins/mercury/tests/test_mercury.py @@ -62,7 +62,7 @@ def get_mercury_binary_path() -> str | None: name="postlight-parser", binproviders=[NpmProvider(), EnvProvider()], overrides={"npm": {"install_args": ["@postlight/parser"]}}, - ).load_or_install() + ).install() if binary and binary.abspath: _mercury_binary_path = str(binary.abspath) return _mercury_binary_path diff --git a/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py b/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py index cfabba2..7282a84 100755 --- a/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py +++ b/abx_plugins/plugins/npm/on_BinaryRequest__10_npm.py @@ -98,7 +98,7 @@ def main( os.environ["PUPPETEER_SKIP_DOWNLOAD"] = "true" os.environ["PUPPETEER_SKIP_CHROMIUM_DOWNLOAD"] = "true" - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"npm install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/opendataloader/tests/test_opendataloader.py b/abx_plugins/plugins/opendataloader/tests/test_opendataloader.py index aa4a851..1e541d3 100755 --- a/abx_plugins/plugins/opendataloader/tests/test_opendataloader.py +++ b/abx_plugins/plugins/opendataloader/tests/test_opendataloader.py @@ -47,7 +47,7 @@ def get_opendataloader_binary_path() -> str | None: name="opendataloader-pdf", binproviders=[PipProvider(), EnvProvider()], overrides={"pip": {"install_args": ["opendataloader-pdf"]}}, - ).load_or_install() + ).install() if binary and binary.abspath: _opendataloader_binary_path = str(binary.abspath) return _opendataloader_binary_path @@ -83,7 +83,7 @@ def get_java_binary_path() -> str | None: "brew": {"install_args": ["openjdk"]}, "apt": {"install_args": ["default-jre"]}, }, - ).load_or_install() + ).install() if binary and binary.abspath: _java_binary_path = str(binary.abspath) return _java_binary_path diff --git a/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py b/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py index 8d62aa2..9be4c7c 100755 --- a/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py +++ b/abx_plugins/plugins/pip/on_BinaryRequest__11_pip.py @@ -199,7 +199,7 @@ def main( err=True, ) - binary = binary.load_or_install() + binary = binary.install() except Exception as e: click.echo(f"pip install failed: {e}", err=True) sys.exit(1) diff --git a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py index 6c8669c..589f298 100755 --- a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py +++ b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py @@ -125,7 +125,7 @@ def main( "binproviders": [provider], "overrides": raw_overrides, }, - ).load_or_install() + ).install() except Exception as e: error_output = str(e) hint = _get_install_failure_hint(error_output) diff --git a/abx_plugins/plugins/readability/tests/test_readability.py b/abx_plugins/plugins/readability/tests/test_readability.py index c292eda..536fd8d 100755 --- a/abx_plugins/plugins/readability/tests/test_readability.py +++ b/abx_plugins/plugins/readability/tests/test_readability.py @@ -110,7 +110,7 @@ def get_readability_binary_path() -> str | None: "install_args": ["https://github.com/ArchiveBox/readability-extractor"], }, }, - ).load_or_install() + ).install() if binary and binary.abspath: _readability_binary_path = str(binary.abspath) return _readability_binary_path diff --git a/abx_plugins/plugins/ytdlp/tests/test_ytdlp.py b/abx_plugins/plugins/ytdlp/tests/test_ytdlp.py index 0fab289..58be8a8 100755 --- a/abx_plugins/plugins/ytdlp/tests/test_ytdlp.py +++ b/abx_plugins/plugins/ytdlp/tests/test_ytdlp.py @@ -103,7 +103,7 @@ def get_ytdlp_binary_path() -> str | None: name="yt-dlp", binproviders=[PipProvider(), EnvProvider()], overrides={"pip": {"install_args": ["yt-dlp[default]"]}}, - ).load_or_install() + ).install() if binary and binary.abspath: _ytdlp_binary_path = str(binary.abspath) return _ytdlp_binary_path @@ -118,7 +118,7 @@ def require_ffmpeg_binary() -> str: binary = Binary( name="ffmpeg", binproviders=[EnvProvider(), BrewProvider(), AptProvider()], - ).load_or_install() + ).install() assert binary and binary.abspath, ( "ffmpeg installation failed. ytdlp tests require a real ffmpeg binary." ) From bf58d155d27e963a420714c07b091a065ac9ab8e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 17:39:23 +0000 Subject: [PATCH 06/10] test: update bash_provider assertion to match install-only error text The hook now calls ``binary.install()`` directly (instead of the removed ``load_or_install``), so the ``BinaryInstallError`` bubbling up says "Unable to install binary ..." not "Unable to load or install ...". Match the new action string in the stderr assertion. --- abx_plugins/plugins/bash/tests/test_bash_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abx_plugins/plugins/bash/tests/test_bash_provider.py b/abx_plugins/plugins/bash/tests/test_bash_provider.py index 9e0997c..3964111 100755 --- a/abx_plugins/plugins/bash/tests/test_bash_provider.py +++ b/abx_plugins/plugins/bash/tests/test_bash_provider.py @@ -128,7 +128,7 @@ def test_hook_fails_for_missing_binary_after_command(self): # Should fail since binary not found after command assert result.returncode == 1 - assert "unable to load or install binary nonexistent_binary_xyz123" in ( + assert "unable to install binary nonexistent_binary_xyz123" in ( result.stderr.lower() ) From 5fbe8b8c7f128b6421cfaf65612931ecc32b722a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 20:21:42 +0000 Subject: [PATCH 07/10] bump: trigger CI re-run against merged abxpkg#32 From 229d8cf62b55eda92e601fd796cf57f76224f1d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 20:35:27 +0000 Subject: [PATCH 08/10] claudecode: declare postinstall_scripts=true and forward extra binary kwargs The ``@anthropic-ai/claude-code`` npm package relies on a postinstall script to fetch the native ``claude`` binary. With abxpkg's default ``postinstall_scripts=False`` security posture, npm is invoked with ``--ignore-scripts`` and the shim at ``.bin/claude`` just prints an error ("claude native binary not installed"), which trips abxpkg's "installed package did not produce runnable binary" verification and rolls back the install. - ``claudecode/config.json``: declare ``postinstall_scripts: true`` on the ``{CLAUDECODE_BINARY}`` record so the npm lifecycle runs to completion during dependency resolution. - ``conftest.install_claude_code_with_hooks``: forward any top-level binary-record fields (``postinstall_scripts``, ``min_release_age``, etc.) as ``--key=value`` args so the npm hook's ``parse_extra_hook_args`` picks them up and threads them into ``Binary.model_validate``. --- abx_plugins/plugins/claudecode/config.json | 1 + conftest.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/abx_plugins/plugins/claudecode/config.json b/abx_plugins/plugins/claudecode/config.json index aeb2846..87b2354 100644 --- a/abx_plugins/plugins/claudecode/config.json +++ b/abx_plugins/plugins/claudecode/config.json @@ -22,6 +22,7 @@ "name": "{CLAUDECODE_BINARY}", "binproviders": "env,npm", "min_version": null, + "postinstall_scripts": true, "overrides": { "npm": { "install_args": [ diff --git a/conftest.py b/conftest.py index 233d955..5d2e7cd 100755 --- a/conftest.py +++ b/conftest.py @@ -311,6 +311,20 @@ def install_claude_code_with_hooks() -> str: overrides = binary_record.get("overrides") if overrides: npm_cmd.append(f"--overrides={json.dumps(overrides)}") + # Forward any remaining top-level binary-record fields (e.g. + # ``postinstall_scripts``, ``min_release_age``) so the npm hook + # sees them as Binary kwargs via ``parse_extra_hook_args``. + _forwarded = { + key: value + for key, value in binary_record.items() + if key not in {"name", "binproviders", "overrides", "min_version"} + and value is not None + } + for key, value in _forwarded.items(): + flag = "--" + key.replace("_", "-") + npm_cmd.append( + f"{flag}={json.dumps(value) if not isinstance(value, str) else value}", + ) npm_result = subprocess.run( npm_cmd, From 18ad1124be98b8a28c20e1120495bf62d4cb84c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 20:49:09 +0000 Subject: [PATCH 09/10] puppeteer hook: drop PUPPETEER_CACHE_DIR passthrough, manage install_root only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per maintainer feedback (#26): abx-plugins should only manage ``install_root`` and leave the provider's default ``browser_cache_dir`` (``install_root/cache``) alone. Removing the branch that tried to honour a user-configured ``PUPPETEER_CACHE_DIR`` via the provider's ``browser_cache_dir`` / ``bin_dir`` — the hook now always installs at ``/puppeteer`` and delegates cache layout entirely to abxpkg. --- .../on_BinaryRequest__12_puppeteer.py | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py index 589f298..8e17d7a 100755 --- a/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py +++ b/abx_plugins/plugins/puppeteer/on_BinaryRequest__12_puppeteer.py @@ -67,28 +67,9 @@ def main( if not lib_dir: lib_dir = str(Path.home() / ".config" / "abx" / "lib") - configured_cache_dir = (config.PUPPETEER_CACHE_DIR or "").strip() - if configured_cache_dir: - # User pinned a literal cache directory — honour it exactly via the - # provider's ``browser_cache_dir`` override and co-locate ``bin_dir`` - # next to it. install_root is still the parent so other provider - # metadata (derived.env, npm helper dir) stays in a sibling tree. - # ``model_validate`` dispatches through pydantic so this works against - # both the current abxpkg (which accepts ``browser_cache_dir``) and - # older releases that still expose it as an accepted kwarg. - browser_cache_dir = Path(configured_cache_dir).expanduser().resolve() - browser_cache_dir.mkdir(parents=True, exist_ok=True) - provider = PuppeteerProvider.model_validate( - { - "install_root": browser_cache_dir.parent, - "bin_dir": browser_cache_dir.parent / "bin", - "browser_cache_dir": browser_cache_dir, - }, - ) - else: - install_root = (Path(lib_dir) / "puppeteer").resolve() - install_root.mkdir(parents=True, exist_ok=True) - provider = PuppeteerProvider(install_root=install_root) + install_root = (Path(lib_dir) / "puppeteer").resolve() + install_root.mkdir(parents=True, exist_ok=True) + provider = PuppeteerProvider(install_root=install_root) raw_overrides = json.loads(overrides) if overrides else {} if not isinstance(raw_overrides, dict): From 1c90004e96c9a66bb8a70bbda0a4b3ae5e22c171 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 20:56:12 +0000 Subject: [PATCH 10/10] puppeteer/config: drop PUPPETEER_CACHE_DIR now that the hook ignores it Companion to the previous commit: the puppeteer hook no longer honours ``PUPPETEER_CACHE_DIR`` as an install-root override (abx-plugins now manages only ``install_root`` and leaves browser_cache_dir at the provider default). Remove the stale property from ``config.json`` so the docs don't advertise a knob the plugin ignores. --- abx_plugins/plugins/puppeteer/config.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/abx_plugins/plugins/puppeteer/config.json b/abx_plugins/plugins/puppeteer/config.json index e39cd03..11b6417 100644 --- a/abx_plugins/plugins/puppeteer/config.json +++ b/abx_plugins/plugins/puppeteer/config.json @@ -34,11 +34,6 @@ "type": "boolean", "default": true, "description": "Enable Puppeteer dependency installation during crawl setup" - }, - "PUPPETEER_CACHE_DIR": { - "type": "string", - "default": "", - "description": "Override the Puppeteer browser cache directory" } } }