mirror of
https://github.com/block/buzz.git
synced 2026-11-13 12:51:34 +00:00
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>
129 lines
4.8 KiB
Bash
Executable File
129 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
app_root="$(cd "$script_dir/.." && pwd)"
|
|
lock_file="${ACP_TOOLS_LOCK_FILE:-$app_root/acp-tools.lock.json}"
|
|
|
|
# shellcheck source=lib/acp-node-wrapper.sh
|
|
source "$script_dir/lib/acp-node-wrapper.sh"
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: desktop/scripts/prepare-acp-tools-resource.sh [target-triple]
|
|
|
|
Stages the locked ACP bridge tools into src-tauri/resources/acp so Tauri can
|
|
bundle them as application resources: vendored npm package trees under
|
|
resources/acp/node and executable wrappers under resources/acp/bin. The
|
|
optional target triple defaults to the Rust host target.
|
|
|
|
Note: resources/acp/bin holds a single target at a time, so staging must stay
|
|
tied to the build target.
|
|
USAGE
|
|
}
|
|
|
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
usage
|
|
exit 0
|
|
fi
|
|
|
|
target="${1:-}"
|
|
ensure_args=()
|
|
if [[ -n "$target" ]]; then
|
|
ensure_args+=(--target "$target")
|
|
else
|
|
target="$(rustc -vV | sed -n 's|host: ||p')"
|
|
fi
|
|
if [[ -z "$target" ]]; then
|
|
echo "Could not determine rust host target." >&2
|
|
exit 1
|
|
fi
|
|
|
|
cache_bin_dir="$("$script_dir/ensure-acp-tools.sh" ${ensure_args[@]+"${ensure_args[@]}"} --print-bin-dir)"
|
|
cache_root="$(dirname "$(dirname "$cache_bin_dir")")"
|
|
resource_root="$app_root/src-tauri/resources/acp"
|
|
resource_bin_dir="$resource_root/bin"
|
|
resource_node_dir="$resource_root/node"
|
|
mkdir -p "$resource_bin_dir"
|
|
|
|
# Keep .gitkeep but refresh any staged tools from the lock.
|
|
find "$resource_bin_dir" -type f ! -name ".gitkeep" -delete
|
|
rm -rf "$resource_node_dir"
|
|
mkdir -p "$resource_node_dir"
|
|
|
|
# Manifest for the app's Node.js runtime doctor check, staged next to the
|
|
# bin dir so the app can resolve it as the bin dir's parent. Removed up
|
|
# front so locks with no npm-sourced tools ship no manifest and the doctor
|
|
# check stays silent.
|
|
node_runtime_manifest="$resource_root/node-runtime.json"
|
|
rm -f "$node_runtime_manifest"
|
|
node_runtime_entries=()
|
|
|
|
codesign_if_darwin() {
|
|
local file="$1"
|
|
if [[ "$(uname -s)" == "Darwin" ]] && command -v codesign >/dev/null 2>&1; then
|
|
codesign --force --sign - "$file" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
while IFS=$'\t' read -r id binary package version node_engine; do
|
|
[[ -n "$id" ]] || continue
|
|
install_dir="$cache_root/$target/$id/$version/npm"
|
|
entrypoint="$install_dir/node_modules/$package/dist/index.js"
|
|
if [[ ! -f "$entrypoint" ]]; then
|
|
echo "Locked npm ACP tool missing from cache: $package@$version" >&2
|
|
exit 1
|
|
fi
|
|
resource_package_dir="$resource_node_dir/$id"
|
|
mkdir -p "$resource_package_dir"
|
|
cp -R "$install_dir/." "$resource_package_dir/"
|
|
resource_entrypoint="$resource_package_dir/node_modules/$package/dist/index.js"
|
|
if [[ ! -f "$resource_entrypoint" ]]; then
|
|
echo "Failed to stage npm ACP tool: $package@$version" >&2
|
|
exit 1
|
|
fi
|
|
write_node_wrapper "$resource_bin_dir/$binary" "../node/$id/node_modules/$package/dist/index.js" "$node_engine"
|
|
node_runtime_entries+=("$id"$'\t'"$binary"$'\t'"$node_engine"$'\t'"$(acp_required_node_major "$node_engine")")
|
|
# Ad-hoc sign every Mach-O in the staged package, not just the main CLIs:
|
|
# the codex native package also vendors executables like rg and zsh, and
|
|
# unsigned nested Mach-Os are killed by Gatekeeper. Darwin only, so Linux
|
|
# staging skips the file(1) scan.
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
while IFS= read -r -d '' candidate; do
|
|
if file -b "$candidate" | grep -q "Mach-O"; then
|
|
codesign_if_darwin "$candidate"
|
|
fi
|
|
done < <(find "$resource_package_dir" -type f -print0)
|
|
fi
|
|
done < <(node - "$lock_file" "$target" <<'NODE'
|
|
const fs = require("node:fs");
|
|
const [lockFile, target] = process.argv.slice(2);
|
|
const data = JSON.parse(fs.readFileSync(lockFile, "utf8"));
|
|
for (const entry of data.tools ?? []) {
|
|
if (entry.target !== target || typeof entry.binary !== "string") continue;
|
|
if (entry.source !== "npm") {
|
|
throw new Error(`Unsupported ACP tool source: ${entry.source}`);
|
|
}
|
|
console.log([entry.id, entry.binary, entry.package, entry.version, entry.nodeEngine ?? ">=22"].join("\t"));
|
|
}
|
|
NODE
|
|
)
|
|
|
|
# One manifest entry per npm-sourced bridge, each carrying its own required
|
|
# Node major, so bridges with different engine ranges surface distinct
|
|
# requirements in the doctor check.
|
|
if ((${#node_runtime_entries[@]} > 0)); then
|
|
node -e '
|
|
const fs = require("node:fs");
|
|
const [manifestFile, ...entries] = process.argv.slice(1);
|
|
const tools = entries.map((line) => {
|
|
const [id, binary, nodeEngine, requiredNodeMajor] = line.split("\t");
|
|
return { id, binary, nodeEngine, requiredNodeMajor: Number(requiredNodeMajor) };
|
|
});
|
|
fs.writeFileSync(manifestFile, `${JSON.stringify({ tools }, null, 2)}\n`);
|
|
' "$node_runtime_manifest" ${node_runtime_entries[@]+"${node_runtime_entries[@]}"}
|
|
echo "Wrote ACP Node runtime manifest: $node_runtime_manifest"
|
|
fi
|
|
|
|
echo "Staged ACP tools resource: $resource_bin_dir"
|