Files
buzz/desktop/scripts/lib/acp-node-wrapper.sh
T
Matt TooheyandClaude Fable 5 a29a3da6fe feat(desktop): pin and stage bundled ACP bridge tools
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>
2026-07-15 13:38:21 +10:00

62 lines
2.5 KiB
Bash

# Shared Node wrapper generation for ACP bridge tools staged from npm.
# Sourced by ensure-acp-tools.sh and prepare-acp-tools-resource.sh so the
# wrapper staged into the dev cache and the wrapper bundled into app
# resources cannot drift (a drift would make dev and bundled installs fail
# differently on the same missing/old Node runtime).
#
# acp_required_node_major <node-engine>
# Prints the minimum Node.js major version implied by a ">=N..." engine
# range, defaulting to 22 when the range is not in that form. Shared by
# the wrapper shim below and the node-runtime.json manifest consumed by
# the app's Node.js runtime doctor check, so the version the wrapper
# enforces at spawn time and the version the doctor reports at setup
# time cannot disagree.
acp_required_node_major() {
local node_engine="$1"
local major
major="$(printf '%s\n' "$node_engine" | sed -n 's/^>=\([0-9][0-9]*\).*$/\1/p')"
if [[ -z "$major" ]]; then
major=22
fi
printf '%s\n' "$major"
}
# write_node_wrapper <wrapper> <entrypoint> [node-engine]
# Writes an executable bash shim at <wrapper> that verifies a Node.js
# runtime satisfying <node-engine> (default ">=22") is on PATH, then
# execs node on <entrypoint>. An absolute <entrypoint> is embedded
# verbatim; a relative one is resolved against the wrapper's directory
# at run time.
write_node_wrapper() {
local wrapper="$1"
local entrypoint="$2"
local node_engine="${3:->=22}"
local required_node_major
required_node_major="$(acp_required_node_major "$node_engine")"
mkdir -p "$(dirname "$wrapper")"
{
printf '#!/usr/bin/env bash\n'
printf 'set -euo pipefail\n'
printf 'if ! command -v node >/dev/null 2>&1; then\n'
printf ' echo "%s requires Node.js %s on PATH." >&2\n' "$(basename "$wrapper")" "$node_engine"
printf ' exit 127\n'
printf 'fi\n'
printf 'required_node_major=%q\n' "$required_node_major"
printf 'node_major="$(node -p '\''process.versions.node.split(".")[0]'\'' 2>/dev/null || true)"\n'
printf 'if [[ -z "$node_major" || "$node_major" -lt "$required_node_major" ]]; then\n'
printf ' echo "%s requires Node.js %s on PATH." >&2\n' "$(basename "$wrapper")" "$node_engine"
printf ' exit 1\n'
printf 'fi\n'
if [[ "$entrypoint" == /* ]]; then
printf 'entrypoint=%q\n' "$entrypoint"
else
printf 'wrapper_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\n'
printf 'entrypoint="$wrapper_dir"/%q\n' "$entrypoint"
fi
printf 'exec node "$entrypoint" "$@"\n'
} > "$wrapper"
chmod +x "$wrapper"
}