From 28edf7c75a086ff25292bec77a4130dd1016b449 Mon Sep 17 00:00:00 2001 From: Ben Barclay Date: Fri, 25 Sep 2026 11:19:18 +1000 Subject: [PATCH] fix(pm): make the staged agent-browser binary executable The npm tarball ships every bin/agent-browser-* as 0644; agent-browser's own postinstall sets the exec bit, and pm runs no postinstall. The first browser_navigate auto-installs agent-browser and then fails with PermissionError. --- pm/packages.py | 4 +++ tests/pm/test_agent_browser_package.py | 48 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/pm/test_agent_browser_package.py diff --git a/pm/packages.py b/pm/packages.py index 93d0f56a00..14f6b6afcb 100644 --- a/pm/packages.py +++ b/pm/packages.py @@ -872,6 +872,10 @@ class AgentBrowser(BinaryPackage): for item in bin_dir.iterdir(): if item.is_file() and item.name.startswith("agent-browser-") and item.name != keep: item.unlink() + # The npm tarball ships every native binary as 0644; agent-browser's + # own postinstall sets the exec bit, and pm runs no postinstall. + kept = bin_dir / keep + kept.chmod(kept.stat().st_mode | 0o111) @register diff --git a/tests/pm/test_agent_browser_package.py b/tests/pm/test_agent_browser_package.py new file mode 100644 index 0000000000..45d2a0a167 --- /dev/null +++ b/tests/pm/test_agent_browser_package.py @@ -0,0 +1,48 @@ +"""agent-browser staging leaves the kept native binary executable. + +The npm tarball stores every ``bin/agent-browser-*`` as 0644; agent-browser's own +postinstall sets the exec bit, and pm runs no postinstall. +""" + +import io +import os +import stat +import tarfile + +import pytest + +from pm import Store, get_package + + +def _npm_tarball(path, names): + with tarfile.open(path, "w:gz") as tar: + for name in names: + data = f"{name} fixture".encode() + info = tarfile.TarInfo(f"package/{name}") + info.size = len(data) + info.mode = 0o755 if name.endswith(".js") else 0o644 + tar.addfile(info, io.BytesIO(data)) + + +@pytest.mark.skipif(os.name == "nt", reason="POSIX exec bit") +@pytest.mark.parametrize("target", ["linux-arm64", "linux-x64", "darwin-arm64"]) +def test_staged_native_binary_is_executable(tmp_path, target): + package = get_package("agent-browser") + archive = tmp_path / "agent-browser.tgz" + _npm_tarball(archive, [ + "package.json", + "bin/agent-browser.js", + "bin/agent-browser-linux-arm64", + "bin/agent-browser-linux-x64", + "bin/agent-browser-darwin-arm64", + ]) + staged = tmp_path / "staged" + package.unpack(archive, staged, target) + package.stage(Store(tmp_path / "store"), staged, "fixture", target) + + binary = package.binary(staged, target) + assert binary == staged / "bin" / f"agent-browser-{target}" + mode = binary.stat().st_mode + assert mode & stat.S_IXUSR and mode & stat.S_IXGRP and mode & stat.S_IXOTH + assert sorted(p.name for p in (staged / "bin").iterdir()) == sorted( + ["agent-browser.js", f"agent-browser-{target}"])