diff --git a/.github/workflows/package-validation.yml b/.github/workflows/package-validation.yml index 3d95a7833..50ad77145 100644 --- a/.github/workflows/package-validation.yml +++ b/.github/workflows/package-validation.yml @@ -102,8 +102,10 @@ jobs: echo "=== Validating packages ===" ERRORS=0 - # Check dist files exist (skip non-Node package directories) - SKIP_PACKAGES="build-plans brand" + # Check dist files exist (skip non-Node package directories). + # broker-* packages ship a Rust-built binary in bin/, not a JS + # dist — they live under packages/ only for workspace linkage. + SKIP_PACKAGES="build-plans brand broker-darwin-arm64 broker-darwin-x64 broker-linux-arm64 broker-linux-x64 broker-win32-x64" for pkg_dir in packages/*/; do pkg_name=$(basename "$pkg_dir") if [ ! -f "$pkg_dir/package.json" ]; then diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bba09c96d..0a84c6c8f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -82,16 +82,29 @@ jobs: include: - os: macos-latest target: aarch64-apple-darwin + broker_pkg: broker-darwin-arm64 binary_name: agent-relay-broker-darwin-arm64 + binary_file: agent-relay-broker - os: macos-latest target: x86_64-apple-darwin + broker_pkg: broker-darwin-x64 binary_name: agent-relay-broker-darwin-x64 + binary_file: agent-relay-broker - os: ubuntu-latest target: x86_64-unknown-linux-musl + broker_pkg: broker-linux-x64 binary_name: agent-relay-broker-linux-x64 + binary_file: agent-relay-broker - os: ubuntu-latest target: aarch64-unknown-linux-musl + broker_pkg: broker-linux-arm64 binary_name: agent-relay-broker-linux-arm64 + binary_file: agent-relay-broker + - os: windows-latest + target: x86_64-pc-windows-msvc + broker_pkg: broker-win32-x64 + binary_name: agent-relay-broker-win32-x64.exe + binary_file: agent-relay-broker.exe steps: - name: Checkout code @@ -117,19 +130,35 @@ jobs: with: key: broker-${{ matrix.target }} - - name: Build broker binary + - name: Build broker binary (unix) + if: runner.os != 'Windows' run: | if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-musl" ]]; then RUSTFLAGS="-C target-feature=+crt-static" cross build --release --bin agent-relay-broker --target ${{ matrix.target }} else RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --bin agent-relay-broker --target ${{ matrix.target }} fi - strip target/${{ matrix.target }}/release/agent-relay-broker 2>/dev/null || true + strip target/${{ matrix.target }}/release/${{ matrix.binary_file }} 2>/dev/null || true - - name: Copy binary with platform name + - name: Build broker binary (windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $env:RUSTFLAGS = "-C target-feature=+crt-static" + cargo build --release --bin agent-relay-broker --target ${{ matrix.target }} + + - name: Copy binary with platform name (unix) + if: runner.os != 'Windows' run: | mkdir -p release-binaries - cp target/${{ matrix.target }}/release/agent-relay-broker release-binaries/${{ matrix.binary_name }} + cp target/${{ matrix.target }}/release/${{ matrix.binary_file }} release-binaries/${{ matrix.binary_name }} + + - name: Copy binary with platform name (windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path release-binaries | Out-Null + Copy-Item "target/${{ matrix.target }}/release/${{ matrix.binary_file }}" "release-binaries/${{ matrix.binary_name }}" # Ad-hoc sign macOS binaries at build time so the Python SDK wheel # doesn't have to invoke codesign at install time. @@ -417,10 +446,13 @@ jobs: const path = require('path'); const version = '$NEW_VERSION'; - // Update @agent-relay/* references across dependencies, - // devDependencies, and peerDependencies so sibling packages - // stay pinned to the same version we're about to publish. - const DEP_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies']; + // Update @agent-relay/* references across every dep section so + // sibling packages stay pinned to the same version we're about + // to publish. optionalDependencies is critical here: @agent-relay/sdk + // pins the per-platform broker packages by exact version, and + // forgetting this section leaves a freshly-published SDK pointing + // at the prior release's broker packages. + const DEP_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']; const rewriteInternalRefs = (pkg) => { for (const depType of DEP_SECTIONS) { for (const dep of Object.keys(pkg[depType] || {})) { @@ -483,10 +515,363 @@ jobs: dist/ retention-days: 1 + # End-to-end smoke test of the optional-dep broker pattern on every target + # platform, using locally-packed tarballs — no registry round-trip, no + # mocking. Each matrix leg: + # 1. Stages the platform's broker binary into its package tree and + # injects os/cpu so the package validates like the published one. + # 2. `npm pack`s the SDK (with packages/sdk/bin emptied so the SDK + # tarball cannot fall back to a bundled binary). + # 3. `npm pack`s the matching broker package. + # 4. Installs both tarballs into a scratch project. + # 5. Asserts getBrokerBinaryPath() resolves through the optional-dep + # package and returns an executable file. + # 6. Runs AgentRelayClient.spawn() end-to-end and shuts it down. + # Gates publish-broker-packages — we do not ship if any platform fails. + smoke-broker-packages: + name: Smoke ${{ matrix.platform }} + needs: [build, build-broker] + if: github.event.inputs.package == 'all' || github.event.inputs.package == 'sdk' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - platform: darwin-arm64 + os: macos-14 + broker_pkg: broker-darwin-arm64 + binary_name: agent-relay-broker-darwin-arm64 + binary_file: agent-relay-broker + pkg_os: darwin + pkg_cpu: arm64 + - platform: darwin-x64 + os: macos-13 + broker_pkg: broker-darwin-x64 + binary_name: agent-relay-broker-darwin-x64 + binary_file: agent-relay-broker + pkg_os: darwin + pkg_cpu: x64 + - platform: linux-x64 + os: ubuntu-latest + broker_pkg: broker-linux-x64 + binary_name: agent-relay-broker-linux-x64 + binary_file: agent-relay-broker + pkg_os: linux + pkg_cpu: x64 + - platform: linux-arm64 + os: ubuntu-24.04-arm + broker_pkg: broker-linux-arm64 + binary_name: agent-relay-broker-linux-arm64 + binary_file: agent-relay-broker + pkg_os: linux + pkg_cpu: arm64 + - platform: win32-x64 + os: windows-latest + broker_pkg: broker-win32-x64 + binary_name: agent-relay-broker-win32-x64.exe + binary_file: agent-relay-broker.exe + pkg_os: win32 + pkg_cpu: x64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.14.0' + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build-output + path: . + + - name: Download broker binary + uses: actions/download-artifact@v4 + with: + name: ${{ matrix.binary_name }} + path: /tmp/broker + + - name: Stage broker binary (unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + mkdir -p "packages/${{ matrix.broker_pkg }}/bin" + cp "/tmp/broker/${{ matrix.binary_name }}" "packages/${{ matrix.broker_pkg }}/bin/${{ matrix.binary_file }}" + chmod +x "packages/${{ matrix.broker_pkg }}/bin/${{ matrix.binary_file }}" + + - name: Stage broker binary (windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path "packages/${{ matrix.broker_pkg }}/bin" | Out-Null + Copy-Item "/tmp/broker/${{ matrix.binary_name }}" "packages/${{ matrix.broker_pkg }}/bin/${{ matrix.binary_file }}" + + - name: Inject os/cpu for ${{ matrix.pkg_os }}-${{ matrix.pkg_cpu }} + shell: bash + run: | + node -e " + const fs = require('fs'); + const p = 'packages/${{ matrix.broker_pkg }}/package.json'; + const pkg = JSON.parse(fs.readFileSync(p, 'utf8')); + pkg.os = ['${{ matrix.pkg_os }}']; + pkg.cpu = ['${{ matrix.pkg_cpu }}']; + fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n'); + " + + - name: Pack SDK, broker, and internal deps + shell: bash + run: | + set -euo pipefail + TARBALLS="$RUNNER_TEMP/tarballs" + mkdir -p "$TARBALLS" + echo "TARBALLS=$TARBALLS" >> "$GITHUB_ENV" + # Empty packages/sdk/bin so the SDK tarball has NO bundled broker + # binary. This forces the smoke test to exercise the optional-dep + # resolution path instead of the legacy bundled fallback. + rm -rf packages/sdk/bin + mkdir -p packages/sdk/bin + (cd packages/sdk && npm pack --ignore-scripts --pack-destination "$TARBALLS") + (cd "packages/${{ matrix.broker_pkg }}" && npm pack --ignore-scripts --pack-destination "$TARBALLS") + # The build job bumped every workspace to NEW_VERSION. The SDK's + # `dependencies` pins `@agent-relay/config` at that version, which + # is not on the registry yet at this point in the workflow — so + # pack it locally and install it alongside the SDK. + (cd packages/config && npm pack --ignore-scripts --pack-destination "$TARBALLS") + ls -lh "$TARBALLS" + + - name: Install tarballs into scratch project + shell: bash + run: | + set -euo pipefail + SCRATCH="$RUNNER_TEMP/smoke" + mkdir -p "$SCRATCH" + echo "SCRATCH=$SCRATCH" >> "$GITHUB_ENV" + cd "$SCRATCH" + npm init -y --silent >/dev/null + SDK_TGZ=$(ls "$TARBALLS"/agent-relay-sdk-*.tgz | head -n1) + BROKER_TGZ=$(ls "$TARBALLS"/agent-relay-broker-${{ matrix.platform }}-*.tgz | head -n1) + CONFIG_TGZ=$(ls "$TARBALLS"/agent-relay-config-*.tgz | head -n1) + echo "Installing $SDK_TGZ + $BROKER_TGZ + $CONFIG_TGZ" + npm install --ignore-scripts --no-audit --no-fund \ + "$SDK_TGZ" "$BROKER_TGZ" "$CONFIG_TGZ" + ls node_modules/@agent-relay/ + + - name: Resolver smoke — getBrokerBinaryPath() + shell: bash + run: | + cd "$SCRATCH" + node --input-type=module -e " + import { getBrokerBinaryPath, getOptionalDepPackageName } from '@agent-relay/sdk/broker-path'; + import { accessSync, constants } from 'node:fs'; + const expectedPkg = getOptionalDepPackageName(); + const p = getBrokerBinaryPath(); + console.log('expected pkg:', expectedPkg); + console.log('resolved:', p); + if (!p) { console.error('FAIL: resolver returned null'); process.exit(1); } + if (!p.includes('${{ matrix.broker_pkg }}')) { + console.error('FAIL: expected path through ${{ matrix.broker_pkg }}, got', p); + process.exit(1); + } + accessSync(p, constants.X_OK); + console.log('OK: resolver returned executable binary from optional-dep package'); + " + + - name: Spawn smoke — AgentRelayClient.spawn() + shell: bash + run: | + cd "$SCRATCH" + node --input-type=module -e " + import { AgentRelayClient } from '@agent-relay/sdk'; + const client = await AgentRelayClient.spawn({ + cwd: process.cwd(), + channels: ['general'], + startupTimeoutMs: 45000, + onStderr: (line) => console.error('[broker]', line), + }); + console.log('OK: AgentRelayClient.spawn() returned'); + await client.shutdown(); + console.log('OK: client.shutdown() completed'); + " || { echo 'SPAWN_FAILED'; exit 1; } + + - name: Negative smoke — optional dep missing (linux-x64 only) + if: matrix.platform == 'linux-x64' + shell: bash + run: | + set -euo pipefail + NEGATIVE="$RUNNER_TEMP/smoke-negative" + mkdir -p "$NEGATIVE" + cd "$NEGATIVE" + npm init -y --silent >/dev/null + SDK_TGZ=$(ls "$TARBALLS"/agent-relay-sdk-*.tgz | head -n1) + CONFIG_TGZ=$(ls "$TARBALLS"/agent-relay-config-*.tgz | head -n1) + # Install SDK + config (an internal required dep whose bumped + # version isn't on the registry yet) but skip the broker optional + # deps entirely. The resolver should return null and spawn() + # should throw the clear error. + npm install --ignore-scripts --no-audit --no-fund --no-optional \ + "$SDK_TGZ" "$CONFIG_TGZ" + node --input-type=module -e " + import { AgentRelayClient } from '@agent-relay/sdk'; + try { + await AgentRelayClient.spawn({ cwd: process.cwd() }); + console.error('FAIL: spawn() should have thrown'); + process.exit(1); + } catch (err) { + const msg = err && err.message ? err.message : String(err); + console.log('got error:', msg); + const expected = 'couldn\\'t find an agent-relay-broker binary for linux-x64'; + if (!msg.includes(expected)) { + console.error('FAIL: error message does not name platform/package'); + process.exit(1); + } + console.log('OK: spawn() threw the expected clear error'); + } + " + + # Publish the per-platform broker packages first. @agent-relay/sdk declares + # these as exact-version optionalDependencies, so they must exist on the + # registry at the matching version before the SDK is published — otherwise + # `npm install @agent-relay/sdk@` races the registry for the broker + # package at the same version. + publish-broker-packages: + name: Publish ${{ matrix.broker_pkg }} + needs: [build, build-broker, smoke-broker-packages] + runs-on: ubuntu-latest + if: github.event.inputs.package == 'all' || github.event.inputs.package == 'sdk' + strategy: + fail-fast: false + max-parallel: 5 + matrix: + include: + - broker_pkg: broker-darwin-arm64 + binary_name: agent-relay-broker-darwin-arm64 + binary_file: agent-relay-broker + os: darwin + cpu: arm64 + - broker_pkg: broker-darwin-x64 + binary_name: agent-relay-broker-darwin-x64 + binary_file: agent-relay-broker + os: darwin + cpu: x64 + - broker_pkg: broker-linux-arm64 + binary_name: agent-relay-broker-linux-arm64 + binary_file: agent-relay-broker + os: linux + cpu: arm64 + - broker_pkg: broker-linux-x64 + binary_name: agent-relay-broker-linux-x64 + binary_file: agent-relay-broker + os: linux + cpu: x64 + - broker_pkg: broker-win32-x64 + binary_name: agent-relay-broker-win32-x64.exe + binary_file: agent-relay-broker.exe + os: win32 + cpu: x64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.14.0' + registry-url: 'https://registry.npmjs.org' + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build-output + path: . + + - name: Download broker binary + uses: actions/download-artifact@v4 + with: + name: ${{ matrix.binary_name }} + path: /tmp/broker + + - name: Stage broker binary into package tree + run: | + set -euo pipefail + mkdir -p packages/${{ matrix.broker_pkg }}/bin + cp "/tmp/broker/${{ matrix.binary_name }}" "packages/${{ matrix.broker_pkg }}/bin/${{ matrix.binary_file }}" + chmod +x "packages/${{ matrix.broker_pkg }}/bin/${{ matrix.binary_file }}" + ls -lh packages/${{ matrix.broker_pkg }}/bin/ + + # os/cpu constraints are injected at publish time. Keeping them out of + # the committed package.json lets these packages live as plain + # workspaces during development — otherwise npm install trips + # EBADPLATFORM on the machines that don't match every platform. + - name: Inject os/cpu for ${{ matrix.os }}-${{ matrix.cpu }} + run: | + node -e " + const fs = require('fs'); + const p = 'packages/${{ matrix.broker_pkg }}/package.json'; + const pkg = JSON.parse(fs.readFileSync(p, 'utf8')); + pkg.os = ['${{ matrix.os }}']; + pkg.cpu = ['${{ matrix.cpu }}']; + fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n'); + console.log('Staged', pkg.name, 'for', pkg.os, pkg.cpu); + " + + - name: Update npm for OIDC support + run: npm install -g npm@latest + + - name: Dry run check + if: github.event.inputs.dry_run == 'true' + working-directory: packages/${{ matrix.broker_pkg }} + run: | + echo "Dry run - would publish @agent-relay/${{ matrix.broker_pkg }}" + npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts + + # Retry up to 3 times — npm registry occasionally flakes and we cannot + # reuse the version on rerun, so transient failures must be absorbed + # here rather than failing the whole release. + - name: Publish to NPM (attempt 1) + id: publish_1 + if: github.event.inputs.dry_run != 'true' + continue-on-error: true + working-directory: packages/${{ matrix.broker_pkg }} + run: npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts + + - name: Wait before retry + if: github.event.inputs.dry_run != 'true' && steps.publish_1.outcome == 'failure' + run: sleep 30 + + - name: Publish to NPM (attempt 2) + id: publish_2 + if: github.event.inputs.dry_run != 'true' && steps.publish_1.outcome == 'failure' + continue-on-error: true + working-directory: packages/${{ matrix.broker_pkg }} + run: npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts + + - name: Wait before retry + if: github.event.inputs.dry_run != 'true' && steps.publish_2.outcome == 'failure' + run: sleep 60 + + - name: Publish to NPM (attempt 3) + id: publish_3 + if: github.event.inputs.dry_run != 'true' && steps.publish_1.outcome == 'failure' && steps.publish_2.outcome == 'failure' + working-directory: packages/${{ matrix.broker_pkg }} + run: npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts + + - name: Fail if all publish attempts failed + if: >- + github.event.inputs.dry_run != 'true' && + steps.publish_1.outcome == 'failure' && + steps.publish_2.outcome == 'failure' && + steps.publish_3.outcome == 'failure' + run: exit 1 + # Publish all packages in parallel (npm publish doesn't need deps published first) publish-packages: name: Publish ${{ matrix.package }} - needs: [build, build-broker] + needs: [build, build-broker, publish-broker-packages] runs-on: ubuntu-latest if: github.event.inputs.package == 'all' strategy: @@ -528,6 +913,10 @@ jobs: name: build-output path: . + # Keep bundling all platform broker binaries into the SDK tarball for + # one release cycle. The SDK prefers the optional-dep package but falls + # back to this bundled copy so mixed-version installs keep working + # during the migration. Delete this step in the next major. - name: Download broker binaries (SDK only) if: matrix.package == 'sdk' uses: actions/download-artifact@v4 @@ -539,7 +928,7 @@ jobs: - name: Make broker binaries executable (SDK only) if: matrix.package == 'sdk' run: | - chmod +x packages/sdk/bin/agent-relay-broker-* + chmod +x packages/sdk/bin/agent-relay-broker-* || true # Remove stale generic binary — SDK resolves platform-specific names at runtime rm -f packages/sdk/bin/agent-relay-broker @@ -603,7 +992,7 @@ jobs: # Publish SDK only (when selected) publish-sdk-only: name: Publish SDK to NPM - needs: [build, build-broker] + needs: [build, build-broker, publish-broker-packages] runs-on: ubuntu-latest if: github.event.inputs.package == 'sdk' @@ -623,6 +1012,10 @@ jobs: name: build-output path: . + # Bundled broker binaries in packages/sdk/bin/ are kept for one release + # cycle as a fallback. New installs resolve the binary via the + # per-platform @agent-relay/broker-* optional deps published by the + # publish-broker-packages job above. - name: Download broker binaries uses: actions/download-artifact@v4 with: @@ -632,7 +1025,7 @@ jobs: - name: Make broker binaries executable run: | - chmod +x packages/sdk/bin/agent-relay-broker-* + chmod +x packages/sdk/bin/agent-relay-broker-* || true # Remove stale generic binary — SDK resolves platform-specific names at runtime rm -f packages/sdk/bin/agent-relay-broker @@ -1001,17 +1394,10 @@ jobs: name: build-output path: . - - name: Download broker binaries - if: github.event.inputs.package != 'cli-prerelease' - uses: actions/download-artifact@v4 - with: - pattern: agent-relay-broker-* - path: bin/ - merge-multiple: true - - - name: Make binaries executable - if: github.event.inputs.package != 'cli-prerelease' - run: chmod +x bin/agent-relay-broker-* + # NOTE: the root `agent-relay` CLI no longer bundles broker binaries in + # its own tarball. Brokers ship as `@agent-relay/broker--` + # optional-deps of `@agent-relay/sdk` (which is itself bundled here via + # bundledDependencies), so users still get a broker on install. - name: Update npm for OIDC support run: npm install -g npm@latest @@ -1432,6 +1818,7 @@ jobs: - `agent-relay-broker-linux-arm64` - Linux ARM64 - `agent-relay-broker-darwin-x64` - macOS Intel - `agent-relay-broker-darwin-arm64` - macOS Apple Silicon + - `agent-relay-broker-win32-x64.exe` - Windows x86_64 ### relay-acp binaries (Zed editor integration) ACP bridge for Zed editor: @@ -1504,6 +1891,27 @@ jobs: with: version: ${{ needs.build.outputs.new_version }} + # Post-publish cross-platform verification of @agent-relay/sdk and its + # per-platform broker optional deps. Installs from the registry on every + # target (macOS arm64/x64, Linux x64/arm64, Windows x64) and runs spawn + # end-to-end. Catches registry-round-trip failures that smoke-broker-packages + # can't see (missing/wrong os/cpu on published manifests, CDN propagation). + verify-publish-sdk: + name: Verify Published SDK (Cross-Platform) + # publish-packages and publish-sdk-only are mutually exclusive: the first + # runs on package='all', the second on package='sdk'. Depend on both and + # accept a success from either so this gate fires for both release flows. + needs: [build, publish-broker-packages, publish-packages, publish-sdk-only] + if: | + always() && + github.event.inputs.dry_run != 'true' && + (github.event.inputs.package == 'all' || github.event.inputs.package == 'sdk') && + needs.publish-broker-packages.result == 'success' && + (needs.publish-packages.result == 'success' || needs.publish-sdk-only.result == 'success') + uses: ./.github/workflows/verify-publish-sdk.yml + with: + version: ${{ needs.build.outputs.new_version }} + summary: name: Summary needs: @@ -1517,11 +1925,14 @@ jobs: verify-standalone-macos, verify-acp-linux, verify-acp-macos, + smoke-broker-packages, + publish-broker-packages, publish-packages, publish-brand-only, publish-sdk-py, publish-main, verify-publish, + verify-publish-sdk, ] runs-on: ubuntu-latest if: always() @@ -1558,11 +1969,14 @@ jobs: echo "| Verify Standalone (macOS) | ${{ needs.verify-standalone-macos.result == 'success' && '✅' || (needs.verify-standalone-macos.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.verify-standalone-macos.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Verify relay-acp (Linux) | ${{ needs.verify-acp-linux.result == 'success' && '✅' || (needs.verify-acp-linux.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.verify-acp-linux.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Verify relay-acp (macOS) | ${{ needs.verify-acp-macos.result == 'success' && '✅' || (needs.verify-acp-macos.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.verify-acp-macos.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Smoke Broker Packages | ${{ needs.smoke-broker-packages.result == 'success' && '✅' || (needs.smoke-broker-packages.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.smoke-broker-packages.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Publish Broker Packages | ${{ needs.publish-broker-packages.result == 'success' && '✅' || (needs.publish-broker-packages.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-broker-packages.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Publish Packages | ${{ needs.publish-packages.result == 'success' && '✅' || (needs.publish-packages.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-packages.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Publish Brand | ${{ needs.publish-brand-only.result == 'success' && '✅' || (needs.publish-brand-only.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-brand-only.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Publish Python SDK | ${{ needs.publish-sdk-py.result == 'success' && '✅' || (needs.publish-sdk-py.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-sdk-py.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Publish Main | ${{ needs.publish-main.result == 'success' && '✅' || (needs.publish-main.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-main.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Post-Publish Verify | ${{ needs.verify-publish.result == 'success' && '✅' || (needs.verify-publish.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.verify-publish.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Post-Publish Verify SDK (cross-platform) | ${{ needs.verify-publish-sdk.result == 'success' && '✅' || (needs.verify-publish-sdk.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.verify-publish-sdk.result }} |" >> $GITHUB_STEP_SUMMARY if [ "$IS_PRERELEASE" = "true" ]; then echo "" >> $GITHUB_STEP_SUMMARY echo "### Next Steps" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/verify-publish-sdk.yml b/.github/workflows/verify-publish-sdk.yml new file mode 100644 index 000000000..625e1a738 --- /dev/null +++ b/.github/workflows/verify-publish-sdk.yml @@ -0,0 +1,187 @@ +name: Verify Published SDK (Cross-Platform) + +# Clean-room verification that @agent-relay/sdk — freshly installed from the +# public registry on each target OS/arch — pulls in the correct +# @agent-relay/broker-- optional dep and actually spawns a +# broker end-to-end. This is the last line of defense for the optional-dep +# pattern: pre-publish the smoke-broker-packages job exercises the logic via +# locally-packed tarballs, but it cannot catch registry round-trip bugs +# (wrong os/cpu manifest, selection logic, CDN propagation). +# +# Triggered: +# - Automatically after the publish workflow completes (via workflow_call) +# - Manually via workflow_dispatch (useful for re-verifying an existing +# version, e.g. after a registry flake or to check propagation). +# +# Not wired to pull_request: the workflow installs from the public registry, +# which means it can only verify versions that have already shipped. For +# pre-publish validation of this logic, use the smoke-broker-packages job in +# publish.yml — it runs the same assertions against locally-packed tarballs. + +on: + workflow_dispatch: + inputs: + version: + description: 'SDK version to verify (default: latest)' + required: false + type: string + default: 'latest' + workflow_call: + inputs: + version: + description: 'SDK version to verify' + required: false + type: string + default: 'latest' + +env: + AGENT_RELAY_TELEMETRY_DISABLED: 1 + +jobs: + verify-sdk: + name: Verify @agent-relay/sdk (${{ matrix.platform }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - platform: darwin-arm64 + os: macos-14 + expected_pkg: '@agent-relay/broker-darwin-arm64' + - platform: darwin-x64 + os: macos-13 + expected_pkg: '@agent-relay/broker-darwin-x64' + - platform: linux-x64 + os: ubuntu-latest + expected_pkg: '@agent-relay/broker-linux-x64' + - platform: linux-arm64 + os: ubuntu-24.04-arm + expected_pkg: '@agent-relay/broker-linux-arm64' + - platform: win32-x64 + os: windows-latest + expected_pkg: '@agent-relay/broker-win32-x64' + + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.14.0' + + - name: Resolve version spec + id: spec + shell: bash + run: | + VERSION="${{ inputs.version }}" + if [ -z "$VERSION" ]; then + VERSION="latest" + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "spec=@agent-relay/sdk@$VERSION" >> "$GITHUB_OUTPUT" + echo "Using spec: @agent-relay/sdk@$VERSION" + + # Registry/CDN propagation can lag the publish by up to a minute. Retry + # with exponential backoff so the first run after publish doesn't miss. + - name: Wait for package on registry + shell: bash + run: | + set -euo pipefail + SPEC="${{ steps.spec.outputs.spec }}" + VERSION="${{ steps.spec.outputs.version }}" + if [ "$VERSION" = "latest" ]; then + echo "version=latest — no wait needed" + exit 0 + fi + for i in 1 2 3 4 5 6; do + if npm view "$SPEC" version >/dev/null 2>&1; then + echo "registry has $SPEC" + exit 0 + fi + WAIT=$((2 ** i)) + echo "attempt $i: registry not ready, sleeping ${WAIT}s" + sleep "$WAIT" + done + echo "FAIL: registry never surfaced $SPEC" + exit 1 + + - name: Install @agent-relay/sdk into scratch project + shell: bash + run: | + set -euo pipefail + SCRATCH="$RUNNER_TEMP/verify-sdk" + mkdir -p "$SCRATCH" + echo "SCRATCH=$SCRATCH" >> "$GITHUB_ENV" + cd "$SCRATCH" + npm init -y --silent >/dev/null + echo "Installing ${{ steps.spec.outputs.spec }}" + npm install --no-audit --no-fund "${{ steps.spec.outputs.spec }}" + echo "" + echo "=== installed @agent-relay/ packages ===" + ls node_modules/@agent-relay/ + + - name: Verify only the matching broker package was installed + shell: bash + run: | + set -euo pipefail + cd "$SCRATCH" + EXPECTED="${{ matrix.expected_pkg }}" + if [ ! -d "node_modules/$EXPECTED" ]; then + echo "FAIL: expected optional dep $EXPECTED was not installed" + echo "installed @agent-relay/ packages:" + ls node_modules/@agent-relay/ 2>&1 || true + exit 1 + fi + echo "OK: $EXPECTED present in node_modules" + + # npm should skip every sibling whose os/cpu doesn't match this + # runner. Catch a missing os/cpu constraint by asserting the + # unmatched packages were NOT installed. + for pkg in \ + @agent-relay/broker-darwin-arm64 \ + @agent-relay/broker-darwin-x64 \ + @agent-relay/broker-linux-arm64 \ + @agent-relay/broker-linux-x64 \ + @agent-relay/broker-win32-x64; do + if [ "$pkg" != "$EXPECTED" ] && [ -d "node_modules/$pkg" ]; then + echo "FAIL: sibling optional dep $pkg was installed on ${{ matrix.platform }}" + echo "npm should skip it based on os/cpu — published manifest may be missing constraints" + exit 1 + fi + done + echo "OK: only ${{ matrix.expected_pkg }} was installed; siblings correctly skipped" + + - name: Resolver smoke — getBrokerBinaryPath() + shell: bash + run: | + cd "$SCRATCH" + node --input-type=module -e " + import { getBrokerBinaryPath, getOptionalDepPackageName } from '@agent-relay/sdk/broker-path'; + import { accessSync, constants } from 'node:fs'; + const expected = getOptionalDepPackageName(); + const p = getBrokerBinaryPath(); + console.log('expected package:', expected); + console.log('resolved:', p); + if (!p) { console.error('FAIL: resolver returned null'); process.exit(1); } + if (!p.includes(expected)) { + console.error('FAIL: resolution did not go through the optional-dep package; got', p); + process.exit(1); + } + accessSync(p, constants.X_OK); + console.log('OK: resolver returned executable binary from optional-dep package'); + " + + - name: Spawn smoke — AgentRelayClient.spawn() + shell: bash + run: | + cd "$SCRATCH" + node --input-type=module -e " + import { AgentRelayClient } from '@agent-relay/sdk'; + const client = await AgentRelayClient.spawn({ + cwd: process.cwd(), + channels: ['general'], + startupTimeoutMs: 45000, + onStderr: (line) => console.error('[broker]', line), + }); + console.log('OK: AgentRelayClient.spawn() returned'); + await client.shutdown(); + console.log('OK: client.shutdown() completed'); + " diff --git a/package-lock.json b/package-lock.json index b1ad4ffd6..43ed274e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,6 +114,26 @@ "resolved": "packages/brand", "link": true }, + "node_modules/@agent-relay/broker-darwin-arm64": { + "resolved": "packages/broker-darwin-arm64", + "link": true + }, + "node_modules/@agent-relay/broker-darwin-x64": { + "resolved": "packages/broker-darwin-x64", + "link": true + }, + "node_modules/@agent-relay/broker-linux-arm64": { + "resolved": "packages/broker-linux-arm64", + "link": true + }, + "node_modules/@agent-relay/broker-linux-x64": { + "resolved": "packages/broker-linux-x64", + "link": true + }, + "node_modules/@agent-relay/broker-win32-x64": { + "resolved": "packages/broker-win32-x64", + "link": true + }, "node_modules/@agent-relay/browser-primitive": { "resolved": "packages/browser-primitive", "link": true @@ -1261,7 +1281,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -15430,6 +15449,31 @@ "name": "@agent-relay/brand", "version": "5.0.0" }, + "packages/broker-darwin-arm64": { + "name": "@agent-relay/broker-darwin-arm64", + "version": "5.0.0", + "license": "MIT" + }, + "packages/broker-darwin-x64": { + "name": "@agent-relay/broker-darwin-x64", + "version": "5.0.0", + "license": "MIT" + }, + "packages/broker-linux-arm64": { + "name": "@agent-relay/broker-linux-arm64", + "version": "5.0.0", + "license": "MIT" + }, + "packages/broker-linux-x64": { + "name": "@agent-relay/broker-linux-x64", + "version": "5.0.0", + "license": "MIT" + }, + "packages/broker-win32-x64": { + "name": "@agent-relay/broker-win32-x64", + "version": "5.0.0", + "license": "MIT" + }, "packages/browser-primitive": { "name": "@agent-relay/browser-primitive", "version": "5.0.0", @@ -15446,15 +15490,6 @@ "vitest": "^3.2.4" } }, - "packages/browser-primitive/node_modules/@agent-relay/config": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@agent-relay/config/-/config-4.0.9.tgz", - "integrity": "sha512-bSYU1q/mCpbNScGCjo32GOi22+TIkVy96vzaLwXkyZrv+hsVlIGtIlpZe3GCYRK+hvKizKTsXc9V0PHaNR8d8Q==", - "dependencies": { - "zod": "^3.23.8", - "zod-to-json-schema": "^3.23.1" - } - }, "packages/cloud": { "name": "@agent-relay/cloud", "version": "5.0.0", @@ -15515,15 +15550,6 @@ "vitest": "^3.2.4" } }, - "packages/github-primitive/node_modules/@agent-relay/config": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@agent-relay/config/-/config-4.0.9.tgz", - "integrity": "sha512-bSYU1q/mCpbNScGCjo32GOi22+TIkVy96vzaLwXkyZrv+hsVlIGtIlpZe3GCYRK+hvKizKTsXc9V0PHaNR8d8Q==", - "dependencies": { - "zod": "^3.23.8", - "zod-to-json-schema": "^3.23.1" - } - }, "packages/hooks": { "name": "@agent-relay/hooks", "version": "5.0.0", @@ -16349,6 +16375,13 @@ "@types/tar": "^6.1.13", "@types/ws": "^8.5.10" }, + "optionalDependencies": { + "@agent-relay/broker-darwin-arm64": "5.0.0", + "@agent-relay/broker-darwin-x64": "5.0.0", + "@agent-relay/broker-linux-arm64": "5.0.0", + "@agent-relay/broker-linux-x64": "5.0.0", + "@agent-relay/broker-win32-x64": "5.0.0" + }, "peerDependencies": { "@agent-relay/credential-proxy": "5.0.0", "@anthropic-ai/claude-agent-sdk": ">=0.1.0", diff --git a/package.json b/package.json index 2bff9eff2..df0efa0e9 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "types": "dist/src/index.d.ts", "files": [ "dist", - "bin", "install.sh", "scripts/postinstall.js", "scripts/build-cjs.mjs", diff --git a/packages/broker-darwin-arm64/README.md b/packages/broker-darwin-arm64/README.md new file mode 100644 index 000000000..4b4af6ede --- /dev/null +++ b/packages/broker-darwin-arm64/README.md @@ -0,0 +1,11 @@ +# @agent-relay/broker-darwin-arm64 + +Prebuilt `agent-relay-broker` binary for **macOS (Apple Silicon)**. + +This package is installed automatically as an optional dependency of +[`@agent-relay/sdk`](https://www.npmjs.com/package/@agent-relay/sdk). You do +not need to depend on it directly. The SDK resolves the correct platform +binary at runtime via `require.resolve`. + +See the [agent-relay repository](https://github.com/AgentWorkforce/relay) for +source and build tooling. diff --git a/packages/broker-darwin-arm64/bin/.gitkeep b/packages/broker-darwin-arm64/bin/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/broker-darwin-arm64/package.json b/packages/broker-darwin-arm64/package.json new file mode 100644 index 000000000..a3fa05b6a --- /dev/null +++ b/packages/broker-darwin-arm64/package.json @@ -0,0 +1,17 @@ +{ + "name": "@agent-relay/broker-darwin-arm64", + "version": "5.0.0", + "description": "agent-relay-broker binary for darwin arm64. Installed automatically as an optional dependency of @agent-relay/sdk.", + "files": [ + "bin" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/AgentWorkforce/relay.git", + "directory": "packages/broker-darwin-arm64" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/broker-darwin-x64/README.md b/packages/broker-darwin-x64/README.md new file mode 100644 index 000000000..f32101af1 --- /dev/null +++ b/packages/broker-darwin-x64/README.md @@ -0,0 +1,11 @@ +# @agent-relay/broker-darwin-x64 + +Prebuilt `agent-relay-broker` binary for **macOS (Intel)**. + +This package is installed automatically as an optional dependency of +[`@agent-relay/sdk`](https://www.npmjs.com/package/@agent-relay/sdk). You do +not need to depend on it directly. The SDK resolves the correct platform +binary at runtime via `require.resolve`. + +See the [agent-relay repository](https://github.com/AgentWorkforce/relay) for +source and build tooling. diff --git a/packages/broker-darwin-x64/bin/.gitkeep b/packages/broker-darwin-x64/bin/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/broker-darwin-x64/package.json b/packages/broker-darwin-x64/package.json new file mode 100644 index 000000000..906544af0 --- /dev/null +++ b/packages/broker-darwin-x64/package.json @@ -0,0 +1,17 @@ +{ + "name": "@agent-relay/broker-darwin-x64", + "version": "5.0.0", + "description": "agent-relay-broker binary for darwin x64. Installed automatically as an optional dependency of @agent-relay/sdk.", + "files": [ + "bin" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/AgentWorkforce/relay.git", + "directory": "packages/broker-darwin-x64" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/broker-linux-arm64/README.md b/packages/broker-linux-arm64/README.md new file mode 100644 index 000000000..85ae8fc85 --- /dev/null +++ b/packages/broker-linux-arm64/README.md @@ -0,0 +1,12 @@ +# @agent-relay/broker-linux-arm64 + +Prebuilt `agent-relay-broker` binary for **Linux ARM64**. The broker is +compiled with `musl` static linking so it works on both glibc and musl hosts. + +This package is installed automatically as an optional dependency of +[`@agent-relay/sdk`](https://www.npmjs.com/package/@agent-relay/sdk). You do +not need to depend on it directly. The SDK resolves the correct platform +binary at runtime via `require.resolve`. + +See the [agent-relay repository](https://github.com/AgentWorkforce/relay) for +source and build tooling. diff --git a/packages/broker-linux-arm64/bin/.gitkeep b/packages/broker-linux-arm64/bin/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/broker-linux-arm64/package.json b/packages/broker-linux-arm64/package.json new file mode 100644 index 000000000..5e3bf6065 --- /dev/null +++ b/packages/broker-linux-arm64/package.json @@ -0,0 +1,17 @@ +{ + "name": "@agent-relay/broker-linux-arm64", + "version": "5.0.0", + "description": "agent-relay-broker binary for linux arm64. Installed automatically as an optional dependency of @agent-relay/sdk.", + "files": [ + "bin" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/AgentWorkforce/relay.git", + "directory": "packages/broker-linux-arm64" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/broker-linux-x64/README.md b/packages/broker-linux-x64/README.md new file mode 100644 index 000000000..f290edb36 --- /dev/null +++ b/packages/broker-linux-x64/README.md @@ -0,0 +1,12 @@ +# @agent-relay/broker-linux-x64 + +Prebuilt `agent-relay-broker` binary for **Linux x86_64**. The broker is +compiled with `musl` static linking so it works on both glibc and musl hosts. + +This package is installed automatically as an optional dependency of +[`@agent-relay/sdk`](https://www.npmjs.com/package/@agent-relay/sdk). You do +not need to depend on it directly. The SDK resolves the correct platform +binary at runtime via `require.resolve`. + +See the [agent-relay repository](https://github.com/AgentWorkforce/relay) for +source and build tooling. diff --git a/packages/broker-linux-x64/bin/.gitkeep b/packages/broker-linux-x64/bin/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/broker-linux-x64/package.json b/packages/broker-linux-x64/package.json new file mode 100644 index 000000000..3c4a97fcf --- /dev/null +++ b/packages/broker-linux-x64/package.json @@ -0,0 +1,17 @@ +{ + "name": "@agent-relay/broker-linux-x64", + "version": "5.0.0", + "description": "agent-relay-broker binary for linux x64. Installed automatically as an optional dependency of @agent-relay/sdk.", + "files": [ + "bin" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/AgentWorkforce/relay.git", + "directory": "packages/broker-linux-x64" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/broker-win32-x64/README.md b/packages/broker-win32-x64/README.md new file mode 100644 index 000000000..3888912c4 --- /dev/null +++ b/packages/broker-win32-x64/README.md @@ -0,0 +1,11 @@ +# @agent-relay/broker-win32-x64 + +Prebuilt `agent-relay-broker.exe` binary for **Windows x86_64**. + +This package is installed automatically as an optional dependency of +[`@agent-relay/sdk`](https://www.npmjs.com/package/@agent-relay/sdk). You do +not need to depend on it directly. The SDK resolves the correct platform +binary at runtime via `require.resolve`. + +See the [agent-relay repository](https://github.com/AgentWorkforce/relay) for +source and build tooling. diff --git a/packages/broker-win32-x64/bin/.gitkeep b/packages/broker-win32-x64/bin/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/broker-win32-x64/package.json b/packages/broker-win32-x64/package.json new file mode 100644 index 000000000..3ab61c2d3 --- /dev/null +++ b/packages/broker-win32-x64/package.json @@ -0,0 +1,17 @@ +{ + "name": "@agent-relay/broker-win32-x64", + "version": "5.0.0", + "description": "agent-relay-broker binary for win32 x64. Installed automatically as an optional dependency of @agent-relay/sdk.", + "files": [ + "bin" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/AgentWorkforce/relay.git", + "directory": "packages/broker-win32-x64" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a20e9bdf4..8e3faf35b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -146,6 +146,13 @@ "tar": "^7.5.10", "yaml": "^2.7.0" }, + "optionalDependencies": { + "@agent-relay/broker-darwin-arm64": "5.0.0", + "@agent-relay/broker-darwin-x64": "5.0.0", + "@agent-relay/broker-linux-arm64": "5.0.0", + "@agent-relay/broker-linux-x64": "5.0.0", + "@agent-relay/broker-win32-x64": "5.0.0" + }, "peerDependencies": { "@agent-relay/credential-proxy": "5.0.0", "@anthropic-ai/claude-agent-sdk": ">=0.1.0", diff --git a/packages/sdk/src/__tests__/broker-path.test.ts b/packages/sdk/src/__tests__/broker-path.test.ts new file mode 100644 index 000000000..0bd99220e --- /dev/null +++ b/packages/sdk/src/__tests__/broker-path.test.ts @@ -0,0 +1,99 @@ +/** + * Tests for broker-path resolver and the formatted spawn-time error. + * + * The resolver is almost pure fs + `require.resolve`, so we stage a fake + * optional-dep package in a tmp dir and exercise the real function. + */ + +import assert from 'node:assert/strict'; +import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, test } from 'vitest'; + +import { + formatBrokerNotFoundError, + getBrokerBinaryPath, + getOptionalDepPackageName, +} from '../broker-path.js'; + +function stageOptionalDepPackage(root: string): string { + const pkgName = getOptionalDepPackageName(); + const ext = process.platform === 'win32' ? '.exe' : ''; + const pkgDir = join(root, 'node_modules', pkgName); + const binDir = join(pkgDir, 'bin'); + mkdirSync(binDir, { recursive: true }); + writeFileSync( + join(pkgDir, 'package.json'), + JSON.stringify({ name: pkgName, version: '0.0.0-test' }, null, 2), + ); + const binaryPath = join(binDir, `agent-relay-broker${ext}`); + writeFileSync(binaryPath, '#!/bin/sh\nexit 0\n'); + if (process.platform !== 'win32') { + chmodSync(binaryPath, 0o755); + } + return binaryPath; +} + +describe('broker-path', () => { + let originalCwd: string; + let originalEnv: NodeJS.ProcessEnv; + let tmp: string; + + beforeEach(() => { + originalCwd = process.cwd(); + originalEnv = { ...process.env }; + tmp = mkdtempSync(join(tmpdir(), 'broker-path-')); + delete process.env.BROKER_BINARY_PATH; + delete process.env.AGENT_RELAY_BIN; + }); + + afterEach(() => { + process.chdir(originalCwd); + process.env = originalEnv; + rmSync(tmp, { recursive: true, force: true }); + }); + + test('getOptionalDepPackageName returns @agent-relay/broker--', () => { + assert.equal( + getOptionalDepPackageName('darwin', 'arm64'), + '@agent-relay/broker-darwin-arm64', + ); + assert.equal(getOptionalDepPackageName('linux', 'x64'), '@agent-relay/broker-linux-x64'); + assert.equal(getOptionalDepPackageName('win32', 'x64'), '@agent-relay/broker-win32-x64'); + }); + + test('formatBrokerNotFoundError names the platform and the optional-dep package', () => { + const msg = formatBrokerNotFoundError(); + assert.match(msg, new RegExp(`${process.platform}-${process.arch}`)); + assert.match(msg, new RegExp(getOptionalDepPackageName())); + assert.match(msg, /--include=optional/); + assert.match(msg, /BROKER_BINARY_PATH/); + }); + + test('BROKER_BINARY_PATH env override short-circuits the resolver', () => { + const fakeBinary = join(tmp, 'agent-relay-broker'); + writeFileSync(fakeBinary, '#!/bin/sh\nexit 0\n'); + if (process.platform !== 'win32') { + chmodSync(fakeBinary, 0o755); + } + + process.env.BROKER_BINARY_PATH = fakeBinary; + assert.equal(getBrokerBinaryPath(), fakeBinary); + }); + + // Optional-dep end-to-end resolution is exercised by the cross-platform + // CI smoke job (.github/workflows/publish.yml → smoke-broker-packages), + // which packs real tarballs into a scratch project and confirms + // getBrokerBinaryPath() goes through node_modules/@agent-relay/broker-*. + // Unit-testing it inside the dev tree is fragile because source-checkout + // and ancestor-bin resolution win before the tmp cwd is consulted. + // Verify at least that the resolver reuses stageOptionalDepPackage to + // land an executable where the optional-dep path would find it. + test('stageOptionalDepPackage produces an executable in the expected layout', () => { + const staged = stageOptionalDepPackage(tmp); + assert.ok(existsSync(staged)); + assert.match(staged, new RegExp(getOptionalDepPackageName().replace('/', '[\\\\/]'))); + assert.match(staged, /[\\/]bin[\\/]/); + }); +}); diff --git a/packages/sdk/src/broker-path.ts b/packages/sdk/src/broker-path.ts index e9c6d58a2..2ce23ccf2 100644 --- a/packages/sdk/src/broker-path.ts +++ b/packages/sdk/src/broker-path.ts @@ -14,6 +14,13 @@ import { fileURLToPath } from 'node:url'; const BROKER_NAME = 'agent-relay-broker'; +export function getOptionalDepPackageName( + platform: NodeJS.Platform = process.platform, + arch: string = process.arch +): string { + return `@agent-relay/broker-${platform}-${arch}`; +} + function addUniquePath(paths: string[], candidate: string | null | undefined): void { if (!candidate || paths.includes(candidate)) { return; @@ -60,6 +67,59 @@ function getCurrentModuleReference(): string | null { return getImportMetaUrl(); } +function getResolutionReferences(): string[] { + const refs: string[] = []; + addUniquePath(refs, getCurrentModuleReference()); + + // Also try the entry script so CLI consumers and bundled installs + // (where the SDK lives under the consuming package's node_modules) can + // still find the optional-dep package. + if (process.argv[1]) { + addUniquePath(refs, process.argv[1]); + } + + // Fall back to the cwd's package.json as a final resolution anchor. + // `createRequire` only needs a file path that sits inside a project root + // — it doesn't have to exist. This catches setups where the SDK's module + // path and the entry script both sit outside the consumer's node_modules + // tree (e.g. a globally-installed SDK importing per-project optional deps, + // some vite/webpack bundling configurations, repl experimentation), but + // the consumer's cwd is inside their own project. We only use this + // reference to run require.resolve; no file I/O is triggered on misses. + addUniquePath(refs, join(process.cwd(), 'package.json')); + + return refs; +} + +function requireResolveFromRefs(specifier: string): string | null { + for (const ref of getResolutionReferences()) { + try { + return createRequire(ref).resolve(specifier); + } catch { + // Try the next reference. + } + } + return null; +} + +/** + * Resolve the broker binary via the platform-specific optional-dependency + * package (`@agent-relay/broker--`). Returns null when the + * optional dep is not installed (expected when users install with + * --no-optional / --omit=optional / --include= omits optional, or when the + * broker hasn't been published for their platform yet). + */ +function getOptionalDepBinaryPath(ext: string): string | null { + const pkgName = getOptionalDepPackageName(); + const binaryFile = `${BROKER_NAME}${ext}`; + + const pkgJsonPath = requireResolveFromRefs(`${pkgName}/package.json`); + if (!pkgJsonPath) return null; + + const binPath = join(dirname(pkgJsonPath), 'bin', binaryFile); + return existsSync(binPath) ? binPath : null; +} + function getSdkBinDirs(): string[] { const binDirs: string[] = []; @@ -90,10 +150,11 @@ function getSdkBinDirs(): string[] { return binDirs; } -// The `agent-relay` npm tarball ships platform-specific brokers at its -// top-level `bin/` (not inside `packages/sdk/bin/`). Walk up from the SDK -// module looking for any ancestor with a `bin/` directory so we can find -// the binary without depending on postinstall to copy it. +// The `agent-relay` npm tarball historically shipped platform-specific +// brokers at its top-level `bin/`. Walk up from the SDK module looking for +// any ancestor with a `bin/` directory. This fallback is retained for one +// release cycle while downstream installs migrate to the optional-dep +// package; delete it in the next major. function getAncestorBinDirs(): string[] { const binDirs: string[] = []; const start = getCurrentModuleDir(); @@ -162,13 +223,16 @@ function getSourceCheckoutBinaryPaths(ext: string, binDirs: string[]): string[] * * Search order: * 1. Explicit env override (BROKER_BINARY_PATH / AGENT_RELAY_BIN) - * 2. Local Cargo build when the SDK is loaded from an agent-relay source checkout - * 3. SDK's bin/ directory (resolved via CJS globals, createRequire, or import.meta.url) - * 4. Platform-specific name (agent-relay-broker-{platform}-{arch}) in bin/ - * 5. Ancestor bin/ directories (the `agent-relay` tarball ships platform- - * specific broker binaries at its package-root `bin/` — finding them - * here removes the postinstall copy dependency) - * 6. Common Cargo development paths (target/release and target/debug) + * 2. Local Cargo build when the SDK is loaded from an agent-relay source + * checkout — keeps dev workflows snappy by preferring a fresh + * `target/release` binary over anything staged in bin/ + * 3. Platform-specific optional-dep package + * (`@agent-relay/broker--`) — primary production path + * 4. SDK's bin/ directory (legacy bundled binary — kept for one release + * cycle so mixed-version installs still work) + * 5. Ancestor bin/ directories (legacy, from PR #768 — kept for one + * release cycle so stale `agent-relay` tarballs still resolve) + * 6. Cargo development paths (target/release and target/debug) * 7. PATH lookup via `which` / `where` * * @returns Absolute path to the broker binary, or null if not found @@ -188,15 +252,21 @@ export function getBrokerBinaryPath(): string | null { } // 1. Prefer a local Cargo build when this SDK is being used from a source checkout. - // In development, the bundled packages/sdk/bin binary can be stale relative to - // the current Rust build in target/release. + // In development, a binary staged in packages/sdk/bin can be stale relative + // to the current Rust build in target/release. for (const developmentPath of getSourceCheckoutBinaryPaths(ext, binDirs)) { if (existsSync(developmentPath)) { return developmentPath; } } - // 2. Exact name in bin/ + // 2. Platform-specific optional-dep package — the primary production path. + const optionalDepBinary = getOptionalDepBinaryPath(ext); + if (optionalDepBinary) { + return optionalDepBinary; + } + + // 3. SDK's bin/ (legacy bundled binary — kept for one release cycle). for (const binDir of binDirs) { const exactPath = join(binDir, `${BROKER_NAME}${ext}`); if (existsSync(exactPath)) { @@ -204,7 +274,7 @@ export function getBrokerBinaryPath(): string | null { } } - // 3. Platform-specific name in bin/ + // 4. Platform-specific name in SDK's bin/ (legacy). for (const binDir of binDirs) { const platformPath = join(binDir, platformSpecific); if (existsSync(platformPath)) { @@ -212,9 +282,8 @@ export function getBrokerBinaryPath(): string | null { } } - // 4. Ancestor bin/ directories (the agent-relay tarball ships brokers at - // its package-root bin/ — exact name first for parity with bundled SDKs, - // then platform-specific name for the prebuilt-only case). + // 5. Ancestor bin/ directories (legacy from PR #768 — the `agent-relay` + // tarball historically shipped brokers at its package-root bin/). for (const binDir of ancestorBinDirs) { const exactPath = join(binDir, `${BROKER_NAME}${ext}`); if (existsSync(exactPath)) { @@ -226,14 +295,14 @@ export function getBrokerBinaryPath(): string | null { } } - // 5. Common development paths for local Cargo builds. + // 6. Common development paths for local Cargo builds. for (const developmentPath of getDevelopmentBinaryPaths(ext, binDirs)) { if (existsSync(developmentPath)) { return developmentPath; } } - // 6. PATH lookup + // 7. PATH lookup try { const cmd = process.platform === 'win32' ? 'where' : 'which'; const result = execFileSync(cmd, [BROKER_NAME], { @@ -249,3 +318,20 @@ export function getBrokerBinaryPath(): string | null { return null; } + +/** + * Human-readable error message explaining that the optional-dep broker + * package for the current platform/arch isn't installed. Used by the SDK + * at broker-spawn time so users get a clear message instead of an + * inscrutable `spawn agent-relay-broker ENOENT`. + */ +export function formatBrokerNotFoundError(): string { + const pkgName = getOptionalDepPackageName(); + return ( + `@agent-relay/sdk couldn't find an agent-relay-broker binary for ` + + `${process.platform}-${process.arch}. The optional dependency ` + + `${pkgName} is expected to be installed alongside @agent-relay/sdk. ` + + `Try reinstalling with --include=optional, or set BROKER_BINARY_PATH ` + + `to point at a broker binary you've downloaded manually.` + ); +} diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index f0ce3dcf5..cbeac0f5d 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -15,7 +15,7 @@ import { randomBytes } from 'node:crypto'; import { readFileSync, existsSync } from 'node:fs'; import path from 'node:path'; import { BrokerTransport, AgentRelayProtocolError } from './transport.js'; -import { getBrokerBinaryPath } from './broker-path.js'; +import { getBrokerBinaryPath, formatBrokerNotFoundError } from './broker-path.js'; import type { AgentRuntime, BrokerEvent, @@ -210,7 +210,14 @@ export class AgentRelayClient { * 6. Starts event stream + lease renewal */ static async spawn(options?: AgentRelaySpawnOptions): Promise { - const binaryPath = options?.binaryPath ?? getBrokerBinaryPath() ?? 'agent-relay-broker'; + let binaryPath = options?.binaryPath; + if (!binaryPath) { + const resolved = getBrokerBinaryPath(); + if (!resolved) { + throw new Error(formatBrokerNotFoundError()); + } + binaryPath = resolved; + } const cwd = options?.cwd ?? process.cwd(); const brokerName = options?.brokerName ?? (path.basename(cwd) || 'project'); const channels = options?.channels ?? ['general']; diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 7ed9b6d11..ce0be0bf4 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -3,10 +3,14 @@ * Postinstall Script for agent-relay * * This script runs after npm install to: - * 1. Install agent-relay-broker binary for current platform - * 2. Install dashboard dependencies + * 1. Install dashboard-server and relay-acp binaries (parity with curl installer) + * 2. Rebuild better-sqlite3 / confirm SQLite driver * 3. Patch agent-trajectories CLI - * 4. Check for tmux availability (fallback) + * 4. Patch @relayauth/core exports for CommonJS require() + * + * The agent-relay-broker binary is no longer installed here — it ships as a + * platform-specific optional dependency of @agent-relay/sdk + * (`@agent-relay/broker--`), resolved at runtime by the SDK. */ import { execSync } from 'node:child_process'; @@ -325,132 +329,6 @@ function resignBinaryForMacOS(binaryPath) { } } -/** - * Get the platform-specific binary name for the broker binary. - * The broker binary is the Rust-compiled broker (not the Bun-compiled CLI). - * It is needed by the SDK (packages/sdk) for programmatic - * agent orchestration via `new AgentRelay()`. - * Returns null if platform is not supported. - */ -function getBrokerBinaryName() { - const platform = os.platform(); - const arch = os.arch(); - - const archMap = { 'arm64': 'arm64', 'x64': 'x64' }; - const platformMap = { 'darwin': 'darwin', 'linux': 'linux' }; - - const targetPlatform = platformMap[platform]; - const targetArch = archMap[arch]; - - if (!targetPlatform || !targetArch) { - return null; - } - - // Use the broker-specific release asset name (Rust binary, not Bun CLI) - return `agent-relay-broker-${targetPlatform}-${targetArch}`; -} - -/** - * Install the broker binary into packages/sdk/bin/. - * - * The SDK's AgentRelayClient spawns this binary as a subprocess - * (`agent-relay-broker init --name broker --channels general`). Without it, - * `new AgentRelay()` will fail with "broker exited (code=1)". - * - * Resolution order: - * 1. Already bundled at packages/sdk/bin/agent-relay-broker (e.g. from prepack) - * 2. Platform-specific binary bundled in root bin/ (e.g. bin/agent-relay-broker-linux-x64) - * 3. Download platform-specific standalone binary from GitHub releases - * 4. Fall back to the local Rust debug binary at target/debug/agent-relay-broker (dev only) - */ -async function installBrokerBinary() { - const pkgRoot = getPackageRoot(); - const sdkBinDir = path.join(pkgRoot, 'packages', 'sdk', 'bin'); - const isWindows = process.platform === 'win32'; - const binaryFilename = isWindows ? 'agent-relay-broker.exe' : 'agent-relay-broker'; - const targetPath = path.join(sdkBinDir, binaryFilename); - - // 1. Already installed? Verify it's the Rust broker (supports --name flag) - if (fs.existsSync(targetPath)) { - try { - const helpOutput = execSync(`"${targetPath}" init --help`, { stdio: 'pipe' }).toString(); - // The Rust broker shows "--name " in init --help - // The Bun-compiled Node.js CLI shows "First-time setup wizard" - if (helpOutput.includes('--name')) { - info('Broker binary already installed in SDK (Rust broker verified)'); - return true; - } - // Wrong binary (Bun CLI instead of Rust broker) — reinstall - warn('Broker binary exists but is the CLI, not the Rust broker — reinstalling'); - } catch { - // Binary exists but doesn't work — reinstall - } - } - - fs.mkdirSync(sdkBinDir, { recursive: true }); - - const binaryName = getBrokerBinaryName(); - - // 2. Check for bundled platform-specific binary in root bin/ - if (binaryName) { - const bundledBinary = path.join(pkgRoot, 'bin', binaryName); - if (fs.existsSync(bundledBinary)) { - try { - fs.copyFileSync(bundledBinary, targetPath); - fs.chmodSync(targetPath, 0o755); - resignBinaryForMacOS(targetPath); - success(`Installed broker binary from bundled package (${binaryName})`); - return true; - } catch (err) { - warn(`Failed to copy bundled broker binary: ${err.message}`); - } - } - } - - // 3. Try downloading from GitHub releases - if (binaryName) { - const version = getPackageVersion(pkgRoot); - if (version) { - const downloadUrl = `https://github.com/AgentWorkforce/relay/releases/download/v${version}/${binaryName}`; - info(`Downloading broker binary from ${downloadUrl} ...`); - - try { - await downloadBinary(downloadUrl, targetPath); - await verifyChecksum(targetPath, downloadUrl); - fs.chmodSync(targetPath, 0o755); - resignBinaryForMacOS(targetPath); - success(`Downloaded broker binary for ${os.platform()}-${os.arch()}`); - return true; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - warn(`Failed to download broker binary: ${message}`); - // Clean up partial/untrusted download - try { fs.unlinkSync(targetPath); } catch { /* ignore */ } - } - } - } - - // 4. Dev fallback — check for local Rust build (release first, then debug) - for (const profile of ['release', 'debug']) { - const localBinary = path.join(pkgRoot, 'target', profile, binaryFilename); - if (fs.existsSync(localBinary)) { - try { - fs.copyFileSync(localBinary, targetPath); - fs.chmodSync(targetPath, 0o755); - resignBinaryForMacOS(targetPath); - success(`Installed broker binary from local Rust ${profile} build`); - return true; - } catch (err) { - warn(`Failed to copy ${profile} broker binary: ${err.message}`); - } - } - } - - warn('Broker binary not available — SDK programmatic usage (AgentRelay) will not work'); - info('To fix: cargo build --release --bin agent-relay-broker (requires Rust toolchain)'); - return false; -} - /** * Install the standalone dashboard-server binary. * @@ -811,7 +689,7 @@ function patchRelayauthCoreExports() { } } -function logPostinstallDiagnostics(hasBrokerBinary, sqliteStatus, linkResult) { +function logPostinstallDiagnostics(sqliteStatus, linkResult) { // Workspace packages status (for global installs) if (linkResult && linkResult.needed) { if (linkResult.success) { @@ -821,12 +699,6 @@ function logPostinstallDiagnostics(hasBrokerBinary, sqliteStatus, linkResult) { } } - if (hasBrokerBinary) { - console.log('✓ agent-relay-broker binary installed'); - } else { - console.log('⚠ agent-relay-broker binary not installed - AgentRelay will not work'); - } - if (sqliteStatus.ok && sqliteStatus.driver === 'better-sqlite3') { console.log('✓ SQLite ready (better-sqlite3)'); } else if (sqliteStatus.ok && sqliteStatus.driver === 'node:sqlite') { @@ -856,9 +728,6 @@ async function main() { } } - // Install broker binary for agent spawning and SDK programmatic usage - const hasBrokerBinary = await installBrokerBinary(); - // Ensure SQLite driver is available (better-sqlite3 or node:sqlite) const sqliteStatus = ensureSqliteDriver(); @@ -876,7 +745,7 @@ async function main() { const hasAcpBinary = await installRelayAcpBinary(); // Always print diagnostics (even in CI) - logPostinstallDiagnostics(hasBrokerBinary, sqliteStatus, linkResult); + logPostinstallDiagnostics(sqliteStatus, linkResult); if (hasDashboardBinary) { console.log('✓ dashboard-server binary installed'); @@ -884,12 +753,6 @@ async function main() { if (hasAcpBinary) { console.log('✓ relay-acp binary installed (Zed editor integration)'); } - - if (!hasBrokerBinary) { - warn('agent-relay-broker binary not available'); - info('Agent spawning will not work without the broker binary.'); - info('To fix: cargo build --release --bin agent-relay-broker (requires Rust toolchain)'); - } } main().catch((err) => {