feat(staged): bundle ACP bridge tools#876
Conversation
Pin the standalone ACP bridge CLIs (claude-agent-acp, codex-acp) from npm in acp-tools.lock.json, per supported target, and ship them as Tauri app resources instead of relying on whatever the user has installed globally. - scripts/update-acp-tools-lock.mjs resolves each package via npm view against the configured registry and fails loudly when a package or one of its per-target native dependencies cannot be resolved, rather than silently pinning a stale version. - scripts/ensure-acp-tools.sh installs the pinned packages into the shared Staged dev cache, validating versions and integrity hashes against the lock. - scripts/prepare-acp-tools-resource.sh stages the vendored package trees under src-tauri/resources/acp/node with executable wrappers in src-tauri/resources/acp/bin (ad-hoc signed on macOS), which tauri.conf.json bundles as resources. The bin dir holds a single target at a time, so staging stays tied to the build target. - just install ensures the pinned tools; just dev stages the resource dir and exports STAGED_ACP_TOOLS_DIR; just build / release-build and the release workflow stage before tauri build; just bump-acp-tools reruns the lock updater. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Resolve the bundled ACP bridge bin dir deterministically at runtime — the STAGED_ACP_TOOLS_DIR dev override first, then the Tauri resource dir for packaged apps — and register it with acp-client at startup so find_command prefers the pinned bundled bridges over user-installed copies everywhere (session spawn, provider discovery). Shape the captured shell-env snapshots handed to agent spawns through apply_bundled_tools_env: prepend the bundled dir to the imported shell PATH so the bundled bridges win while user-installed CLIs (and their auth state) remain discoverable, and pin GOOSE_SEARCH_PATHS after the shell-env import so user shell values cannot override it — serialized as a JSON array to match Goose's config env parsing and scoped to the managed dir plus any explicit pre-existing Goose search dirs, not the whole shell PATH. Doctor checks and fixes resolve from the same PATH shape so they never prompt users to install a bridge Staged already bundles. Covered with unit tests for dir resolution, PATH prepending, and the GOOSE_SEARCH_PATHS construction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Run just bump-acp-tools to re-pin the bundled ACP bridge CLIs in acp-tools.lock.json against the latest npm releases: - claude-agent-acp 0.56.0 -> 0.57.0, pulling in @anthropic-ai/claude-agent-sdk 0.3.202 (Claude Code 2.1.202) and its per-target native packages - codex-acp stays at 1.1.0, which is still the latest release Verified the updated lock installs cleanly via ensure-acp-tools.sh, including integrity hash validation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Both PATH-building sites in the agent spawn env — the bundled ACP dir prepend in acp_tools::prepend_dir_to_path and the well-known tool dirs appended in shell_env::build_extended_path_from_path — joined the final list with join_paths().unwrap_or_default(), so a single dir that join_paths rejects (e.g. an install path containing ':', legal on macOS) emptied the whole spawned-agent PATH. Route both through a shared join_paths_best_effort that retries the join without the un-joinable dirs (logging each dropped entry), so one bad entry degrades to a best-effort PATH instead of erasing every search path. Ports squareup/berd@bb912c96 to Staged's split PATH-shaping layout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The freshness stamp lived in the per-version tool_dir while the staged output $bin_dir/$binary is shared across versions, so a lock bump followed by a revert (or a branch switch) left the old version's stamp self-consistent and skipped staging while the staged wrapper still pointed at the newer version — silently shipping a different version than the lockfile claims. Move the stamp next to the staged output ($bin_dir/$binary.stamp) so it describes exactly the artifact being freshness-checked; any lock change for that binary now forces a re-stage. The per-version tool_dir installs remain as the download cache. Also prune files in $bin_dir that no longer correspond to a locked binary (or its stamp), so tools removed from the lock don't linger on the Goose search path. Verified with a scratch cache: fresh stage, no-op re-run, staging the pre-bump 0.56.0 lock (only claude re-staged), then reverting to the current lock correctly re-stages 0.57.0 — the previously broken case — and the pruning removes stray staged files while keeping locked ones. Ports squareup/berd@1db993fb (npm loop only; Staged's lock has no github-release staging loop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
write_node_wrapper was duplicated (near-)verbatim in ensure-acp-tools.sh and prepare-acp-tools-resource.sh, so a future edit to one copy could make the dev-cache wrapper and the bundled resource wrapper drift. Extract it into scripts/lib/acp-node-wrapper.sh and source it from both scripts with matching shellcheck source= hints. No behavior change; the absolute-vs-relative entrypoint handling stays inside the function (ensure passes the cache-absolute entrypoint, prepare the resource-relative one). Verified by re-running both staging flows: the dev-cache wrapper (absolute entrypoint) and the bundled resource wrapper (relative entrypoint) are generated as before and report bridge versions 0.57.0 (claude) and 1.1.0 (codex). Ports squareup/berd@24d7518b. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Bundled npm ACP bridges are bash shims that exec node, so a machine without a suitable Node runtime read as healthy everywhere and only failed at first session spawn with a bare exit 127 on stderr. prepare-acp-tools-resource.sh now stages a node-runtime.json manifest next to the bundled bin dir with one entry per npm-sourced bridge, each carrying its own required Node major derived from that tool's locked nodeEngine range — bridges may require different Node majors and are checked independently. The major parsing is extracted into the shared acp-node-wrapper.sh lib and reused by the wrapper shims, so the version the shim enforces at spawn time and the version the doctor reports cannot disagree. Locks with no npm tools ship no manifest. A new node-runtime doctor check (Tools section, fix_url pointing at nodejs.org) resolves the manifest as the bundled bin dir's parent, resolves node on the doctor PATH, probes process.versions.node, and compares the detected major against each tool's requirement: pass when all are satisfied, warn naming only the unmet bridges, warn listing all requirements when node is missing or unversionable. A missing manifest emits no check; an unreadable one warns instead of silently hiding the packaging break. The check runs in Staged's doctor command layer alongside the shared doctor crate report, over the same env snapshot. Verified with cargo test (9 new tests incl. an end-to-end manifest -> probe run), clippy, and staging runs showing the manifest written for the current lock and removed for an empty lock. Ports squareup/berd@07087303 to Staged's doctor command layer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The staging codesign filter only matched files named exactly claude or codex, so other Mach-O executables vendored by the npm packages — the codex package ships rg and a bundled zsh, and upstream can add more — were staged unsigned and killed by Gatekeeper when the bundled bridge spawned them. Replace the name filter with a scan of every file in the staged package, signing anything file(1) identifies as Mach-O. The scan is gated on Darwin so Linux staging does not pay the file(1) cost. Verified by re-running prepare-acp-tools-resource.sh: all four Mach-Os in the staged tree (claude SDK binary, codex CLI, and the previously missed rg and zsh) now carry valid ad-hoc signatures. Release signing needs no extra config: staged-release.yml signs via block/apple-codesign-action, which routes through the same internal codesign_helper service as berd's Buildkite plugin — it deep-scans the bundle and Developer-ID-signs every nested Mach-O before notarizing. Ports squareup/berd@737e33a5. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Run just bump-acp-tools to re-pin the bundled ACP bridge CLIs in acp-tools.lock.json against the latest npm releases: - claude-agent-acp 0.57.0 -> 0.58.1, pulling in @anthropic-ai/claude-agent-sdk 0.3.205 (Claude Code 2.1.205) and its per-target native packages - codex-acp 1.1.0 -> 1.1.2, pulling in @openai/codex 0.144.1 The bump surfaced a gap in update-acp-tools-lock.mjs: codex-acp 1.1.2 declares its @openai/codex dependency as a range (^0.144.0) rather than an exact pin, and npm view returns an array of per-version objects when a range matches multiple published versions, so the updater failed with "Missing @openai/codex@^0.144.0 version". Teach it to pick the highest matching version from multi-match results so ranged dependencies still pin the newest release (0.144.1 over 0.144.0 here). Verified the updated lock installs cleanly via ensure-acp-tools.sh (integrity hashes validated) and stages via prepare-acp-tools-resource.sh, with both staged bridges reporting the new versions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5124302ac6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| let mut command = tokio::process::Command::new(node_path); | ||
| command | ||
| .args(["-p", "process.versions.node"]) | ||
| .stdin(Stdio::null()) | ||
| .stdout(Stdio::piped()) | ||
| .stderr(Stdio::piped()); |
There was a problem hiding this comment.
Probe Node with the captured doctor environment
When node resolves to an asdf/mise-style shim or any shell-managed launcher that depends on variables from the captured login environment, this probe runs with Staged's inherited process environment instead of the env_vars snapshot used to resolve the path and later spawn the bundled wrapper. In that setup the doctor can report an unknown/missing Node runtime even though the ACP wrapper would succeed under the captured session environment; pass the same env snapshot to this command before probing the version.
Useful? React with 👍 / 👎.
Buzz desktop spawned the claude-agent-acp and codex-acp bridges from whatever the user had installed globally — an unpinned `npm install -g` surface with no integrity checking, which produced stale-bridge drift (the deprecated @zed-industries/codex-acp 0.16.x gate) and missing-tool failures. Bundle both bridges as app resources instead, pinned per target: - desktop/acp-tools.lock.json pins @agentclientprotocol/claude-agent-acp 0.58.1 and @agentclientprotocol/codex-acp 1.1.2 (npm `latest` at time of commit) for the four supported targets, with integrity hashes and Block Artifactory tarballs for the package, its claude-agent-sdk / @openai/codex dependency, and the per-target native package. - desktop/scripts/update-acp-tools-lock.mjs regenerates the lock from the registry's `latest` dist-tags, failing loudly on any unresolvable package — never silently pinning an older version. Ranged dependencies (codex-acp's ^0.144.0) resolve to the highest matching version when `npm view` returns an array. - desktop/scripts/ensure-acp-tools.sh installs the locked tools into a shared dev cache (~/Library/Caches/buzz-dev/acp-tools), validates versions + integrity against the lock, and stamps staged binaries next to the shared bin dir so any lock change — including a revert — forces a re-stage; binaries no longer in the lock are pruned. - desktop/scripts/prepare-acp-tools-resource.sh stages the vendored npm trees + node wrapper shims into desktop/src-tauri/resources/acp, writes the node-runtime.json manifest for the app's Node.js doctor check, and ad-hoc signs every nested Mach-O (file(1) scan — the codex package vendors rg, zsh, and codex-code-mode-host beyond the main CLIs) so Gatekeeper doesn't kill them in local builds. - desktop/scripts/lib/acp-node-wrapper.sh is the single wrapper-shim generator shared by both scripts, so the dev-cache and bundled wrappers (and the Node major they enforce) cannot drift. - Wiring: `just dev` / `just staging` stage the resources and export BUZZ_ACP_TOOLS_DIR; `just desktop-release-build` stages per target before `tauri build`; `just bump-acp-tools` reruns the lock updater; tauri.conf.json bundles resources/acp; staged artifacts are gitignored with a .gitkeep placeholder. Runtime resolution of the staged dir follows in the next commit. The sprout-releases internal pipeline will need the same staging step before its `tauri build`; that lives in a separate repo. Ports the build-time half of the Staged implementation in block/builderbot#876 (branch commits 16b115a7 bundle feature, f5c6bba9 re-stage after lock revert, 288fa7eb shared node wrapper lib, adea4017 node-runtime manifest, 5124302a sign all nested Mach-Os, de6a9782 ranged-dependency updater fix), itself ported from squareup/berd f07df1d2 + 1db993fb + 24d7518b + 07087303 + 737e33a5. Verified: update-acp-tools-lock.mjs regenerated the lock against the Block registry (byte-identical pins to the donor lock; both packages confirmed at npm `latest`); fresh stage installs both tools and the staged wrappers report 0.58.1 / 1.1.2; no-op re-run performs zero npm installs; a stamp/lock mismatch forces a re-stage and stray binaries are pruned; all five staged Mach-Os pass `codesign --verify`; cargo check on desktop/src-tauri passes with the new resources entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Buzz desktop spawned the claude-agent-acp and codex-acp bridges from whatever the user had installed globally — an unpinned `npm install -g` surface with no integrity checking, which produced stale-bridge drift (the deprecated @zed-industries/codex-acp 0.16.x gate) and missing-tool failures. Bundle both bridges as app resources instead, pinned per target: - desktop/acp-tools.lock.json pins @agentclientprotocol/claude-agent-acp 0.58.1 and @agentclientprotocol/codex-acp 1.1.2 (npm `latest` at time of commit) for the four supported targets, with integrity hashes and Block Artifactory tarballs for the package, its claude-agent-sdk / @openai/codex dependency, and the per-target native package. - desktop/scripts/update-acp-tools-lock.mjs regenerates the lock from the registry's `latest` dist-tags, failing loudly on any unresolvable package — never silently pinning an older version. Ranged dependencies (codex-acp's ^0.144.0) resolve to the highest matching version when `npm view` returns an array. - desktop/scripts/ensure-acp-tools.sh installs the locked tools into a shared dev cache (~/Library/Caches/buzz-dev/acp-tools), validates versions + integrity against the lock, and stamps staged binaries next to the shared bin dir so any lock change — including a revert — forces a re-stage; binaries no longer in the lock are pruned. - desktop/scripts/prepare-acp-tools-resource.sh stages the vendored npm trees + node wrapper shims into desktop/src-tauri/resources/acp, writes the node-runtime.json manifest for the app's Node.js doctor check, and ad-hoc signs every nested Mach-O (file(1) scan — the codex package vendors rg, zsh, and codex-code-mode-host beyond the main CLIs) so Gatekeeper doesn't kill them in local builds. - desktop/scripts/lib/acp-node-wrapper.sh is the single wrapper-shim generator shared by both scripts, so the dev-cache and bundled wrappers (and the Node major they enforce) cannot drift. - Wiring: `just dev` / `just staging` stage the resources and export BUZZ_ACP_TOOLS_DIR; `just desktop-release-build` stages per target before `tauri build`; `just bump-acp-tools` reruns the lock updater; tauri.conf.json bundles resources/acp; staged artifacts are gitignored with a .gitkeep placeholder. Runtime resolution of the staged dir follows in the next commit. The sprout-releases internal pipeline will need the same staging step before its `tauri build`; that lives in a separate repo. Ports the build-time half of the Staged implementation in block/builderbot#876 (branch commits 16b115a7 bundle feature, f5c6bba9 re-stage after lock revert, 288fa7eb shared node wrapper lib, adea4017 node-runtime manifest, 5124302a sign all nested Mach-Os, de6a9782 ranged-dependency updater fix), itself ported from squareup/berd f07df1d2 + 1db993fb + 24d7518b + 07087303 + 737e33a5. Verified: update-acp-tools-lock.mjs regenerated the lock against the Block registry (byte-identical pins to the donor lock; both packages confirmed at npm `latest`); fresh stage installs both tools and the staged wrappers report 0.58.1 / 1.1.2; no-op re-run performs zero npm installs; a stamp/lock mismatch forces a re-stage and stray binaries are pruned; all five staged Mach-Os pass `codesign --verify`; cargo check on desktop/src-tauri passes with the new resources entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Summary
Bundles the standalone ACP bridge CLIs (
claude-agent-acp,codex-acp) with the Staged app instead of relying on whatever the user has installed globally, and wires them through runtime resolution, doctor, and release packaging.acp-tools.lock.jsonpins each bridge per supported target (currently claude-agent-acp 0.57.0, codex-acp 1.1.0).update-acp-tools-lock.mjsre-pins against npm,ensure-acp-tools.shinstalls into the shared dev cache with integrity validation, andprepare-acp-tools-resource.shstages the vendored trees as Tauri resources with node wrapper shims.just install/dev/buildand the release workflow run the staging steps.STAGED_ACP_TOOLS_DIRdev override or the Tauri resource dir) is registered withacp-clientand prepended to the spawned-agent PATH, so the pinned bridges win over user-installed copies while user CLIs and their auth state stay discoverable.GOOSE_SEARCH_PATHSis pinned after shell-env import.node-runtime.jsonmanifest and verifies the detected Node major against each npm bridge's locked engine requirement, so missing/old Node surfaces in doctor instead of a bare exit 127 at session spawn.rg/zsh.Ports the corresponding berd changes to Staged's layout.
🤖 Generated with Claude Code