-
Notifications
You must be signed in to change notification settings - Fork 0
feat(persona): wire relaycast MCP into broker-launched personas #170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,7 @@ import { | |
| type PersonaSelection, | ||
| type PersonaSpec, | ||
| type PersonaTag, | ||
| type RelayMcpConfig, | ||
| type SidecarMdMode, | ||
| type SkillMaterializationPlan | ||
| } from '@agentworkforce/persona-kit'; | ||
|
|
@@ -492,6 +493,33 @@ function emitDropWarnings(lines: string[]): void { | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Detect that we're launching under an Agent Relay broker and, if so, build the | ||
| * relaycast wiring to inject into the harness's MCP config so the persona can | ||
| * message the team — the same capability a non-persona broker spawn gets. | ||
| * | ||
| * The broker sets `RELAY_API_KEY` (+ optional `RELAY_BASE_URL` / | ||
| * `RELAY_DEFAULT_WORKSPACE`) on every worker child, and the broker-assigned | ||
| * worker name on `RELAY_AGENT_NAME`. Both the key and the name are required: | ||
| * the name must match what the broker routes to so the relaycast identity and | ||
| * the PTY worker are the same agent. Absent either, we're not under a broker | ||
| * (or it's too old to expose the name) and return undefined — a plain | ||
| * `agentworkforce agent` run is unaffected. | ||
| */ | ||
| function resolveRelayMcpFromEnv(env: NodeJS.ProcessEnv): RelayMcpConfig | undefined { | ||
| const apiKey = env.RELAY_API_KEY?.trim(); | ||
| const agentName = env.RELAY_AGENT_NAME?.trim(); | ||
| if (!apiKey || !agentName) return undefined; | ||
| const baseUrl = env.RELAY_BASE_URL?.trim(); | ||
| const defaultWorkspace = env.RELAY_DEFAULT_WORKSPACE?.trim(); | ||
| return { | ||
| apiKey, | ||
| agentName, | ||
| ...(baseUrl ? { baseUrl } : {}), | ||
| ...(defaultWorkspace ? { defaultWorkspace } : {}) | ||
| }; | ||
| } | ||
|
|
||
| function signalExitCode(signal: NodeJS.Signals | null): number { | ||
| if (!signal) return 0; | ||
| const num = (constants.signals as Record<string, number | undefined>)[signal]; | ||
|
|
@@ -1343,13 +1371,15 @@ function runDryRun(selection: PersonaSelection): number { | |
| ); | ||
| let spec: InteractiveSpec; | ||
| try { | ||
| const relayMcp = resolveRelayMcpFromEnv(process.env); | ||
| spec = buildInteractiveSpec({ | ||
| harness, | ||
| personaId, | ||
| model, | ||
| systemPrompt, | ||
| harnessSettings, | ||
| mcpServers: mcpResolution.servers, | ||
| ...(relayMcp ? { relayMcp } : {}), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The launch summary can become misleading here: relaycast may be injected via Prompt for AI agents |
||
| permissions: effectiveSelection.permissions | ||
| }); | ||
| } catch (err) { | ||
|
|
@@ -1718,13 +1748,15 @@ async function runInteractive( | |
| } | ||
| } | ||
|
|
||
| const relayMcp = resolveRelayMcpFromEnv(process.env); | ||
| const spec = buildInteractiveSpec({ | ||
| harness, | ||
| personaId, | ||
| model, | ||
| systemPrompt, | ||
| harnessSettings, | ||
| mcpServers: resolvedMcp, | ||
| ...(relayMcp ? { relayMcp } : {}), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
+1751
to
+1759
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Include injected relaycast in the sanitized spawn summary. The broker path now injects relaycast into 🩹 Proposed fix const relayMcp = resolveRelayMcpFromEnv(process.env);
+ const summaryMcpServerNames = Object.keys(
+ relayMcp ? { relaycast: true, ...(resolvedMcp ?? {}) } : (resolvedMcp ?? {})
+ );
const spec = buildInteractiveSpec({
harness,
personaId,
model,
systemPrompt,
@@
const summary: string[] = [`model=${model}`];
if (harness === 'claude') {
- const servers = Object.keys(resolvedMcp ?? {});
- summary.push(`mcp-strict=${servers.length ? servers.join(',') : '(none)'}`);
+ summary.push(
+ `mcp-strict=${summaryMcpServerNames.length ? summaryMcpServerNames.join(',') : '(none)'}`
+ );
if (effectiveSelection.permissions?.allow?.length) {
summary.push(`allow=${effectiveSelection.permissions.allow.length} rule(s)`);
}🤖 Prompt for AI Agents |
||
| permissions: effectiveSelection.permissions, | ||
| ...(installRoot !== undefined ? { pluginDirs: [installRoot] } : {}) | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,36 @@ export interface InteractiveSpec { | |
| configFiles: InteractiveConfigFile[]; | ||
| } | ||
|
|
||
| /** | ||
| * Relaycast wiring for a persona launched under an Agent Relay broker. When | ||
| * present, {@link buildInteractiveSpec} injects a `relaycast` MCP server into | ||
| * the harness's MCP config so the persona can message the team — the same | ||
| * capability a non-persona broker spawn gets automatically. | ||
| * | ||
| * Personas otherwise can't reach relaycast: the broker wires its MCP by | ||
| * recognizing the harness CLI it spawns, but a persona's PTY command is the | ||
| * `agentworkforce` launcher (not the harness), and the claude branch emits | ||
| * `--strict-mcp-config`, so a project `.mcp.json` is ignored. Injecting here — | ||
| * into the same `--mcp-config` payload the harness already receives — is the | ||
| * only path that survives strict mode. Callers populate this from the | ||
| * `RELAY_*` env the broker sets on the launcher process (see | ||
| * {@link buildRelaycastMcpServer}). | ||
| */ | ||
| export interface RelayMcpConfig { | ||
| /** Relaycast API key (`RELAY_API_KEY`). */ | ||
| apiKey: string; | ||
| /** | ||
| * Broker-assigned worker name (`RELAY_AGENT_NAME`). Must match the name the | ||
| * broker routes messages to, so the relaycast identity and the PTY worker | ||
| * are the same agent. Registered strictly (`RELAY_STRICT_AGENT_NAME=1`). | ||
| */ | ||
| agentName: string; | ||
| /** Relaycast base URL (`RELAY_BASE_URL`); omitted ⇒ MCP server's default. */ | ||
| baseUrl?: string; | ||
| /** Default workspace id/name (`RELAY_DEFAULT_WORKSPACE`). */ | ||
| defaultWorkspace?: string; | ||
| } | ||
|
|
||
| export interface BuildInteractiveSpecInput { | ||
| harness: Harness; | ||
| /** | ||
|
|
@@ -58,6 +88,14 @@ export interface BuildInteractiveSpecInput { | |
| systemPrompt: string; | ||
| /** Env-resolved MCP servers (pass the output of `resolveMcpServersLenient().servers`). */ | ||
| mcpServers?: Record<string, McpServerSpec>; | ||
| /** | ||
| * When set, a `relaycast` MCP server is merged into {@link mcpServers} so a | ||
| * persona running under an Agent Relay broker can talk to the team. A | ||
| * persona-declared server literally named `relaycast` takes precedence (it | ||
| * is not overwritten). Wired for claude and codex; opencode still warns that | ||
| * MCP injection is unsupported. | ||
| */ | ||
| relayMcp?: RelayMcpConfig; | ||
| permissions?: PersonaPermissions; | ||
| harnessSettings?: HarnessSettings; | ||
| /** | ||
|
|
@@ -186,17 +224,41 @@ function appendCodexMcpServerArgs( | |
| * The opencode branch emits a warning if the persona declares `mcpServers` | ||
| * or `permissions` — those features aren't wired for opencode yet. | ||
| */ | ||
| /** | ||
| * Build the stdio MCP server spec for relaycast, mirroring the env block the | ||
| * broker injects for a recognized harness (`npx -y @relaycast/mcp` + `RELAY_*`). | ||
| * The agent token is intentionally omitted: the relaycast MCP auto-mints one | ||
| * from `RELAY_API_KEY` + the strict agent name, which is the recommended path. | ||
| */ | ||
| function buildRelaycastMcpServer(relay: RelayMcpConfig): McpServerSpec { | ||
| const env: Record<string, string> = { | ||
| RELAY_API_KEY: relay.apiKey, | ||
| RELAY_AGENT_NAME: relay.agentName, | ||
| RELAY_AGENT_TYPE: 'agent', | ||
| RELAY_STRICT_AGENT_NAME: '1' | ||
| }; | ||
| if (relay.baseUrl) env.RELAY_BASE_URL = relay.baseUrl; | ||
| if (relay.defaultWorkspace) env.RELAY_DEFAULT_WORKSPACE = relay.defaultWorkspace; | ||
| return { type: 'stdio', command: 'npx', args: ['-y', '@relaycast/mcp'], env }; | ||
| } | ||
|
|
||
| export function buildInteractiveSpec(input: BuildInteractiveSpecInput): InteractiveSpec { | ||
| const { | ||
| harness, | ||
| personaId, | ||
| model, | ||
| systemPrompt, | ||
| mcpServers, | ||
| permissions, | ||
| harnessSettings, | ||
| pluginDirs | ||
| } = input; | ||
| // Merge the relaycast server into the persona's declared servers when running | ||
| // under a broker. A persona-declared `relaycast` wins, so authors can still | ||
| // override it. Kept pure: callers pass relayMcp explicitly (resolved from | ||
| // env), so this function reads no environment itself. | ||
| const mcpServers = input.relayMcp | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Relay-injected MCP servers now trigger an opencode warning that incorrectly says the persona declared Prompt for AI agents |
||
| ? { relaycast: buildRelaycastMcpServer(input.relayMcp), ...(input.mcpServers ?? {}) } | ||
| : input.mcpServers; | ||
| const warnings: string[] = []; | ||
| const hasPluginDirs = pluginDirs !== undefined && pluginDirs.length > 0; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using conditional object spreading for optional properties like
baseUrlanddefaultWorkspace, you can simplify the return object by directly assigning them with a fallback toundefined. Since these properties are optional inRelayMcpConfig,undefinedvalues are perfectly valid and cleaner to read.