From b56ad19c2425693dcfbca73838f20837676fcce1 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 9 Jun 2026 23:28:36 +0200 Subject: [PATCH 1/2] fix(persona): restore agent-relay MCP injection for spawn-modal personas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Persona spawns passed skipRelayPrompt: true to the broker, which not only skips broker-side MCP arg injection (harmless — the agentworkforce wrapper is not a CLI the broker recognizes) but also suppresses the RELAY_AGENT_NAME / RELAY_AGENT_TOKEN env stamps on the worker. The workforce CLI's resolveRelayMcpFromEnv requires RELAY_API_KEY + RELAY_AGENT_NAME, so it silently skipped its own relay-MCP injection and persona harnesses launched without the agent-relay MCP. Drop the flag so the broker stamps the env, and export AGENT_RELAY_BIN to the broker (inherited by workers) so the workforce CLI's `agent-relay-broker mcp-args --register` path resolves the same binary Pear runs instead of a PATH lookup that fails in packaged installs. Co-Authored-By: Claude Fable 5 --- src/main/broker.test.ts | 6 ++++++ src/main/broker.ts | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/broker.test.ts b/src/main/broker.test.ts index 7d0b3758..22b6c67d 100644 --- a/src/main/broker.test.ts +++ b/src/main/broker.test.ts @@ -870,6 +870,12 @@ describe('BrokerManager local + cloud coexistence', () => { expect(local.spawnPty).not.toHaveBeenCalledWith(expect.objectContaining({ args: expect.arrayContaining(['--install-in-repo']) })) + // The workforce CLI only injects the agent-relay MCP into the inner + // harness when the broker stamps RELAY_AGENT_NAME into the worker env, + // and the broker suppresses that stamp when skipRelayPrompt is set. + expect(local.spawnPty).not.toHaveBeenCalledWith(expect.objectContaining({ + skipRelayPrompt: true + })) expect(result).toEqual({ name: 'autonomous-actor', runtime: 'pty', diff --git a/src/main/broker.ts b/src/main/broker.ts index e7ce1f1d..5b5204b2 100644 --- a/src/main/broker.ts +++ b/src/main/broker.ts @@ -1545,6 +1545,13 @@ export class BrokerManager { // Phase 1 of #125: the local broker stays the workspace creator, so the // key is only threaded when explicitly pinned via env. const brokerBinary = await resolveHarnessBrokerBinary(explicitWorkspaceKey) + // Spawned workers inherit the broker's env. AGENT_RELAY_BIN lets the + // workforce CLI's `mcp-args --register` relay-MCP injection find the + // same broker binary Pear runs — a PATH lookup fails in packaged + // installs and can hit a version-skewed global binary in dev. In the + // legacy-shim case binaryPath is the shim, so prefer the real binary. + const agentRelayBinForWorkers = + brokerBinary.env.PEAR_AGENT_RELAY_BROKER_BINARY ?? brokerBinary.binaryPath const opts: AgentRelaySpawnOptions = { cwd, brokerName: name, @@ -1555,6 +1562,7 @@ export class BrokerManager { env: { PATH: augmentedPath(), ...brokerBinary.env, + AGENT_RELAY_BIN: agentRelayBinForWorkers, ...(agentRelayMcpCommand ? { AGENT_RELAY_MCP_COMMAND: agentRelayMcpCommand } : {}) }, onStderr: (line: string) => { @@ -2858,13 +2866,18 @@ export class BrokerManager { (await session.client.listAgents()).map((agent) => agent.name) ) const personaArgs = ['agent', input.personaId] + // No skipRelayPrompt here: the workforce CLI injects the agent-relay MCP + // into the inner harness, but it only does so when the broker stamps + // RELAY_AGENT_NAME (+ RELAY_AGENT_TOKEN) into the worker env — and the + // broker suppresses those stamps when skip_relay_prompt is set. Broker-side + // arg injection stays a no-op either way because `agentworkforce` is not a + // CLI the broker recognizes, so the wrapper command is unchanged. let nextInput: SpawnPtyInput = { name: getAvailableAgentName(input.baseName, existingNames), cli: input.command.cli, args: [...input.command.args, ...personaArgs], cwd: session.cwd, - channels: session.channels, - skipRelayPrompt: true + channels: session.channels } for (let attempt = 0; attempt < 20; attempt += 1) { From 76c2f2cde014b97b882142739dd72c0a7ef5591f Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 9 Jun 2026 23:55:20 +0200 Subject: [PATCH 2/2] refactor(broker): return realBinaryPath from resolveHarnessBrokerBinary Review follow-up: the AGENT_RELAY_BIN value was reverse-engineered from the shim env key (PEAR_AGENT_RELAY_BROKER_BINARY ?? binaryPath); have the resolver own that distinction explicitly instead. Co-Authored-By: Claude Fable 5 --- src/main/broker.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/broker.ts b/src/main/broker.ts index 5b5204b2..efabd494 100644 --- a/src/main/broker.ts +++ b/src/main/broker.ts @@ -424,7 +424,10 @@ function ensureBrokerBinaryCompatShim(): string { return shimPath } -async function resolveHarnessBrokerBinary(workspaceKey?: string): Promise<{ binaryPath: string; env: NodeJS.ProcessEnv }> { +// `binaryPath` is what Pear launches (possibly the legacy compat shim); +// `realBinaryPath` is always the actual broker binary, for callers that hand +// the binary itself to other processes. +async function resolveHarnessBrokerBinary(workspaceKey?: string): Promise<{ binaryPath: string; realBinaryPath: string; env: NodeJS.ProcessEnv }> { const binaryPath = resolveBundledBrokerBinary() const flags = await inspectBrokerInitCliFlags(binaryPath) @@ -435,7 +438,7 @@ async function resolveHarnessBrokerBinary(workspaceKey?: string): Promise<{ bina } if (flags.supportsInstanceName) { - return { binaryPath, env: {} } + return { binaryPath, realBinaryPath: binaryPath, env: {} } } if (!flags.supportsName) { @@ -446,6 +449,7 @@ async function resolveHarnessBrokerBinary(workspaceKey?: string): Promise<{ bina console.warn(`[broker] Broker binary uses legacy --name flag; launching through compatibility shim: ${binaryPath}`) return { binaryPath: shimPath, + realBinaryPath: binaryPath, env: { PEAR_AGENT_RELAY_BROKER_BINARY: binaryPath, PEAR_AGENT_RELAY_BROKER_SUPPORTS_INSTANCE_NAME: flags.supportsInstanceName ? '1' : '0', @@ -1545,13 +1549,6 @@ export class BrokerManager { // Phase 1 of #125: the local broker stays the workspace creator, so the // key is only threaded when explicitly pinned via env. const brokerBinary = await resolveHarnessBrokerBinary(explicitWorkspaceKey) - // Spawned workers inherit the broker's env. AGENT_RELAY_BIN lets the - // workforce CLI's `mcp-args --register` relay-MCP injection find the - // same broker binary Pear runs — a PATH lookup fails in packaged - // installs and can hit a version-skewed global binary in dev. In the - // legacy-shim case binaryPath is the shim, so prefer the real binary. - const agentRelayBinForWorkers = - brokerBinary.env.PEAR_AGENT_RELAY_BROKER_BINARY ?? brokerBinary.binaryPath const opts: AgentRelaySpawnOptions = { cwd, brokerName: name, @@ -1562,7 +1559,13 @@ export class BrokerManager { env: { PATH: augmentedPath(), ...brokerBinary.env, - AGENT_RELAY_BIN: agentRelayBinForWorkers, + // Spawned workers inherit the broker's env. AGENT_RELAY_BIN is the + // ecosystem-standard broker-binary override (harness-driver + // broker-path, workforce runtime relay-mcp); it lets the workforce + // CLI's `mcp-args --register` relay-MCP injection find the same + // broker binary Pear runs — a PATH lookup fails in packaged + // installs and can hit a version-skewed global binary in dev. + AGENT_RELAY_BIN: brokerBinary.realBinaryPath, ...(agentRelayMcpCommand ? { AGENT_RELAY_MCP_COMMAND: agentRelayMcpCommand } : {}) }, onStderr: (line: string) => {