Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 74 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }})
Expand Down Expand Up @@ -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() &&
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
82 changes: 79 additions & 3 deletions packages/broker-sdk/src/workflows/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -107,6 +108,75 @@
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<void> {
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<string, any>;
const data = (body.data ?? body) as Record<string, any>;
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(),
}),
Comment thread Fixed
Comment on lines +167 to +173

Check warning

Code scanning / CodeQL

Network data written to file Medium

Write to file system depends on
Untrusted data
.
{ mode: 0o600 },
);
Comment thread Fixed
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

process.env.RELAY_API_KEY = apiKey;
}

// ── Event subscription ──────────────────────────────────────────────────

on(listener: WorkflowEventListener): () => void {
Expand Down Expand Up @@ -402,9 +472,12 @@
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<string, AgentDefinition>();
Expand Down Expand Up @@ -497,9 +570,12 @@
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<string, AgentDefinition>();
Expand Down
44 changes: 27 additions & 17 deletions scripts/postinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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}`;
}

/**
Expand All @@ -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 <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
}
Expand Down Expand Up @@ -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}`);
}
}
}

Expand Down
Loading