Files
buzz/scripts/build-sprig.sh
Salman Mohammed 5621006bcf refactor: move agent Git bootstrap into ACP harness (#7819)
discovered in
https://github.com/block/buzz/issues/7742#issuecomment-5783512985

## Summary

Goose uses its native shell, so it bypassed the Git identity and signing
setup inside `buzz-dev-mcp`. Agent commits could use the host Git
identity, while MCP shells had a separate config. This moves Git setup
into `buzz-acp`, where both paths receive the same agent identity, Nostr
signer, and relay-scoped credential helper.

Before:

```text
Buzz Desktop
  └─ buzz-acp                 ← Desktop supplied relay Git authentication
      ├─ Goose → native shell ← no agent Git author or signing setup
      └─ Buzz Agent
          └─ buzz-dev-mcp
              └─ shell         ← MCP created the keyfile and Git signing config
```

After:

```text
Buzz Desktop or Sprig
  └─ buzz-acp                 ← owns the keyfile, identity, signing, and scoped credentials
      ├─ Goose → native shell ← inherits the complete Git environment
      └─ Buzz Agent
          └─ buzz-dev-mcp
              └─ shell         ← receives the same Git environment via mcpServers[].env
```

The harness keeps the signing key in a private temporary file and
removes it on normal exit, startup failure, and graceful termination. It
composes inherited `GIT_CONFIG_*` entries, forwards the complete block
to MCP servers, and bundles the helpers through the ACP multicall
entrypoint. For standard `buzz-acp` launches, Desktop and Sprig no
longer write Git config. A custom Desktop ACP command bypasses the
harness, so Desktop preserves its prior relay-scoped credential helper
for that supported override.

This changes Git settings for every ACP runtime. The startup signal path
also changes so a termination during adapter initialization removes the
keyfile. The temporary file can remain after SIGKILL or a machine crash.

### Related issue

Related: #6177 covers stricter commit identity enforcement and touches
the same Git paths. That PR and this one need to be reconciled before
either lands. No issue was found for this runtime bootstrap gap.

### Testing

- Ran the ignored `git_runtime_tests` with real Goose 1.50.0 and Buzz
Agent ACP sessions. `real_goose_native_git_shell` passed again on this
PR branch with `BUZZ_TEST_BIN_DIR` pointing to built binaries. Goose
made a signed commit and tag through its native shell; Buzz Agent did
the same through its MCP shell. Both verified signatures, identity,
credential scoping, and keyfile cleanup.
- Built the Linux Sprig image and ran `scripts/test-sprig-image.sh
buzz-sprig:acp-git-bootstrap`. The container made and verified a signed
commit and tag, then confirmed keyfile removal after SIGTERM.
- `just ci` passed on the original branch. After rebasing onto `main`
and addressing review feedback, the ACP Git bootstrap integration tests,
focused desktop smoke test, custom-command regression test, formatting,
and ACP/Desktop Clippy passed. The pre-push hook hit two
timing-sensitive ACP deadline tests under parallel load; both passed
alone and the complete ACP package passed serially (964 library, 2 Git
bootstrap, 9 pool lifecycle tests). The push used `--no-verify` after
those checks; GitHub CI is rerunning on the new head.
- Under injected `BUZZ_ACP_ALLOWED_RESPOND_TO`, `BUZZ_ACP_LAZY_POOL`,
and `BUZZ_ACP_IDLE_POOL_SLEEP`, the clean-env serial package command in
`crates/buzz-acp/TESTING.md` passed: 964 active library tests, 2 Git
bootstrap tests, and 9 pool lifecycle tests. Both named Databricks OAuth
tests passed locally; another reviewer reproduced their
environment-specific failures on `main`.
- No authenticated relay clone or push was run. The local probes used
disposable repositories and did not contact a relay.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-23 14:41:00 -04:00

195 lines
6.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build Sprig — one deploy-anywhere multicall binary for the Buzz ACP
# harness, agent, and developer MCP. The archive exposes these command names:
#
# sprig implementation binary
# buzz-acp link to sprig (ACP harness and Git helper personalities)
# buzz-agent link to sprig (ACP-compliant agent)
# buzz-dev-mcp link to sprig (developer MCP server; also dispatches
# rg/tree/buzz)
#
# Usage:
# ./scripts/build-sprig.sh [version] [target]
#
# Environment overrides:
# TARGET cross-compile target (defaults to host)
# USE_CROSS=1 use `cross` instead of `cargo` for the build
# BUILD_PROFILE Cargo profile to build/package (default: sprig).
# Set BUILD_PROFILE=release to use Cargo's default release
# profile. BUILD_PROFILE=dev/debug is rejected because Cargo
# writes dev builds to target/debug, not target/dev.
# SKIP_BUILD=1 skip the cargo/cross build (use a prebuilt sprig already
# present in target/[<target>/]<profile>)
# ARCHIVE_BASENAME override the archive basename (sans .tar.gz). Useful for
# rolling releases where the asset filename should be stable
# across builds (e.g. `sprig-<target>`). Defaults to
# `sprig-<version>-<target>`.
# DIST_DIR output directory (default: dist)
#
# Output:
# ${DIST_DIR}/${ARCHIVE_BASENAME}.tar.gz
# ${DIST_DIR}/${ARCHIVE_BASENAME}.tar.gz.sha256
#
# The tarball contains:
# sprig
# buzz-acp
# buzz-agent
# buzz-dev-mcp
# README.md
# sprig.json { version, git_sha, target, binaries: [{name, sha256, size}] }
set -euo pipefail
VERSION="${1:-${VERSION:-0.0.0-dev}}"
HOST_TARGET="$(rustc -vV | sed -n 's|host: ||p')"
TARGET="${2:-${TARGET:-$HOST_TARGET}}"
DIST_DIR="${DIST_DIR:-dist}"
BUILD_PROFILE="${BUILD_PROFILE:-sprig}"
case "$BUILD_PROFILE" in
dev|debug)
echo "error: BUILD_PROFILE=$BUILD_PROFILE is not supported by this script; Cargo writes dev builds to target/debug. Use a release-like profile such as 'sprig' or 'release'." >&2
exit 1
;;
esac
if GIT_SHA="$(git rev-parse HEAD 2>/dev/null)"; then
:
else
GIT_SHA="unknown"
fi
BUNDLE_BIN="sprig"
COMMANDS=(buzz-acp buzz-agent buzz-dev-mcp)
echo "==> Building Sprig v${VERSION} for ${TARGET}"
echo " git_sha=${GIT_SHA}"
echo " binary=${BUNDLE_BIN}"
echo " commands=${COMMANDS[*]}"
echo " cargo_profile=${BUILD_PROFILE}"
if [[ "${USE_CROSS:-0}" == "1" ]] || [[ "$TARGET" != "$HOST_TARGET" ]]; then
if ! command -v cross >/dev/null 2>&1; then
echo "error: cross-compiling to $TARGET requires \`cross\` (install: cargo install cross --version 0.2.5)" >&2
exit 1
fi
BUILDER=(cross build --profile "$BUILD_PROFILE" --target "$TARGET")
BIN_DIR="target/${TARGET}/${BUILD_PROFILE}"
else
BUILDER=(cargo build --profile "$BUILD_PROFILE")
BIN_DIR="target/${BUILD_PROFILE}"
fi
if [[ "${SKIP_BUILD:-0}" == "1" ]]; then
echo " (SKIP_BUILD=1 set — expecting prebuilt ${BUNDLE_BIN} in ${BIN_DIR}/)"
else
"${BUILDER[@]}" -p "$BUNDLE_BIN"
fi
if [[ ! -f "${BIN_DIR}/${BUNDLE_BIN}" ]]; then
echo "error: ${BIN_DIR}/${BUNDLE_BIN} not found after build" >&2
exit 1
fi
mkdir -p "${DIST_DIR}"
STAGING="$(mktemp -d)"
trap 'rm -rf "${STAGING}"' EXIT
sha256_of() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
else
shasum -a 256 "$1" | awk '{print $1}'
fi
}
cp "${BIN_DIR}/${BUNDLE_BIN}" "${STAGING}/${BUNDLE_BIN}"
chmod 0755 "${STAGING}/${BUNDLE_BIN}"
if command -v strip >/dev/null 2>&1; then
strip "${STAGING}/${BUNDLE_BIN}" 2>/dev/null || true
fi
MANIFEST_ENTRIES=()
ALL_NAMES=("${BUNDLE_BIN}" "${COMMANDS[@]}")
for bin in "${ALL_NAMES[@]}"; do
if [[ "$bin" != "$BUNDLE_BIN" ]]; then
ln -s "${BUNDLE_BIN}" "${STAGING}/${bin}"
fi
sha="$(sha256_of "${STAGING}/${bin}")"
size="$(wc -c < "${STAGING}/${bin}" | tr -d ' ')"
MANIFEST_ENTRIES+=("{\"name\":\"${bin}\",\"sha256\":\"${sha}\",\"size\":${size}}")
done
ENTRIES_JSON="$(IFS=,; echo "${MANIFEST_ENTRIES[*]}")"
cat > "${STAGING}/sprig.json" <<JSON
{
"name": "sprig",
"version": "${VERSION}",
"git_sha": "${GIT_SHA}",
"target": "${TARGET}",
"binaries": [${ENTRIES_JSON}]
}
JSON
cat > "${STAGING}/README.md" <<'README'
# Sprig
Sprig is the all-in-one Buzz agent binary for deploy-anywhere environments.
It exposes the ACP harness, ACP agent, and developer MCP command names as symlinks
to one multicall binary so shared Rust runtime/TLS code is stored only once.
Commands:
- `sprig` — prints usage/version. Invoke a personality by one of the links below.
- `buzz-acp` — ACP harness and `git-credential-nostr` / `git-sign-nostr`
multicall entrypoint that bridges Buzz channel events to an
ACP-compliant agent over stdio.
- `buzz-agent` — ACP-compliant agent (spawns MCP servers, calls LLMs).
- `buzz-dev-mcp` — Developer MCP server (shell, str_replace, todo) and
multicall entrypoint for `rg`, `tree`, and `buzz`.
See `sprig.json` for SHA-256s, sizes, target, and source git SHA.
## Install
```bash
tar -xzf sprig-*.tar.gz -C /opt/sprig
export PATH="/opt/sprig:$PATH"
```
## Configure
```bash
# Agent provider
export BUZZ_AGENT_PROVIDER=anthropic # or openai
export ANTHROPIC_API_KEY=sk-...
export ANTHROPIC_MODEL=claude-sonnet-4-20250514
# Nostr identity (shared by buzz-acp, git auth, signing, and buzz CLI)
export NOSTR_PRIVATE_KEY=nsec1...
export BUZZ_PRIVATE_KEY="$NOSTR_PRIVATE_KEY"
export BUZZ_RELAY_URL=https://your-relay.example.com
```
README
ARCHIVE_BASENAME="${ARCHIVE_BASENAME:-sprig-${VERSION}-${TARGET}}"
ARCHIVE_NAME="${ARCHIVE_BASENAME}.tar.gz"
ARCHIVE_PATH="${DIST_DIR}/${ARCHIVE_NAME}"
tar \
--sort=name \
--owner=0 --group=0 --numeric-owner \
-czf "${ARCHIVE_PATH}" \
-C "${STAGING}" \
. 2>/dev/null || \
tar -czf "${ARCHIVE_PATH}" -C "${STAGING}" .
sha256_of "${ARCHIVE_PATH}" > "${ARCHIVE_PATH}.sha256"
echo "$(cat "${ARCHIVE_PATH}.sha256") ${ARCHIVE_NAME}" > "${ARCHIVE_PATH}.sha256"
echo ""
echo "==> Built: ${ARCHIVE_PATH}"
ls -lh "${ARCHIVE_PATH}" "${ARCHIVE_PATH}.sha256"
echo ""
echo "==> sprig.json:"
sed 's/^/ /' "${STAGING}/sprig.json"