From 5c1fcc43eedf87d6f62464f20b42fa7488cade4c Mon Sep 17 00:00:00 2001 From: Proactive Runtime Bot Date: Wed, 1 Jul 2026 10:30:42 -0700 Subject: [PATCH 1/8] feat(cli): provision server-side inbound integration bridge in `integration subscribe` `agent-relay integration subscribe` now creates a relaycast inbound-target and a relayfile-cloud webhook subscription (via the relayfile control-plane) so provider messages are injected server-side into the target relay channel, with rollback on partial failure and `{ok,data}` envelope parsing. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + .../commands/integration-subscribe.test.ts | 22 ++++- packages/cli/src/cli/commands/integration.ts | 97 +++++++++++++++++++ .../src/cli/commands/relaycast-groups.test.ts | 35 ++++++- 4 files changed, 153 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e9fb4648..f2daff9c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `agent-relay skills add` installs the `/orchestrate` skill (from `agentrelay.com/skill.md`) into your coding harnesses. An interactive TUI asks whether to install for the current project or globally and which harnesses to target (Claude Code, Codex, Cursor, Gemini, OpenCode); `--global`/`--local`, `--harness `, and `--all` flags drive it non-interactively. - `agent-relay up --verbose` now prints step-by-step startup progress (port resolution, broker process spawn, handshake retries, fleet sidecar, node-delivery wait, agent spawns) and streams the broker's own startup-phase logs and stderr live, instead of only surfacing a terse error if startup fails. +- `agent-relay integration subscribe` now wires a server-side inbound bridge: it provisions a relaycast inbound-target and a relayfile-cloud webhook subscription so real provider messages (Slack/GitHub/Linear) are injected into the target relay channel — and delivered to on-node agents — with no local watcher or client process running. ### Changed diff --git a/packages/cli/src/cli/commands/integration-subscribe.test.ts b/packages/cli/src/cli/commands/integration-subscribe.test.ts index b5ace224c..13bdeacce 100644 --- a/packages/cli/src/cli/commands/integration-subscribe.test.ts +++ b/packages/cli/src/cli/commands/integration-subscribe.test.ts @@ -1,5 +1,5 @@ import { Command } from 'commander'; -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { registerIntegrationCommands, @@ -7,6 +7,10 @@ import { type RelayfileBinding, } from './integration.js'; +afterEach(() => { + vi.unstubAllGlobals(); +}); + interface InboundWebhook { webhookId: string; url: string; @@ -84,6 +88,8 @@ function createRelayfileMock( resolveResourcePath: vi.fn(async (_provider: string, resource: string) => ({ pathGlob: resource })), ensureCompatible: vi.fn(async () => undefined), resolveWritebackBinding: vi.fn(async () => ({ url: 'https://ingress.example', secret: 's3cr3t' })), + createWebhookSubscription: vi.fn(async () => ({ subscriptionId: 'whsub_1' })), + deleteWebhookSubscription: vi.fn(async () => undefined), ...overrides, }; } @@ -96,6 +102,15 @@ function harness( ) { const relay = opts.relay ?? createRelayMock(); const relayfile = opts.relayfile ?? createRelayfileMock(); + vi.stubGlobal( + 'fetch', + vi.fn(async () => + new Response(JSON.stringify({ ok: true, data: { url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', secret: 'inbound-secret' } }), { + status: 201, + headers: { 'content-type': 'application/json' }, + }) + ) + ); const log = vi.fn(); const error = vi.fn(); const exit = vi.fn(); @@ -139,6 +154,11 @@ describe('integration subscribe', () => { expect(relayfile.bind).toHaveBeenCalledWith( expect.objectContaining({ provider: 'slack', resource: RESOURCE, channel: 'general' }) ); + expect(relayfile.createWebhookSubscription).toHaveBeenCalledWith({ + url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', + pathGlobs: [RESOURCE], + secret: 'inbound-secret', + }); }); it('resolves provider-native resources before binding and replacement lookup', async () => { diff --git a/packages/cli/src/cli/commands/integration.ts b/packages/cli/src/cli/commands/integration.ts index c4a666b67..cf84bda1c 100644 --- a/packages/cli/src/cli/commands/integration.ts +++ b/packages/cli/src/cli/commands/integration.ts @@ -20,6 +20,7 @@ import { assertRelayfileVersion, type RelayfileClientOptions, } from '@relayfile/client'; +import { resolveBaseUrl, resolveWorkspaceKey } from '../lib/sdk-client.js'; // Re-export the version gate so existing tests importing it from this module // (and any callers) keep working after it moved to the published client package. @@ -52,6 +53,11 @@ export interface RelayfileWritebackBinding { secret: string; } +export interface RelayfileWebhookSubscription { + subscriptionId: string; + secret?: string; +} + export interface RelayfileBridge { isConnected(provider: string): Promise; connect(provider: string): Promise; @@ -88,6 +94,12 @@ export interface RelayfileBridge { * subscription and the ingress agree on it without any static shared secret. */ resolveWritebackBinding(channel: string): Promise; + createWebhookSubscription(input: { + url: string; + pathGlobs: string[]; + secret: string; + }): Promise; + deleteWebhookSubscription(subscriptionId: string): Promise; } export type IntegrationCommandDependencies = SdkCommandDeps & { @@ -232,6 +244,32 @@ export function defaultRelayfileBridge(options?: RelayfileClientOptions): Relayf return undefined; } }, + async createWebhookSubscription(input) { + const extendedClient = client as RelayfileControlPlaneClient & { + createWebhookSubscription?: (input: { + url: string; + pathGlobs: string[]; + secret: string; + }) => Promise; + }; + if (!extendedClient.createWebhookSubscription) { + throw new Error( + 'relayfile control-plane does not support webhook subscriptions yet. Update relayfile and re-run `agent-relay integration subscribe`.' + ); + } + return extendedClient.createWebhookSubscription(input); + }, + async deleteWebhookSubscription(subscriptionId) { + const extendedClient = client as RelayfileControlPlaneClient & { + deleteWebhookSubscription?: (subscriptionId: string) => Promise; + }; + if (!extendedClient.deleteWebhookSubscription) { + throw new Error( + 'relayfile control-plane does not support deleting webhook subscriptions yet. Update relayfile and retry cleanup.' + ); + } + await extendedClient.deleteWebhookSubscription(subscriptionId); + }, }; } @@ -289,6 +327,10 @@ function explicitWorkspaceKey(opts: Record): boolean { return typeof opts.workspaceKey === 'string' && opts.workspaceKey.trim() !== ''; } +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + function shouldRetryWithLocalWorkspaceKey(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); return ( @@ -368,6 +410,44 @@ async function resolveWriteback( return { url: binding.url, secret: binding.secret }; } +async function createRelayfileInboundTarget( + commandOpts: Record, + local: LocalRelayOptions | undefined, + input: { channel: string; provider: string; pathGlob: string } +): Promise<{ url: string; secret: string }> { + const options = sdkOptionsFromOpts(commandOpts); + const resolved = local && !explicitWorkspaceKey(commandOpts) ? localRetryOptions(options, local) : options; + const workspaceKey = resolveWorkspaceKey(resolved); + const baseUrl = resolveBaseUrl(resolved) ?? 'https://cast.agentrelay.com'; + const response = await fetch(new URL('/v1/integrations/relayfile/inbound-target', baseUrl), { + method: 'POST', + headers: { + Authorization: `Bearer ${workspaceKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + channel: input.channel, + provider: input.provider, + pathGlob: input.pathGlob, + }), + }); + let body: unknown; + try { + body = await response.json(); + } catch { + body = undefined; + } + if (!response.ok) { + const message = isRecord(body) && typeof body.message === 'string' ? body.message : `relaycast returned ${response.status}`; + throw new Error(`Could not create relayfile inbound target: ${message}`); + } + const data = isRecord(body) && isRecord(body.data) ? body.data : body; + if (!isRecord(data) || typeof data.url !== 'string' || typeof data.secret !== 'string') { + throw new Error('relaycast returned an invalid relayfile inbound target response'); + } + return { url: data.url.trim(), secret: data.secret.trim() }; +} + function targetChannel(target: string): string { const trimmed = target.trim(); // Strip the leading sigil so the channel id is canonical (`general`, not @@ -629,6 +709,11 @@ async function runSubscribe( const channel = targetChannel(to); const events = commaList(opts.events); const writeback = await resolveWriteback(deps, opts, channel); + const inboundTarget = await createRelayfileInboundTarget(opts, local, { + channel, + provider, + pathGlob, + }); const prefix = webhookNamePrefix(provider, pathGlob); const name = inboundWebhookName(prefix); @@ -640,8 +725,14 @@ async function runSubscribe( let webhook: { webhookId: string; token: string } | undefined; let subscription: { id: string } | undefined; + let relayfileWebhook: RelayfileWebhookSubscription | undefined; try { webhook = await relay.webhooks.createInbound({ channel, name }); + relayfileWebhook = await deps.relayfile.createWebhookSubscription({ + url: inboundTarget.url, + pathGlobs: [pathGlob], + secret: inboundTarget.secret, + }); subscription = await relay.integrations.subscriptions.create({ event: events.length === 1 ? events[0]! : 'message.created', events: events.length ? events : ['message.created', 'thread.reply'], @@ -670,6 +761,11 @@ async function runSubscribe( .delete(webhook.webhookId) .catch((cleanupErr) => warnCleanup(deps, 'webhook', webhook!.webhookId, cleanupErr)); } + if (relayfileWebhook) { + await deps.relayfile + .deleteWebhookSubscription(relayfileWebhook.subscriptionId) + .catch((cleanupErr) => warnCleanup(deps, 'relayfile webhook subscription', relayfileWebhook!.subscriptionId, cleanupErr)); + } throw err; } @@ -685,6 +781,7 @@ async function runSubscribe( // Show the native resource the user typed, plus the resolved glob when they differ. const boundLabel = pathGlob === resource ? resource : `${resource} (${pathGlob})`; deps.log(`✓ ${provider} ${boundLabel} bound -> ${to}`); + deps.log(`✓ Server-side inbound webhook subscription: ${relayfileWebhook!.subscriptionId}`); deps.log('✓ Listening. Replies will post back in-thread.'); } diff --git a/packages/cli/src/cli/commands/relaycast-groups.test.ts b/packages/cli/src/cli/commands/relaycast-groups.test.ts index f22580925..050e01f85 100644 --- a/packages/cli/src/cli/commands/relaycast-groups.test.ts +++ b/packages/cli/src/cli/commands/relaycast-groups.test.ts @@ -1,5 +1,5 @@ import { Command } from 'commander'; -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { registerAgentCommands } from './agent.js'; import { registerChannelCommands } from './channel.js'; @@ -8,6 +8,22 @@ import { registerIntegrationCommands, type IntegrationCommandDependencies } from import { registerCapabilitiesCommands } from './capabilities.js'; import type { SdkCommandDeps } from '../lib/sdk-command.js'; +beforeEach(() => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => + new Response(JSON.stringify({ ok: true, data: { url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', secret: 'inbound-secret' } }), { + status: 201, + headers: { 'content-type': 'application/json' }, + }) + ) + ); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + function createRelayMock() { return { agents: { @@ -312,6 +328,8 @@ describe('SDK-backed CLI groups', () => { url: 'https://file.test/v1/workspaces/rw_test/integrations/relay/writeback', secret: 'test-secret', })), + createWebhookSubscription: vi.fn(async () => ({ subscriptionId: 'whsub_1' })), + deleteWebhookSubscription: vi.fn(async () => undefined), }, } satisfies Partial); @@ -342,6 +360,8 @@ describe('SDK-backed CLI groups', () => { url: 'https://file.test/v1/workspaces/rw_test/integrations/relay/writeback', secret: 'test-secret', })), + createWebhookSubscription: vi.fn(async () => ({ subscriptionId: 'whsub_1' })), + deleteWebhookSubscription: vi.fn(async () => undefined), }; const log = vi.fn(); const error = vi.fn(); @@ -382,6 +402,11 @@ describe('SDK-backed CLI groups', () => { channel: 'slackbot', name: expect.stringMatching(/^relayfile:slack:.+-[0-9a-f]{10}:[0-9a-f]{10}$/), }); + expect(relayfile.createWebhookSubscription).toHaveBeenCalledWith({ + url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', + pathGlobs: ['#acme'], + secret: 'inbound-secret', + }); expect(relay.integrations.subscriptions.create).toHaveBeenCalledWith({ event: 'message.created', events: ['message.created', 'thread.reply'], @@ -415,6 +440,8 @@ describe('SDK-backed CLI groups', () => { url: 'https://file.agentrelay.com/v1/workspaces/rw_7ccfea89/integrations/relay/writeback', secret: 'derived-secret-hex', })), + createWebhookSubscription: vi.fn(async () => ({ subscriptionId: 'whsub_1' })), + deleteWebhookSubscription: vi.fn(async () => undefined), }; const program = new Command(); program.exitOverride(); @@ -464,6 +491,8 @@ describe('SDK-backed CLI groups', () => { url: 'https://file.test/v1/workspaces/rw_test/integrations/relay/writeback', secret: 'test-secret', })), + createWebhookSubscription: vi.fn(async () => ({ subscriptionId: 'whsub_1' })), + deleteWebhookSubscription: vi.fn(async () => undefined), }; const log = vi.fn(); const error = vi.fn(); @@ -533,6 +562,8 @@ describe('SDK-backed CLI groups', () => { url: 'https://file.test/v1/workspaces/rw_test/integrations/relay/writeback', secret: 'test-secret', })), + createWebhookSubscription: vi.fn(async () => ({ subscriptionId: 'whsub_1' })), + deleteWebhookSubscription: vi.fn(async () => undefined), }, } satisfies Partial); @@ -569,6 +600,8 @@ describe('SDK-backed CLI groups', () => { url: 'https://file.test/v1/workspaces/rw_test/integrations/relay/writeback', secret: 'test-secret', })), + createWebhookSubscription: vi.fn(async () => ({ subscriptionId: 'whsub_1' })), + deleteWebhookSubscription: vi.fn(async () => undefined), }; const log = vi.fn(); const error = vi.fn(); From 0defe54a2ed4f39baf8c0cf8bbea7ffe4486cfbe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Jul 2026 17:34:18 +0000 Subject: [PATCH 2/8] style: auto-format with Prettier --- .../commands/integration-subscribe.test.ts | 20 ++++++++++++++----- packages/cli/src/cli/commands/integration.ts | 9 +++++++-- .../src/cli/commands/relaycast-groups.test.ts | 20 ++++++++++++++----- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/cli/commands/integration-subscribe.test.ts b/packages/cli/src/cli/commands/integration-subscribe.test.ts index 13bdeacce..892052aa7 100644 --- a/packages/cli/src/cli/commands/integration-subscribe.test.ts +++ b/packages/cli/src/cli/commands/integration-subscribe.test.ts @@ -104,11 +104,21 @@ function harness( const relayfile = opts.relayfile ?? createRelayfileMock(); vi.stubGlobal( 'fetch', - vi.fn(async () => - new Response(JSON.stringify({ ok: true, data: { url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', secret: 'inbound-secret' } }), { - status: 201, - headers: { 'content-type': 'application/json' }, - }) + vi.fn( + async () => + new Response( + JSON.stringify({ + ok: true, + data: { + url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', + secret: 'inbound-secret', + }, + }), + { + status: 201, + headers: { 'content-type': 'application/json' }, + } + ) ) ); const log = vi.fn(); diff --git a/packages/cli/src/cli/commands/integration.ts b/packages/cli/src/cli/commands/integration.ts index cf84bda1c..489579ed3 100644 --- a/packages/cli/src/cli/commands/integration.ts +++ b/packages/cli/src/cli/commands/integration.ts @@ -438,7 +438,10 @@ async function createRelayfileInboundTarget( body = undefined; } if (!response.ok) { - const message = isRecord(body) && typeof body.message === 'string' ? body.message : `relaycast returned ${response.status}`; + const message = + isRecord(body) && typeof body.message === 'string' + ? body.message + : `relaycast returned ${response.status}`; throw new Error(`Could not create relayfile inbound target: ${message}`); } const data = isRecord(body) && isRecord(body.data) ? body.data : body; @@ -764,7 +767,9 @@ async function runSubscribe( if (relayfileWebhook) { await deps.relayfile .deleteWebhookSubscription(relayfileWebhook.subscriptionId) - .catch((cleanupErr) => warnCleanup(deps, 'relayfile webhook subscription', relayfileWebhook!.subscriptionId, cleanupErr)); + .catch((cleanupErr) => + warnCleanup(deps, 'relayfile webhook subscription', relayfileWebhook!.subscriptionId, cleanupErr) + ); } throw err; } diff --git a/packages/cli/src/cli/commands/relaycast-groups.test.ts b/packages/cli/src/cli/commands/relaycast-groups.test.ts index 050e01f85..aeda47976 100644 --- a/packages/cli/src/cli/commands/relaycast-groups.test.ts +++ b/packages/cli/src/cli/commands/relaycast-groups.test.ts @@ -11,11 +11,21 @@ import type { SdkCommandDeps } from '../lib/sdk-command.js'; beforeEach(() => { vi.stubGlobal( 'fetch', - vi.fn(async () => - new Response(JSON.stringify({ ok: true, data: { url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', secret: 'inbound-secret' } }), { - status: 201, - headers: { 'content-type': 'application/json' }, - }) + vi.fn( + async () => + new Response( + JSON.stringify({ + ok: true, + data: { + url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', + secret: 'inbound-secret', + }, + }), + { + status: 201, + headers: { 'content-type': 'application/json' }, + } + ) ) ); }); From 9ea6575a012e9d882ca79006545e7f4e219537aa Mon Sep 17 00:00:00 2001 From: "agent-relay-code[bot]" Date: Wed, 1 Jul 2026 17:46:01 +0000 Subject: [PATCH 3/8] chore: apply pr-reviewer fixes for #1222 --- package-lock.json | 112 +++++++++++++++++----------------------------- 1 file changed, 41 insertions(+), 71 deletions(-) diff --git a/package-lock.json b/package-lock.json index 384af8447..08463c83b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agent-relay/monorepo", - "version": "9.1.7", + "version": "9.1.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agent-relay/monorepo", - "version": "9.1.7", + "version": "9.1.9", "license": "Apache-2.0", "workspaces": [ "packages/*" @@ -1769,7 +1769,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -1941,7 +1940,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -1959,7 +1957,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -1977,7 +1974,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -1995,7 +1991,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -2013,7 +2008,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -2031,7 +2025,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -2049,7 +2042,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2067,7 +2059,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2085,7 +2076,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2103,7 +2093,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2121,7 +2110,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2139,7 +2127,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2157,7 +2144,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2175,7 +2161,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2193,7 +2178,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2211,7 +2195,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2229,7 +2212,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2247,7 +2229,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2265,7 +2246,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2283,7 +2263,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2301,7 +2280,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2319,7 +2297,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -2337,7 +2314,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -2355,7 +2331,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -2373,7 +2348,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -2391,7 +2365,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -4991,7 +4964,6 @@ "version": "0.0.7", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", - "dev": true, "optional": true, "engines": { "node": ">=10.0.0" @@ -5319,7 +5291,6 @@ "version": "0.0.10", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", - "dev": true, "hasInstallScript": true, "optional": true, "dependencies": { @@ -7755,7 +7726,6 @@ "version": "2.27.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", - "dev": true, "license": "MIT", "optional": true }, @@ -9940,44 +9910,44 @@ }, "packages/brand": { "name": "@agent-relay/brand", - "version": "9.1.7" + "version": "9.1.9" }, "packages/broker-darwin-arm64": { "name": "@agent-relay/broker-darwin-arm64", - "version": "9.1.7", + "version": "9.1.9", "license": "MIT" }, "packages/broker-darwin-x64": { "name": "@agent-relay/broker-darwin-x64", - "version": "9.1.7", + "version": "9.1.9", "license": "MIT" }, "packages/broker-linux-arm64": { "name": "@agent-relay/broker-linux-arm64", - "version": "9.1.7", + "version": "9.1.9", "license": "MIT" }, "packages/broker-linux-x64": { "name": "@agent-relay/broker-linux-x64", - "version": "9.1.7", + "version": "9.1.9", "license": "MIT" }, "packages/broker-win32-x64": { "name": "@agent-relay/broker-win32-x64", - "version": "9.1.7", + "version": "9.1.9", "license": "MIT" }, "packages/cli": { "name": "agent-relay", - "version": "9.1.7", + "version": "9.1.9", "license": "Apache-2.0", "dependencies": { - "@agent-relay/cloud": "9.1.7", - "@agent-relay/config": "9.1.7", - "@agent-relay/fleet": "9.1.7", - "@agent-relay/harness-driver": "9.1.7", - "@agent-relay/sdk": "9.1.7", - "@agent-relay/utils": "9.1.7", + "@agent-relay/cloud": "9.1.9", + "@agent-relay/config": "9.1.9", + "@agent-relay/fleet": "9.1.9", + "@agent-relay/harness-driver": "9.1.9", + "@agent-relay/sdk": "9.1.9", + "@agent-relay/utils": "9.1.9", "@modelcontextprotocol/sdk": "^1.0.0", "@relaycast/sdk": "^5.0.5", "@relayfile/client": "^0.10.19", @@ -10047,9 +10017,9 @@ }, "packages/cloud": { "name": "@agent-relay/cloud", - "version": "9.1.7", + "version": "9.1.9", "dependencies": { - "@agent-relay/config": "9.1.7", + "@agent-relay/config": "9.1.9", "@aws-sdk/client-s3": "3.1020.0", "ignore": "^7.0.5", "tar": "^7.5.10" @@ -10065,7 +10035,7 @@ }, "packages/config": { "name": "@agent-relay/config", - "version": "9.1.7", + "version": "9.1.9", "dependencies": { "zod": "^3.23.8", "zod-to-json-schema": "^3.23.1" @@ -10078,60 +10048,60 @@ }, "packages/evals": { "name": "@agent-relay/evals", - "version": "9.1.7", + "version": "9.1.9", "license": "Apache-2.0", "dependencies": { - "@agent-relay/harness-driver": "9.1.7", - "@agent-relay/integration-prompts": "9.1.7" + "@agent-relay/harness-driver": "9.1.9", + "@agent-relay/integration-prompts": "9.1.9" } }, "packages/fleet": { "name": "@agent-relay/fleet", - "version": "9.1.7", + "version": "9.1.9", "license": "Apache-2.0", "dependencies": { - "@agent-relay/harness-driver": "9.1.7", - "@agent-relay/harnesses": "9.1.7", - "@agent-relay/sdk": "9.1.7", + "@agent-relay/harness-driver": "9.1.9", + "@agent-relay/harnesses": "9.1.9", + "@agent-relay/sdk": "9.1.9", "zod": "^3.23.8" } }, "packages/harness-driver": { "name": "@agent-relay/harness-driver", - "version": "9.1.7", + "version": "9.1.9", "license": "Apache-2.0", "dependencies": { - "@agent-relay/sdk": "9.1.7", + "@agent-relay/sdk": "9.1.9", "ws": "^8.18.3", "zod": "^3.23.8" }, "optionalDependencies": { - "@agent-relay/broker-darwin-arm64": "9.1.7", - "@agent-relay/broker-darwin-x64": "9.1.7", - "@agent-relay/broker-linux-arm64": "9.1.7", - "@agent-relay/broker-linux-x64": "9.1.7", - "@agent-relay/broker-win32-x64": "9.1.7" + "@agent-relay/broker-darwin-arm64": "9.1.9", + "@agent-relay/broker-darwin-x64": "9.1.9", + "@agent-relay/broker-linux-arm64": "9.1.9", + "@agent-relay/broker-linux-x64": "9.1.9", + "@agent-relay/broker-win32-x64": "9.1.9" } }, "packages/harnesses": { "name": "@agent-relay/harnesses", - "version": "9.1.7", + "version": "9.1.9", "license": "Apache-2.0", "dependencies": { - "@agent-relay/harness-driver": "9.1.7", - "@agent-relay/sdk": "9.1.7" + "@agent-relay/harness-driver": "9.1.9", + "@agent-relay/sdk": "9.1.9" } }, "packages/integration-prompts": { "name": "@agent-relay/integration-prompts", - "version": "9.1.7", + "version": "9.1.9", "license": "Apache-2.0" }, "packages/policy": { "name": "@agent-relay/policy", - "version": "9.1.7", + "version": "9.1.9", "dependencies": { - "@agent-relay/config": "9.1.7" + "@agent-relay/config": "9.1.9" }, "devDependencies": { "@types/node": "^22.19.3", @@ -10140,7 +10110,7 @@ }, "packages/sdk": { "name": "@agent-relay/sdk", - "version": "9.1.7", + "version": "9.1.9", "dependencies": { "@relaycast/sdk": "^5.0.5" }, @@ -10176,9 +10146,9 @@ }, "packages/utils": { "name": "@agent-relay/utils", - "version": "9.1.7", + "version": "9.1.9", "dependencies": { - "@agent-relay/config": "9.1.7", + "@agent-relay/config": "9.1.9", "compare-versions": "^6.1.1" }, "devDependencies": { From 5f6e7156f2ac61be8bd123552cf298588fb06ba6 Mon Sep 17 00:00:00 2001 From: Proactive Runtime Bot Date: Wed, 8 Jul 2026 10:24:13 +0200 Subject: [PATCH 4/8] test(cli): make integration subscribe auth deterministic --- packages/cli/src/cli/commands/integration-subscribe.test.ts | 3 ++- packages/cli/src/cli/commands/relaycast-groups.test.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/cli/commands/integration-subscribe.test.ts b/packages/cli/src/cli/commands/integration-subscribe.test.ts index f59648e32..f57b6e16e 100644 --- a/packages/cli/src/cli/commands/integration-subscribe.test.ts +++ b/packages/cli/src/cli/commands/integration-subscribe.test.ts @@ -130,7 +130,8 @@ function harness( registerIntegrationCommands(program, { createAgentRelay: () => relay as never, relayfile: relayfile as never, - resolveLocalRelayOptions: opts.resolveLocalRelayOptions ?? (async () => undefined), + resolveLocalRelayOptions: + opts.resolveLocalRelayOptions ?? (async () => ({ workspaceKey: 'rk_live_test' })), isInteractive: () => false, log, error, diff --git a/packages/cli/src/cli/commands/relaycast-groups.test.ts b/packages/cli/src/cli/commands/relaycast-groups.test.ts index aeda47976..62e773708 100644 --- a/packages/cli/src/cli/commands/relaycast-groups.test.ts +++ b/packages/cli/src/cli/commands/relaycast-groups.test.ts @@ -460,7 +460,7 @@ describe('SDK-backed CLI groups', () => { log: vi.fn(), error: vi.fn(), exit: vi.fn() as never, - resolveLocalRelayOptions: vi.fn(async () => undefined), + resolveLocalRelayOptions: vi.fn(async () => ({ workspaceKey: 'rk_live_local' })), isInteractive: () => false, relayfile, } satisfies Partial); From a56a7b6edfd7a11dfdc4be07ff71a6df99f09e0c Mon Sep 17 00:00:00 2001 From: Proactive Runtime Bot Date: Wed, 8 Jul 2026 10:46:14 +0200 Subject: [PATCH 5/8] fix(cli): validate relayfile inbound target responses --- .../commands/integration-subscribe.test.ts | 25 +++++++++++++++++++ packages/cli/src/cli/commands/integration.ts | 20 ++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/cli/commands/integration-subscribe.test.ts b/packages/cli/src/cli/commands/integration-subscribe.test.ts index f57b6e16e..a58920f30 100644 --- a/packages/cli/src/cli/commands/integration-subscribe.test.ts +++ b/packages/cli/src/cli/commands/integration-subscribe.test.ts @@ -225,6 +225,31 @@ describe('integration subscribe', () => { ); }); + it.each([ + ['blank URL', { url: ' ', secret: 'inbound-secret' }, 'invalid relayfile inbound target response'], + [ + 'blank secret', + { url: 'https://cast.test/inbound', secret: ' ' }, + 'invalid relayfile inbound target response', + ], + ['non-HTTPS URL', { url: 'http://cast.test/inbound', secret: 'inbound-secret' }, 'non-https'], + ])('rejects %s in inbound-target responses before creating webhooks', async (_case, data, message) => { + const { program, relay, relayfile, error, exit } = harness(); + vi.mocked(fetch).mockResolvedValueOnce( + new Response(JSON.stringify({ ok: true, data }), { + status: 201, + headers: { 'content-type': 'application/json' }, + }) + ); + + await program.parseAsync(ARGS(), { from: 'user' }); + + expect(error).toHaveBeenCalledWith(expect.stringContaining(message)); + expect(exit).toHaveBeenCalledWith(1); + expect(relay.webhooks.createInbound).not.toHaveBeenCalled(); + expect(relayfile.createWebhookSubscription).not.toHaveBeenCalled(); + }); + it('retires an orphaned, unbound legacy webhook after the new binding is live', async () => { const relay = createRelayMock({ inboundWebhooks: [ diff --git a/packages/cli/src/cli/commands/integration.ts b/packages/cli/src/cli/commands/integration.ts index b3e21f012..915018d04 100644 --- a/packages/cli/src/cli/commands/integration.ts +++ b/packages/cli/src/cli/commands/integration.ts @@ -445,11 +445,29 @@ async function createRelayfileInboundTarget( : `relaycast returned ${response.status}`; throw new Error(`Could not create relayfile inbound target: ${message}`); } + return parseRelayfileInboundTargetResponse(body); +} + +function parseRelayfileInboundTargetResponse(body: unknown): { url: string; secret: string } { const data = isRecord(body) && isRecord(body.data) ? body.data : body; if (!isRecord(data) || typeof data.url !== 'string' || typeof data.secret !== 'string') { throw new Error('relaycast returned an invalid relayfile inbound target response'); } - return { url: data.url.trim(), secret: data.secret.trim() }; + const url = data.url.trim(); + const secret = data.secret.trim(); + if (!url || !secret) { + throw new Error('relaycast returned an invalid relayfile inbound target response'); + } + let parsedUrl: URL; + try { + parsedUrl = new URL(url); + } catch { + throw new Error('relaycast returned an invalid relayfile inbound target URL'); + } + if (parsedUrl.protocol !== 'https:') { + throw new Error('relaycast returned a non-https relayfile inbound target URL'); + } + return { url: parsedUrl.toString(), secret }; } function resolveInboundTargetBaseUrl(options: SdkClientOptions): string { From d0f6fb9e23fd0f21a7c7143d2e0d929b0f62c675 Mon Sep 17 00:00:00 2001 From: Proactive Runtime Bot Date: Wed, 8 Jul 2026 11:04:34 +0200 Subject: [PATCH 6/8] test(cli): cover subscribe without workspace key --- .../commands/integration-subscribe.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/cli/src/cli/commands/integration-subscribe.test.ts b/packages/cli/src/cli/commands/integration-subscribe.test.ts index a58920f30..45f91f92f 100644 --- a/packages/cli/src/cli/commands/integration-subscribe.test.ts +++ b/packages/cli/src/cli/commands/integration-subscribe.test.ts @@ -1,4 +1,7 @@ import { Command } from 'commander'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { @@ -225,6 +228,28 @@ describe('integration subscribe', () => { ); }); + it('fails loudly before provisioning when no workspace key is available', async () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'relay-no-workspace-')); + vi.stubEnv('AGENT_RELAY_HOME', home); + vi.stubEnv('RELAY_WORKSPACE_KEY', ''); + vi.stubEnv('RELAY_API_KEY', ''); + const { program, relay, relayfile, error, exit } = harness({ + resolveLocalRelayOptions: async () => undefined, + }); + + try { + await program.parseAsync(ARGS(), { from: 'user' }); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + + expect(error).toHaveBeenCalledWith(expect.stringContaining('No workspace key found')); + expect(exit).toHaveBeenCalledWith(1); + expect(fetch).not.toHaveBeenCalled(); + expect(relay.webhooks.createInbound).not.toHaveBeenCalled(); + expect(relayfile.createWebhookSubscription).not.toHaveBeenCalled(); + }); + it.each([ ['blank URL', { url: ' ', secret: 'inbound-secret' }, 'invalid relayfile inbound target response'], [ From 066e90cc30ed2e45896e12a28af24f79d770e5c1 Mon Sep 17 00:00:00 2001 From: Proactive Runtime Bot Date: Wed, 8 Jul 2026 11:21:54 +0200 Subject: [PATCH 7/8] test(cli): restore env stubs after subscribe tests --- packages/cli/src/cli/commands/integration-subscribe.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/cli/commands/integration-subscribe.test.ts b/packages/cli/src/cli/commands/integration-subscribe.test.ts index 45f91f92f..a6d5779cc 100644 --- a/packages/cli/src/cli/commands/integration-subscribe.test.ts +++ b/packages/cli/src/cli/commands/integration-subscribe.test.ts @@ -11,6 +11,7 @@ import { } from './integration.js'; afterEach(() => { + vi.unstubAllEnvs(); vi.unstubAllGlobals(); }); From efb19ada80fb278e2a150bb7d2f98732626cbe6d Mon Sep 17 00:00:00 2001 From: Proactive Runtime Bot Date: Wed, 8 Jul 2026 21:12:20 +0200 Subject: [PATCH 8/8] chore: bump relayfile client for webhook subscriptions --- package-lock.json | 20 ++++++++-------- packages/cli/package.json | 2 +- packages/cli/src/cli/commands/integration.ts | 24 ++------------------ 3 files changed, 13 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index b2c22f6c7..95636b93d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3092,6 +3092,15 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/@relayfile/client": { + "version": "0.10.20", + "resolved": "https://registry.npmjs.org/@relayfile/client/-/client-0.10.20.tgz", + "integrity": "sha512-cMNFX4Pq/y7LAB8kQxWMwvekkznD0Og4IK3XrJni8WZnaV7JxmgeW9BjX3NI7topVqYx32vFkDIslgggOoWd5g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@relayfile/core": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@relayfile/core/-/core-0.8.10.tgz", @@ -10094,7 +10103,7 @@ "@agent-relay/utils": "9.2.2", "@modelcontextprotocol/sdk": "^1.0.0", "@relaycast/sdk": "^5.0.5", - "@relayfile/client": "^0.10.19", + "@relayfile/client": "^0.10.20", "@relayflows/cli": "^1.0.1", "@xterm/headless": "^6.0.0", "commander": "^12.1.0", @@ -10153,15 +10162,6 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "packages/cli/node_modules/@relayfile/client": { - "version": "0.10.19", - "resolved": "https://registry.npmjs.org/@relayfile/client/-/client-0.10.19.tgz", - "integrity": "sha512-1hoaPaMc/jB2sMq/a5gbEZ7vGHPAx8WaN23Jr03h8DTowmCeoqwu+cliMWRASo90VmdatJyfjTzQc8csHIXL2g==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "packages/cloud": { "name": "@agent-relay/cloud", "version": "9.2.2", diff --git a/packages/cli/package.json b/packages/cli/package.json index 473198161..49b93f70b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,7 +51,7 @@ "@agent-relay/utils": "9.2.2", "@modelcontextprotocol/sdk": "^1.0.0", "@relaycast/sdk": "^5.0.5", - "@relayfile/client": "^0.10.19", + "@relayfile/client": "^0.10.20", "@relayflows/cli": "^1.0.1", "@xterm/headless": "^6.0.0", "commander": "^12.1.0", diff --git a/packages/cli/src/cli/commands/integration.ts b/packages/cli/src/cli/commands/integration.ts index 915018d04..e87d11454 100644 --- a/packages/cli/src/cli/commands/integration.ts +++ b/packages/cli/src/cli/commands/integration.ts @@ -245,30 +245,10 @@ export function defaultRelayfileBridge(options?: RelayfileClientOptions): Relayf } }, async createWebhookSubscription(input) { - const extendedClient = client as RelayfileControlPlaneClient & { - createWebhookSubscription?: (input: { - url: string; - pathGlobs: string[]; - secret: string; - }) => Promise; - }; - if (!extendedClient.createWebhookSubscription) { - throw new Error( - 'relayfile control-plane does not support webhook subscriptions yet. Update relayfile and re-run `agent-relay integration subscribe`.' - ); - } - return extendedClient.createWebhookSubscription(input); + return client.createWebhookSubscription(input); }, async deleteWebhookSubscription(subscriptionId) { - const extendedClient = client as RelayfileControlPlaneClient & { - deleteWebhookSubscription?: (subscriptionId: string) => Promise; - }; - if (!extendedClient.deleteWebhookSubscription) { - throw new Error( - 'relayfile control-plane does not support deleting webhook subscriptions yet. Update relayfile and retry cleanup.' - ); - } - await extendedClient.deleteWebhookSubscription(subscriptionId); + await client.deleteWebhookSubscription(subscriptionId); }, }; }