feat(desktop): bundle pinned ACP bridge tools with the app#1795
Draft
matt2e wants to merge 18 commits into
Draft
feat(desktop): bundle pinned ACP bridge tools with the app#1795matt2e wants to merge 18 commits into
matt2e wants to merge 18 commits into
Conversation
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>
Register the staged ACP tools bin dir (BUZZ_ACP_TOOLS_DIR dev override, else the resources/acp/bin Tauri resource dir) once at app setup, and consult it everywhere bridge commands are resolved: - New managed_agents::acp_tools module: OnceLock registration plus command_in_bundled_dir, which resolves bare command names only — path-like commands name a specific binary the user picked and are never redirected into the bundle (Path::join with an absolute path would replace the bundled dir entirely). - resolve_command_uncached consults the bundled dir first, so the discovery sweep, readiness find_command, and spawn-time agent-command resolution (BUZZ_ACP_AGENT_COMMAND) all prefer the pinned bridges. Registration runs at the top of .setup() because resolutions are cached for the app lifetime. - build_augmented_path gains the bundled dir as its highest-priority segment, covering both the agent spawn PATH and the CLI auth-probe PATH, and now joins entries best-effort: an entry embedding the PATH separator (legal in macOS paths) is dropped with a log line instead of collapsing the entire augmented PATH to None. The donor's GOOSE_SEARCH_PATHS pinning is deliberately not ported — Buzz spawns bridges via its own buzz-acp harness, which takes the resolved command from BUZZ_ACP_AGENT_COMMAND and the augmented PATH. The discovery.rs file-size ratchet is bumped 1171 -> 1178 for the new sweep check; the codex version-gate retirement later in this series shrinks the file and ratchets it back down. Ports block/builderbot apps/staged 55eb2772 (resolve bundled ACP tools at runtime) and afa524e7 (best-effort PATH join), themselves ports of squareup/berd 2add3727 and bb912c96. Verification: - cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib: 1382 passed, 0 failed, 11 ignored (14 new tests cover env-override precedence, bare-name-only bundled resolution, executable checks, bundled-dir PATH priority, and un-joinable entry handling) - cargo clippy --all-targets: clean; cargo fmt --check: clean Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The bundled ACP bridges are shell shims that exec node; without a suitable Node.js on the spawn PATH the first agent session dies with a bare exit 127. Surface the requirement in the Doctor panel instead: - New managed_agents::node_runtime module reads the staged resources/acp/node-runtime.json manifest (one entry per npm-sourced bridge, each with its own required Node major), resolves node from the same augmented PATH the agent spawn and CLI auth probes use (so the check cannot disagree with what the wrapper shims find at spawn time), probes `node -p process.versions.node` with a 10s timeout, and reports pass/warn with a per-bridge satisfied/unmet/unknown verdict list. A missing manifest keeps the check silent (no npm-sourced bridges bundled); an unreadable one warns instead of hiding a packaging break. - New check_acp_node_runtime Tauri command plus a Doctor panel section (message, node path, per-bridge requirements, Install Node.js fix link on warn) that the Re-run button refreshes alongside the runtime rows. The e2e mock bridge grows a nodeRuntimeCheck fixture knob. - tokio gains the "process" feature for the async node probe. Readiness gating for bundled bridges needs no code here: Buzz's classify_runtime already reports adapter_missing when find_command (which prefers the bundle since the previous commit) cannot resolve the bridge, and cli_login_requirements resolves the adapter the same way — so a broken bundle reads not_installed instead of ready. This is the behaviour squareup/berd 17c5e9e5 had to add explicitly. Ports block/builderbot apps/staged adea4017 (node-runtime doctor manifest), itself a port of squareup/berd 07087303. Verification: - cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib: 1392 passed, 0 failed, 11 ignored (10 new tests: version parsing, requirement labels, all four check states, manifest missing/invalid/ loaded including the exact staging-script JSON shape) - cargo clippy --all-targets: clean; cargo fmt --check: clean - desktop: tsc --noEmit clean; biome clean; pnpm test 2744 passed - playwright doctor-states.spec.ts (smoke): 8 passed, including new 06-node-runtime-pass / 07-node-runtime-warn / 08-node-runtime-hidden-when-not-bundled Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
With the ACP bridges (@agentclientprotocol/claude-agent-acp, @agentclientprotocol/codex-acp) bundled with the app at pinned versions and resolved ahead of user installs, three pieces of machinery are dead code for those two runtimes: - The codex 0.16.x version gate: probe_codex_acp_major_version, codex_adapter_availability / codex_adapter_is_outdated, and the AdapterOutdated availability status (Rust enum variant plus the frontend "adapter_outdated" union member and all its UI branches). The bundled adapter is always the pinned 1.x package, so probing for the deprecated @zed-industries/codex-acp package has no trigger left. - The EEXIST uninstall-then-reinstall two-step in plan_adapter_install, which existed only to swap the deprecated codex package for the new one. The plan is now the simple missing -> catalog-commands mapping, and the function drops its runtime_id parameter. - The in-app `npm install -g` flow for the two bundled bridges: their catalog entries now carry empty adapter_install_commands and a hint that the adapter ships with the Buzz desktop app. Goose keeps its npm install flow untouched — its adapter is not bundled. cli_login_requirements now classifies availability purely via classify_runtime (which resolves bundle-first), replacing the codex version-probe special case. The configNudge validator rejects the retired "adapter_outdated" literal, so stale nudge JSON emitted by an older app version cannot render a card the current UI has no branch for (regression test inverted accordingly). This is a Buzz-specific retirement enabled by the bundling series; the donor series (block/builderbot apps/staged, berd) had no equivalent version gate to remove. File-size ledger entries for discovery.rs (1178 -> 1056), discovery/tests.rs (1029 -> 825), and readiness.rs (1754 -> 1583) are ratcheted down to bank the deletions, per the note left in the bundled-resolution commit. Verification: - cargo test --lib (desktop/src-tauri): 1381 passed, 0 failed - cargo clippy --lib --tests -D warnings: clean - cargo fmt --check: clean - tsc --noEmit: clean; biome check src: no new diagnostics - pnpm test (desktop): 2744 passed, 0 failed - node scripts/check-file-sizes.mjs: pass with ratcheted limits Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
install_acp_runtime_blocking previously reported success whenever every
install step it ran succeeded. For the bundled bridges (claude, codex)
the adapter install plan is empty, so a broken bundle would run zero
steps and report success — discovery would immediately classify the
runtime as not installed again, an install-succeeds/still-broken loop
with nothing actionable surfaced to the user.
Add a Phase 3 verification gate after the final resolve-cache clear: a
pure adapter_verification_step seam re-resolves the runtime's adapter
commands and, when none resolves, appends a failed synthetic "verify"
step and flips the result to success:false. The hint points at
reinstalling Buzz when the adapter is bundled (the catalog carries no
install commands) and at the install step output for npm-installed
adapters like goose.
Ports the install-verification gate from the berd donor series
("require the bundled bridge in install verification", berd 4028e34c,
squashed into berd 09388d7): post-fix verification must apply the same
resolved-bundled-bridge gate as readiness, otherwise an install can
verify as success while the card immediately flips back to
not-installed. The readiness half of that gate is already structural
in Buzz — classify_runtime yields AdapterMissing/NotInstalled when the
bundle-first resolution finds no adapter.
Verification:
- cargo test --lib (desktop/src-tauri): 1385 passed, 0 failed
(4 new adapter_verification_step tests: resolves -> None, bundled
failure carries the reinstall-Buzz hint, unbundled failure does not,
empty command list -> nothing to verify)
- cargo clippy --lib --tests -D warnings: clean
- cargo fmt --check: clean
- node desktop/scripts/check-file-sizes.mjs: pass
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Two quality gates fail at the branch head with no working-tree changes, blocking any new commit behind the pre-commit/pre-push hooks: - clippy 1.95 flags the redundant closure around process_is_running in should-restart discovery wiring (agent_discovery.rs); pass the function directly. - agent_discovery.rs measures 1400 lines against its 1398 file-size ledger limit (two lines landed without a matching ratchet bump); ratchet the ledger to match reality — this change adds no lines. The ledger also carries the ratchet bumps (discovery.rs 1130 -> 1134, tauri.ts 1317 -> 1320, types.ts 1049 -> 1051) for the Doctor bundled-adapter copy change that lands in the next commit: lefthook checks out the staged half of a partially staged file while running pre-commit, so splitting the ledger hunks across the two commits makes the gate judge the next commit's files against the old limits. Verification: cargo clippy --lib --tests -D warnings (desktop/src-tauri) clean; node desktop/scripts/check-file-sizes.mjs passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
… path For the bundled claude/codex bridges, the Doctor panel rendered the resolved adapter path — a deep resource-dir path like /Applications/Buzz.app/Contents/Resources/resources/acp/bin/… — plus an "Available via installed on PATH" description that is wrong for a bundled tool. Replace both with the plain statement "ACP bridge bundled with Buzz." and keep showing only the user's CLI path. - AcpRuntimeCatalogEntry gains adapter_bundled, computed in the discovery sweep via the new acp_tools::path_is_in_bundled_dir (component-wise starts_with against the registered bundled tools bin dir, false when no bundle is registered). It covers both states that surface an adapter path: available and cli_missing. - Doctor RuntimeRow: when adapter_bundled, the "Available via …" line becomes "ACP bridge bundled with Buzz.", the mono adapter-path line is dropped (the CLI path stays), and the cli_missing copy reads "ACP bridge bundled with Buzz, but the <label> CLI is not installed." instead of quoting the bundle path. - adapter_bundled is optional on the raw TS type (mapped ?? false) so existing e2e fixtures stay valid; the field is always sent by the backend. The file-size ledger bumps for discovery.rs / tauri.ts / types.ts landed with the preceding gate-repair commit (lefthook checks out the staged half of partially staged files, so the ledger could not be split across the two commits). Verification: - cargo test --lib (desktop/src-tauri): 1423 passed, 0 failed (new component-wise path_is_in_dir test) - cargo clippy --lib --tests -D warnings: clean; cargo fmt --check: clean - tsc --noEmit: clean; biome check: no new diagnostics - pnpm test (desktop): 2744 passed, 0 failed - playwright doctor-states.spec.ts: 9 passed, including new 09-bundled-adapter asserting the copy renders, the bundle path does not, and the CLI path still does - node scripts/check-file-sizes.mjs + check-px-text.mjs: pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…back The claude catalog entry still listed the retired Zed-era claude-code-acp adapter as a resolution fallback after claude-agent-acp. Since the bundling series, the bundled claude-agent-acp always resolves first, so the fallback is unreachable in the discovery sweep, readiness classification, and install verification — and were the bundle ever broken, silently spawning a stray install of the deprecated package is exactly the drift the series exists to prevent. Reporting adapter_missing (with the reinstall-Buzz hint) is the correct outcome there. Move the name from the claude entry's commands to its aliases: known_acp_runtime still maps stored records, override pins, and avatar lookups carrying the legacy command to the claude runtime (the override tests feeding claude-code-acp all pass unchanged), but no resolution path sweeps for it anymore. The commands/aliases split this makes load-bearing is now documented on the KnownAcpRuntime fields, and comments claiming discovery "may select an installed alias" are reworded to the stored-record reality. The default_agent_args normalization arm and the frontend/buzz-acp normalization paths (agentReuse.ts, buzz-acp config.rs) deliberately keep the name for stored records, as does KNOWN_AGENT_BINARIES for orphan-process cleanup. Completes item B.3 of the post-bundling cleanup plan. The discovery.rs file-size ledger is ratcheted 1134 -> 1139 for the five new comment lines (the file sat exactly at its limit). Verification: - cargo test --lib (desktop/src-tauri): 1423 passed, 0 failed, 11 ignored — including the avatar-url and override-pin tests that exercise claude-code-acp identity mapping via the alias path - cargo clippy --lib --tests -D warnings: clean - cargo fmt --check: clean - node desktop/scripts/check-file-sizes.mjs: pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The Doctor page's framing copy — header description, section heading,
loading and empty states — spoke in protocol terms ("ACP runtime
commands", "Agent CLIs and ACP runtimes") that mean nothing to users
scanning the page. Rewrite the five page-level strings in
DoctorSettingsPanel.tsx to plain "AI agents" language:
- Header description: "Check that everything needed to run AI agents
is set up on this computer." (was "Verify the ACP runtime commands
available to the desktop app.")
- Section heading: "AI agents" (was "Agent CLIs and ACP runtimes")
- Section description: "Whether each supported agent is installed and
signed in."
- Loading state: "Checking for installed agents…"
- Empty state: "No supported AI agents found on this computer."
Row-level and diagnostic detail deliberately keeps its ACP wording —
the mono "ACP adapter:" path label, per-runtime status sentences
("ACP bridge bundled with Buzz.", the cli_missing variants), and all
backend-sourced hints — since users digging into a specific runtime's
row can handle the protocol name. The onboarding SetupStep framing and
the config-nudge card title have the same split and are out of scope
for this pass.
Copy-only change: no test, fixture, or e2e spec asserts any of the
five old strings.
Verification:
- tsc --noEmit: clean
- biome check DoctorSettingsPanel.tsx: clean
- node scripts/check-file-sizes.mjs + check-px-text.mjs: pass
(file shrank by one line; no ledger change needed)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…i_missing gate
Since the ACP bundles vendor full native CLIs (Claude Code inside
@anthropic-ai/claude-agent-sdk-*, codex inside @openai/codex-*), agent
sessions never touch a user-installed claude/codex. Yet the auth probes
still resolved those CLIs from the user's PATH, and a missing user CLI
put the runtime behind a CliMissing availability gate — blocking agents
that would have run fine on the bundle. Point the probes at the vendored
CLIs and delete the gate plus everything downstream of it (curl-pipe CLI
install commands, Doctor's user-CLI path row, the cli_missing nudge card).
The vendored CLIs are deliberately NOT staged into resources/acp/bin:
that dir is the highest-priority segment of the agent-spawn PATH, and
CLIs there would shadow the user's (possibly newer) claude/codex inside
every session. Instead a probe-only manifest maps CLI names to paths
inside the staged node trees.
- prepare-acp-tools-resource.sh reads nativePackage/nativeExecutable
from acp-tools.lock.json, verifies the vendored binary exists in the
staged tree (chmod +x), and writes harness-clis.json next to
node-runtime.json: {"clis":[{id,cli,path}]} with resource-root-relative
paths. The manifest is gitignored like its sibling.
- acp_tools::bundled_harness_cli() resolves a CLI name through the
manifest — bare names only, relative non-escaping paths only, must be
an executable file. Absent manifest/entry degrades to None.
- discovery::resolve_probe_binary() tries the bundled CLI first, then
the user's PATH — auth probes in the discovery sweep and
readiness::cli_login_requirements both go through it. Auth state now
reflects the CLI the sessions actually use, while dev builds without
a staged bundle keep working via the PATH fallback.
- classify_runtime: a resolving adapter is Available unconditionally;
underlying_cli now only disambiguates AdapterMissing vs NotInstalled.
CliMissing is removed from the desktop enum (runtime-only, never
persisted) and from the TS union; buzz-acp's wire mirror keeps the
variant so payloads from older desktops still parse (AdapterOutdated
precedent), while the FE nudge validator rejects the retired literal
so stale JSON can't render a card the UI has no branch for.
- claude/codex catalog entries drop underlying_cli and the curl-pipe
cli_install_commands; login provisioning copy stays as-is.
- UI: cli_missing branches removed from Doctor (StatusIcon, RuntimeRow,
user-CLI path row), SetupStep, persona pickers ("(CLI missing)"
suffix + sort rank), AgentDefinitionDialog warning, and the nudge
card; retired the cli_missing nudge screenshot spec.
Verification:
- cargo test --lib (desktop/src-tauri): 1436 passed, 0 failed (new
bundled_harness_cli manifest/escape/absent tests, probe-runs-without-
underlying-CLI readiness test, adapter-implies-Available discovery test)
- cargo test -p buzz-acp --lib: 497 passed (cli_missing round-trip kept)
- cargo clippy --lib --tests -D warnings (both crates): clean;
cargo fmt --check (both crates): clean
- tsc --noEmit: clean; biome check + file-size/px-text/pubkey guards: pass
- pnpm test (desktop): 2792 passed, 0 failed (new validator-rejects-
cli_missing test)
- playwright doctor-states.spec.ts + doctor-cta-screenshots.spec.ts:
12 passed (Doctor shows no CLI path row; nudge cards unaffected)
- prepare-acp-tools-resource.sh: manifest written; vendored
claude --version → 2.1.205 (Claude Code); vendored
codex login status → exit 0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…th Buzz." The Doctor runtime row's status line for a bundled adapter read "ACP bridge bundled with Buzz." — the protocol jargon adds nothing in a row that already names the runtime, and the sentence only needs to say where the tool comes from. Shorten it to "Bundled with Buzz." (keeping the trailing period to match the sibling "Available via ..." line). Updated in step: the DoctorSettingsPanel comment quoting the line, the 09-bundled-adapter e2e assertion and its doc comment, and the AcpRuntimeCatalogEntry::adapter_bundled doc comment that cites the UI copy. No other test or fixture asserts the old string; the node-runtime check's backend-sourced messages keep their ACP wording per the row-detail/page-framing split from the de-jargon pass. Verification: - tsc --noEmit: clean; biome check on both touched TS files: clean - cargo fmt --check (desktop/src-tauri): clean (doc-comment-only change) - playwright doctor-states.spec.ts: 9 passed, including 09-bundled-adapter against the new copy Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
… drift machinery Since the bundling series emptied every runtime's adapter_install_commands, three pieces of machinery in the earlier cleanup plan were dead code. Delete them, and fix the Phase-3 install verification hint that the emptying broke for goose: - node_required install gate: runtime_needs_npm was constant false, so the node_required computation never fired and Doctor's amber "Node.js is required to install this adapter" callout could never render. Deleted the trigger (discovery.rs), the wire field (types.rs, tauri.ts, types.ts), the callout component + the nodeRequired half of the Install-button gate (DoctorSettingsPanel.tsx), and the fixture fields (e2eBridge.ts, agentReadiness.test.mjs, doctor-states.spec.ts). E2e test 04-node-required force-fed the retired state and is deleted; the bundled bridges' real Node.js requirement bites at spawn, not install, and is covered by the node-runtime Doctor section (07-node-runtime-warn). The cleanup plan's per-row spawn-time callout derived from nodeRuntimeCheck remains open as a follow-up. - npm preflight/EACCES machinery: Phase 2 of install_acp_runtime_blocking applied npm handling only to adapter_install_commands — empty everywhere. Deleted is_npm_global_install (+7 tests), npm_eacces_hint/npm_eacces_guidance/ NPM_MISSING_HINT (+5 tests), npm_preflight_check/resolve_npm_prefix/ npm_install_target_is_writable/unix_is_writable (+3 tests), the Phase-2 npm branches, and the stale Phase-1 preflight note. - codex adapter-availability drift stamp: built to catch a manual npm install/downgrade flipping the adapter under a running agent — impossible now that bare-name resolution prefers the bundle. Deleted the cache + availability_drift predicate (+5 tests), the codex-only discovery warm, the spawn-time stamp, the availability_drift half of needs_restart (hash_drift stays), and the ManagedAgentProcess field with its finish_spawn plumbing. Goose verify hint fix: adapter_verification_step received bundled = adapter_install_commands.is_empty(), now true for all four runtimes — a goose curl install that succeeded but left `goose` unresolvable claimed "The Goose ACP adapter ships with the Buzz desktop app… Reinstall Buzz", wrong on both counts. The new runtime_adapter_is_bundled seam requires cli_install_commands AND adapter_install_commands to be empty (goose has a curl CLI installer; claude/codex/buzz-agent have neither), with catalog-driven tests pinning goose to the check-the-step-output hint. That hint also drops its "and your npm global prefix" tail — no npm-installed adapters remain in the catalog. File-size ledger ratcheted down to bank the deletions: agent_discovery.rs 1410 -> 1021, discovery.rs 1147 -> 1046, runtime.rs 2216 -> 2079, tauri.ts 1342 -> 1340, types.ts 1066 -> 1064. Covers sections 1 and 3 of the post-bundling cleanup note; the adapter_missing retirement (section 2) and copy/fixture staleness (section 4) are separate follow-ups. Verification: - cargo test --lib (desktop/src-tauri): 1423 passed, 0 failed (1436 at head - 15 deleted npm/drift tests + 2 new bundled-flag tests) - cargo clippy --lib --tests -D warnings: clean; cargo fmt --check: clean - tsc --noEmit: clean; biome: no new diagnostics - pnpm test (desktop): 2792 passed, 0 failed - playwright doctor-states.spec.ts: 8 passed; doctor-cta-screenshots.spec.ts: 3 passed - node scripts/check-file-sizes.mjs + check-px-text.mjs: pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
A partial run like `just bump-acp-tools --target aarch64-apple-darwin`
rewrote the entire acp-tools.lock.json with only the selected targets,
silently dropping the other three targets' entries. Because ensure/
prepare treat a target with zero lock entries as success (stage
nothing, exit 0), a later desktop-release-build for a dropped target
would ship an empty resources/acp and silently fall back to unpinned
user installs — exactly the drift the bundling series exists to
prevent.
Make --target runs merge instead of replace: before resolving, read
the existing lock and carry over every entry whose target is not
selected, then write the union. The guard rails match the script's
fail-loudly ethos:
- A missing lock preserves nothing (there is nothing to drop) and the
run proceeds — first-time generation with --target still works.
- An unreadable, unparseable, or tools-array-less lock aborts the
partial run before any registry work, refusing to overwrite pins it
cannot see.
- Full runs (no --target, or all four targets) never read the lock,
so `just bump-acp-tools` can still regenerate a corrupt lock from
scratch.
Preserved entries are logged ("Preserved 6 existing lock entries for
unselected targets") so a partial bump's diff is explainable from its
output, and the usage text documents the merge behaviour.
Addresses the partial-run finding from the bundling-series code
review (review 2ed3d00d on b52a665).
Verification (against the Block registry, temp lock files):
- --target aarch64-apple-darwin on a copy of the committed lock:
6 unselected entries preserved byte-identically; merged lock
byte-identical to the committed lock (registry pins unchanged);
8 entries across all 4 targets.
- --target with no existing lock: writes only the selected target's
2 entries, exit 0.
- --target with corrupt JSON / {"tools":"nope"}: exits 1 in <1s with
a refusing-to-overwrite message; lock file left untouched.
- pnpm exec biome check + node scripts/check-file-sizes.mjs: clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…dir runs
ensure-acp-tools.sh printed "No ACP tools locked for target ..." only
in the non---print-bin-dir branch, so the exact invocation path release
builds take (prepare-acp-tools-resource.sh capturing --print-bin-dir
output) suppressed the notice entirely. An empty-target staging — the
failure mode the lock-merge fix guards against — left no trace in build
logs and was only discoverable later via Doctor showing the bridges as
not installed.
Emit the notice to stderr unconditionally before the --print-bin-dir
branch: stderr keeps the --print-bin-dir stdout contract (a single bin
dir path) intact while making the empty staging visible in release
build logs, matching the script's other diagnostics ("Installing ACP
tool ..." already goes to stderr).
Addresses the suppressed-notice finding from the bundling-series code
review (review 2ed3d00d on b52a665).
Verification:
- --target fake-unknown-target --print-bin-dir: notice on stderr,
stdout is exactly the bin dir path, exit 0
- --target fake-unknown-target (no flag): notice on stderr, empty
stdout, exit 0
- real host target --print-bin-dir: no-op stage, prints only the bin
dir path, exit 0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ACP staging codesign_if_darwin ran `codesign ... >/dev/null 2>&1 || true`, discarding both the exit status and the error output. An unsigned nested Mach-O therefore never surfaced at stage time — it surfaced much later as Gatekeeper killing a subprocess mid-session, the exact failure mode the nested-Mach-O signing scan exists to prevent, with nothing in the build output to connect the two. Keep the failure non-fatal (an unsignable Mach-O fragment that never executes should not sink the stage, and release builds re-sign with the real identity anyway) but make it visible: capture codesign's combined output and, on non-zero exit, print a stderr warning naming the file plus codesign's own diagnostics. Addresses the swallowed-codesign-failure finding from the bundling-series code review (review 2ed3d00d on b52a665). Verification: - Forced failure (Mach-O in a read-only directory): warning with file path and codesign's "internal error in Code Signing subsystem" on stderr; script continues under set -e, exit 0. - Notable non-failure probed while testing: codesign xattr-signs non-Mach-O and even corrupt-header files successfully, so the realistic trigger is filesystem/permission trouble, not file(1) false positives. - Full prepare-acp-tools-resource.sh run: no warnings, both manifests written, all 5 staged Mach-Os pass codesign --verify. - bash -n: syntax OK. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
acp-tools.lock.json pinned Block's internal Artifactory host in all 24 tarball URL fields — internal infrastructure leaked into an OSS repo, and anyone regenerating the lock against a different registry churned every URL in the diff even when no pin changed. The URLs are informational only: ensure-acp-tools.sh installs via the developer's ambient npm registry and validates the registry-agnostic sha512 integrity from npm's package-lock, so no install path dereferences them. Normalize at write time instead: update-acp-tools-lock.mjs rebuilds every tarball URL on https://registry.npmjs.org from the resolved package name plus the registry-reported tarball basename (the basename is not derivable from name@version alone — @openai/codex native tarballs carry a platform suffix like codex-0.144.1-darwin-arm64.tgz). A dist.tarball with no /-/ segment fails loudly rather than writing an unnormalizable URL. The usage text documents the normalization, and the committed lock is regenerated: the diff is exactly the 24 tarball fields; every version and integrity hash is byte-identical. Addresses the internal-Artifactory-URL finding from the bundling-series code review (review 2ed3d00d on b52a665). Verification (ambient registry = Block Artifactory, proving normalization against an internal mirror): - Full regeneration: versions/integrity byte-identical to the previous lock; only tarball hosts changed. - Partial --target aarch64-apple-darwin rerun on the normalized lock: byte-identical output (preserved + fresh entries agree). - All normalized URL shapes probe live on registry.npmjs.org (303 CDN redirect), including the platform-suffixed codex native tarball. - ensure-acp-tools.sh with the new lock: validates and no-op stages, exit 0 (the freshness stamp does not compare tarball fields, so no spurious re-install). - pnpm exec biome check on the script: clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The bundling series wired ACP tool staging into `just dev`, `just staging`, and `just desktop-release-build`, and flagged the internal sprout-releases pipeline as follow-up — but the OSS release workflow was missed entirely. None of release.yml's four platform jobs ran prepare-acp-tools-resource.sh before `pnpm tauri build`, so every OSS release artifact (DMGs, deb/AppImage, NSIS installer) shipped an empty resources/acp: the app silently fell back to unpinned user-installed bridges, which is exactly the drift the bundling series exists to prevent, while the catalog's "ships with the Buzz desktop app… Reinstall Buzz" copy pointed users at a reinstall that could never fix it. Add a "Stage bundled ACP tools" step to all four jobs, after the sidecar build and before the Tauri build (tauri.conf.json already bundles resources/acp): - macOS arm64 / Intel and Linux stage their target's pinned npm trees and bash wrapper shims; node + npm come from the already-activated hermit toolchain. - Windows runs the same script under Git Bash (the job already runs every step with `shell: bash`; node comes from setup-node). The lock currently pins no windows-msvc entries, so the step is a visible no-op — ensure-acp-tools.sh prints its empty-target notice to the build log and stages nothing — until the Windows bundling lands. Ensuring the staging scripts run before every `tauri build` invocation path was the first work item of the Windows-bundling plan, independent of the Windows decision itself. Verification: - prepare-acp-tools-resource.sh aarch64-apple-darwin: staged wrappers report the pinned 0.58.1 / 1.1.2 versions (same invocation the new steps run, on the same runner OS as the arm64 job). - ensure-acp-tools.sh --target x86_64-pc-windows-msvc (pre-Windows- bundling lock state): "No ACP tools locked" notice on stderr, empty staging, exit 0 — the Windows job stays green before the lock gains windows entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The windows-x86_64 build was the one supported target left out of the
ACP bundling series: no lock entries, no staged resources, and — since
the series made the claude/codex catalog entries platform-unconditional
(empty install commands, "ships with the Buzz desktop app" hint) — a
Windows user without their own claude-agent-acp hit a dead end that
reinstalling could never fix. Everything upstream already exists (both
bridges publish win32-x64 native packages; the release job already runs
under Git Bash; the desktop's resolution/spawn layer is Windows-aware),
so extend the bundle to Windows. The one genuinely Windows-shaped
problem is the runtime shim: the staged bridges are bash wrappers that
Windows cannot execute — and the resolver only looks for <binary>.exe
anyway. Solve it with a tiny compiled launcher instead of .cmd shims,
which would have rippled a two-candidate list (.exe, .cmd) through the
shared resolution code and left an intermediate cmd.exe in the spawn
chain that kill_on_drop cannot reap through.
- New root-workspace crate buzz-acp-node-launcher: reads the sibling
<binary>.shim.json ({entrypoint, nodeEngine, requiredNodeMajor})
written by the staging scripts, resolves node from PATH, enforces the
lock's Node major with the exact wrapper-shim error message and exit
codes (127 missing / 1 too old), then runs node on the vendored
entrypoint, proxying stdio and the exit code. On Windows the child
joins a Job Object with KILL_ON_JOB_CLOSE (mirroring buzz-dev-mcp's
KillGroup) so terminating the launcher — as buzz-acp's kill_on_drop
does — takes the node tree with it; on Unix it execs node like the
bash shim, existing there so workspace clippy/tests pass everywhere.
Unsafe is confined to the Win32 FFI, cfg-forbidden elsewhere, per the
buzz-dev-mcp precedent.
- update-acp-tools-lock.mjs gains x86_64-pc-windows-msvc (npmOs win32,
npmCpu x64, no libc; native executables claude.exe and
vendor/x86_64-pc-windows-msvc/bin/codex.exe). The committed lock
grows 8 -> 10 entries via a partial --target run: the two new
Windows pins match the other targets' versions exactly (0.58.1 /
1.1.2, sdk 0.3.205, codex 0.144.1-win32-x64) and the existing 8
entries are preserved byte-identically.
- The staging scripts branch per target family through the shared
wrapper lib: Unix targets keep the bash wrapper; Windows targets
stage the launcher as <binary>.exe next to <binary>.shim.json
(write_windows_node_launcher). The launcher builds via cargo on
first use (ACP_NODE_LAUNCHER_EXE overrides; target dir resolved via
cargo metadata, never ./target), a target with no locked tools still
stages nothing without needing cargo, and dev-cache shims embed
bin-dir-relative entrypoints because Git Bash absolute paths
(/c/Users/...) are unresolvable to a native exe. The freshness path
re-copies the launcher when the built binary changes (cmp-gated: a
running agent's open .exe is never rewritten), the prune reduces
<binary>[.exe][.stamp] and <binary>.shim.json to the lock's bare
binary name, and harness-clis.json drops the .exe suffix from its
cli keys so the app's bare-name auth probes ("claude", "codex")
resolve the vendored CLIs on Windows too.
- release.yml's Windows job needs no extra wiring: the staging step
added with the previous commit now finds lock entries and builds the
launcher with the job's already-installed MSVC toolchain. The
windows-rust CI job gains a cargo test step for the launcher so its
spawn path gates on a real Windows runner.
Per the plan's risk note, codex-on-Windows maturity is a validate-
before-release concern: the lock's per-tool-per-target shape allows
dropping the codex-acp windows entry if a real session shakes out
badly. win32-arm64 packages exist but there is no arm64 Windows
release job; deferred until one exists. Node.js stays a user
prerequisite, surfaced by the existing node-runtime Doctor section.
Verification (macOS host):
- cargo test -p buzz-acp-node-launcher: 9 passed — manifest parsing,
version parsing, path resolution, plus end-to-end launcher runs
(arg/stdio/exit-code proxying via real node, missing-manifest,
missing-entrypoint, and too-old-Node failures with the wrapper-shim
message). cargo clippy --all-targets -D warnings and
cargo fmt --all --check: clean.
- Cross-staged the Windows target end-to-end with
ACP_NODE_LAUNCHER_EXE standing in for the MSVC launcher: both win32
npm trees install and validate against the lock (integrity + X_OK on
the vendored claude.exe/codex.exe, confirming the locked vendor
paths), bin dir stages <binary>.exe + .exe.stamp + .shim.json with
relative entrypoints, re-run performs zero installs, stray
.exe/.shim.json artifacts are pruned, and prepare writes
harness-clis.json with bare "claude"/"codex" keys mapping to the
vendored .exe paths.
- Empty-target Windows staging (aarch64-pc-windows-msvc) exits 0 with
the notice, without cargo on PATH.
- Darwin re-stage after the cross-stage: bash wrappers report 0.58.1 /
1.1.2, manifests back to darwin shape — Unix staging unregressed.
- biome check on update-acp-tools-lock.mjs: clean.
Windows-runner validation (NSIS install, Doctor states, live claude +
codex sessions, auth probes against the vendored CLIs) needs a real
Windows machine and rides the first release train with these entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TODO:
claudeorcodexinstalled anymoreExplanation of the change
claude-agent-acpand ``codex-acp` as part of Buzzclaude-agent-acp cliand the equivalent incodex-acpSummary
Buzz desktop previously spawned the
claude-agent-acpandcodex-acpbridges from whatever the user had installed globally vianpm install -g— an unpinned surface with no integrity checking that produced stale-bridge drift and missing-tool failures. This series bundles both bridges with the app at pinned versions and makes the app prefer them end-to-end:desktop/acp-tools.lock.jsonpins@agentclientprotocol/claude-agent-acp0.58.1 and@agentclientprotocol/codex-acp1.1.2 per target with integrity hashes. New scripts install the locked tools into a shared dev cache, stage the vendored npm trees + node wrapper shims intoresources/acp, and ad-hoc sign every nested Mach-O.just dev/just staging/just desktop-release-buildwire the staging in;just bump-acp-toolsregenerates the lock from npmlatest.managed_agents::acp_toolsmodule registers the staged bin dir (BUZZ_ACP_TOOLS_DIRdev override, else the Tauri resource dir) at app setup. Discovery, readiness probes, and agent spawn all consult it first, and it becomes the highest-priority segment of the augmented spawn PATH.node; without a suitable Node on the spawn PATH the first session dies with a bare exit 127. A new Doctor check reads the stagednode-runtime.jsonmanifest, probesnodefrom the same augmented PATH the spawn uses, and reports per-bridge satisfied/unmet verdicts with an Install Node.js fix link.@zed-industries/codex-acpprobe, theAdapterOutdatedstatus (Rust + frontend), and the in-appnpm install -gflow for the two bundled bridges are dead code and removed. Goose keeps its npm install flow — its adapter is not bundled.install_acp_runtime_blockinggains a post-install verification gate that re-resolves the runtime's adapter commands; a broken bundle now fails the install with a reinstall-Buzz hint instead of looping install-succeeds/still-not-installed.Ports the Staged implementation from block/builderbot#876, itself ported from squareup/berd. The sprout-releases internal pipeline will need the same staging step before its
tauri build(separate repo).Tradeoff: bundle size
The pinned bridges vendor full native CLIs (Claude Code and
codex), so the shipped app grows substantially:.app).dmg)"Before" is measured (installed v0.4.2 app, v0.4.2 release DMG); "after" is that baseline plus the measured 586 MB staged
resources/acptree (gzip -6 compresses it to ~192 MB — the DMG's zlib should land in the same range). The payload is dominated by the vendoredcodex(247 MB) and Claude Code (227 MB) binaries, which are what let auth probes and sessions run without any user-installed CLI. Linux bundles grow by a similar order; Windows is not in the lock and is unchanged.Test plan
cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib: 1422 passed, 0 failed (28 new tests across acp_tools resolution, node_runtime parsing/probing, and adapter install verification)cargo clippy --all-targets -D warnings,cargo fmt --check: cleantsc --noEmitclean, biome clean,pnpm test2744 passeddoctor-states.spec.ts(smoke): 8 passed, including new node-runtime pass/warn/hidden statescodesign --verify🤖 Generated with Claude Code
