From fa5a41f19352307ff6e9074f54f8af15680cbc4e Mon Sep 17 00:00:00 2001 From: Khaliq Date: Wed, 18 Feb 2026 21:59:56 +0100 Subject: [PATCH] fix: bundle Rust broker binary for SDK programmatic usage The SDK's AgentRelayClient spawns `agent-relay init --name broker --channels ` which requires the Rust broker binary, not the Bun-compiled Node.js CLI. The publish workflow was uploading the CLI as `agent-relay-{platform}-{arch}` and postinstall was downloading it into sdk-ts/bin/, causing "broker exited (code=1)" errors for all workflow runs. Changes: - Add `build-broker` CI job that compiles src/main.rs (Rust) for all platforms as `agent-relay-broker-{platform}-{arch}` - Update postinstall.js to download broker-specific release assets - Add validation to detect wrong binary (CLI vs Rust broker) - Check target/release before target/debug in dev fallback - Add Relaycast auto-provisioning to WorkflowRunner so workflows run without manual RELAY_API_KEY setup Co-Authored-By: Claude Opus 4.6 --- .github/workflows/publish.yml | 76 ++++++++++++++++++- packages/broker-sdk/src/workflows/runner.ts | 82 ++++++++++++++++++++- scripts/postinstall.js | 44 ++++++----- 3 files changed, 180 insertions(+), 22 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d7a53a588..d9aea0c1c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -127,6 +127,64 @@ jobs: path: bin/${{ matrix.binary_name }} retention-days: 1 + # Build Rust broker binary for all platforms (needed by SDK's AgentRelayClient) + build-broker: + name: Build broker (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + if: github.event.inputs.package == 'all' || github.event.inputs.package == 'main' + strategy: + matrix: + include: + - os: macos-latest + target: aarch64-apple-darwin + binary_name: agent-relay-broker-darwin-arm64 + - os: macos-latest + target: x86_64-apple-darwin + binary_name: agent-relay-broker-darwin-x64 + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + binary_name: agent-relay-broker-linux-x64 + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + binary_name: agent-relay-broker-linux-arm64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install cross-compilation tools (Linux ARM64) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + key: broker-${{ matrix.target }} + + - name: Build broker binary + run: cargo build --release --bin agent-relay --target ${{ matrix.target }} + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Copy binary with platform name + run: | + mkdir -p release-binaries + cp target/${{ matrix.target }}/release/agent-relay release-binaries/${{ matrix.binary_name }} + + - name: Upload binary + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.binary_name }} + path: release-binaries/${{ matrix.binary_name }} + retention-days: 1 + # Build standalone binaries using bun compile (cross-platform, no Node.js required) build-standalone: name: Build standalone (${{ matrix.target }}) @@ -899,7 +957,7 @@ jobs: # Create git tag and release create-release: name: Create Release - needs: [build, build-binaries, build-standalone, build-acp-standalone, verify-binaries, publish-main] + needs: [build, build-binaries, build-broker, build-standalone, build-acp-standalone, verify-binaries, publish-main] runs-on: ubuntu-latest if: | always() && @@ -941,6 +999,13 @@ jobs: path: release-binaries/ merge-multiple: true + - name: Download broker binaries + uses: actions/download-artifact@v4 + with: + pattern: agent-relay-broker-* + path: release-binaries/ + merge-multiple: true + - name: Make binaries executable run: | # Make uncompressed binaries executable (skip .gz files) @@ -1066,6 +1131,13 @@ jobs: - `relay-pty-darwin-x64` - macOS Intel - `relay-pty-darwin-arm64` - macOS Apple Silicon + ### Broker binaries (SDK programmatic usage) + Rust broker binary for `new AgentRelay()` / workflow orchestration: + - `agent-relay-broker-linux-x64` - Linux x86_64 + - `agent-relay-broker-linux-arm64` - Linux ARM64 + - `agent-relay-broker-darwin-x64` - macOS Intel + - `agent-relay-broker-darwin-arm64` - macOS Apple Silicon + ### relay-acp binaries (Zed editor integration) ACP bridge for Zed editor: - `relay-acp-linux-x64` - Linux x86_64 @@ -1138,7 +1210,7 @@ jobs: summary: name: Summary - needs: [build, build-binaries, build-standalone, build-acp-standalone, verify-binaries, verify-standalone-linux, verify-standalone-macos, verify-acp-linux, verify-acp-macos, publish-packages, publish-main, verify-publish] + needs: [build, build-binaries, build-broker, build-standalone, build-acp-standalone, verify-binaries, verify-standalone-linux, verify-standalone-macos, verify-acp-linux, verify-acp-macos, publish-packages, publish-main, verify-publish] runs-on: ubuntu-latest if: always() diff --git a/packages/broker-sdk/src/workflows/runner.ts b/packages/broker-sdk/src/workflows/runner.ts index b7af380dd..2a6ee7b7e 100644 --- a/packages/broker-sdk/src/workflows/runner.ts +++ b/packages/broker-sdk/src/workflows/runner.ts @@ -6,7 +6,8 @@ import { randomBytes } from 'node:crypto'; import { existsSync } from 'node:fs'; -import { readFile } from 'node:fs/promises'; +import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { homedir } from 'node:os'; import path from 'node:path'; import { parse as parseYaml } from 'yaml'; @@ -107,6 +108,75 @@ export class WorkflowRunner { this.summaryDir = options.summaryDir ?? path.join(this.cwd, '.relay', 'summaries'); } + // ── Relaycast auto-provisioning ──────────────────────────────────────── + + /** + * Ensure a Relaycast workspace API key is available for the broker. + * Resolution order: + * 1. RELAY_API_KEY environment variable + * 2. Cached credentials at ~/.agent-relay/relaycast.json + * 3. Auto-create a new workspace via the Relaycast API + */ + private async ensureRelaycastApiKey(channel: string): Promise { + if (process.env.RELAY_API_KEY) return; + + // Check cached credentials + const cachePath = path.join(homedir(), '.agent-relay', 'relaycast.json'); + if (existsSync(cachePath)) { + try { + const raw = await readFile(cachePath, 'utf-8'); + const creds = JSON.parse(raw); + if (creds.api_key) { + process.env.RELAY_API_KEY = creds.api_key; + return; + } + } catch { + // Cache corrupt — fall through to auto-create + } + } + + // Auto-create a Relaycast workspace with a unique name + const workspaceName = `relay-${channel}-${randomBytes(4).toString('hex')}`; + const baseUrl = process.env.RELAYCAST_BASE_URL ?? 'https://api.relaycast.dev'; + const res = await fetch(`${baseUrl}/v1/workspaces`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ name: workspaceName }), + }); + + if (!res.ok) { + throw new Error( + `Failed to auto-create Relaycast workspace: ${res.status} ${await res.text()}`, + ); + } + + const body = (await res.json()) as Record; + const data = (body.data ?? body) as Record; + const apiKey = data.api_key as string; + const workspaceId = (data.workspace_id ?? data.id) as string; + + if (!apiKey) { + throw new Error('Relaycast workspace response missing api_key'); + } + + // Cache credentials for future runs + const cacheDir = path.dirname(cachePath); + await mkdir(cacheDir, { recursive: true, mode: 0o700 }); + await writeFile( + cachePath, + JSON.stringify({ + workspace_id: workspaceId, + api_key: apiKey, + agent_id: '', + agent_name: null, + updated_at: new Date().toISOString(), + }), + { mode: 0o600 }, + ); + + process.env.RELAY_API_KEY = apiKey; + } + // ── Event subscription ────────────────────────────────────────────────── on(listener: WorkflowEventListener): () => void { @@ -402,9 +472,12 @@ export class WorkflowRunner { await this.updateRunStatus(runId, 'running'); this.emit({ type: 'run:started', runId }); + const channel = resolved.swarm.channel ?? 'general'; + await this.ensureRelaycastApiKey(channel); + this.relay = new AgentRelay({ ...this.relayOptions, - channels: [resolved.swarm.channel ?? 'general'], + channels: [channel], }); const agentMap = new Map(); @@ -497,9 +570,12 @@ export class WorkflowRunner { try { await this.updateRunStatus(runId, 'running'); + const resumeChannel = config.swarm.channel ?? 'general'; + await this.ensureRelaycastApiKey(resumeChannel); + this.relay = new AgentRelay({ ...this.relayOptions, - channels: [config.swarm.channel ?? 'general'], + channels: [resumeChannel], }); const agentMap = new Map(); diff --git a/scripts/postinstall.js b/scripts/postinstall.js index c4c496a5f..74bc352ef 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -359,7 +359,8 @@ async function installRelayPtyBinary() { /** * Get the platform-specific binary name for the broker binary. - * The broker binary is needed by the SDK (packages/broker-sdk) for programmatic + * The broker binary is the Rust-compiled broker (not the Bun-compiled CLI). + * It is needed by the SDK (packages/broker-sdk) for programmatic * agent orchestration via `new AgentRelay()`. * Returns null if platform is not supported. */ @@ -377,7 +378,8 @@ function getBrokerBinaryName() { return null; } - return `agent-relay-${targetPlatform}-${targetArch}`; + // Use the broker-specific release asset name (Rust binary, not Bun CLI) + return `agent-relay-broker-${targetPlatform}-${targetArch}`; } /** @@ -399,12 +401,18 @@ async function installBrokerBinary() { const binaryFilename = isWindows ? 'agent-relay.exe' : 'agent-relay'; const targetPath = path.join(sdkBinDir, binaryFilename); - // 1. Already installed? + // 1. Already installed? Verify it's the Rust broker (supports --name flag) if (fs.existsSync(targetPath)) { try { - execSync(`"${targetPath}" init --help`, { stdio: 'pipe' }); - info('Broker binary already installed in SDK'); - return true; + 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 } @@ -433,17 +441,19 @@ async function installBrokerBinary() { } } - // 3. Dev fallback — check for local Rust build - const debugBinary = path.join(pkgRoot, 'target', 'debug', binaryFilename); - if (fs.existsSync(debugBinary)) { - try { - fs.copyFileSync(debugBinary, targetPath); - fs.chmodSync(targetPath, 0o755); - resignBinaryForMacOS(targetPath); - success('Installed broker binary from local Rust debug build'); - return true; - } catch (err) { - warn(`Failed to copy debug broker binary: ${err.message}`); + // 3. 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}`); + } } }