agent-relay-runner 0.90.0 → 0.91.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.90.0",
3
+ "version": "0.91.1",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "directory": "runner"
21
21
  },
22
22
  "dependencies": {
23
- "agent-relay-sdk": "0.2.69"
23
+ "agent-relay-sdk": "0.2.70"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "latest",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.90.0",
4
+ "version": "0.91.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
package/src/adapter.ts CHANGED
@@ -100,6 +100,10 @@ export interface RunnerSpawnConfig {
100
100
  // Stage 2 (#215): the MCP endpoint the agent connects to — the runner-local proxy URL when the
101
101
  // proxy is active. Undefined → the adapter targets the relay's MCP endpoint directly (Stage 1).
102
102
  relayMcpEndpoint?: string;
103
+ // #672: the shared host-listener URL the per-agent `callmux bridge` connects to, when the
104
+ // profile/spawn opted into `agentProfile.sharedMcp`. Resolved runner-side (env → default);
105
+ // the launch assembler only emits the bridge entry when sharedMcp is on.
106
+ sharedMcpUrl?: string;
103
107
  monitor?: {
104
108
  deliver(messages: Message[]): Promise<number[]>;
105
109
  };
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { resolve, join } from "node:path";
3
- import type { AgentProfile, AgentProfileBase, SpawnProvider } from "agent-relay-sdk";
3
+ import type { AgentProfile, AgentProfileBase, ProvisioningMcpServer, SpawnProvider } from "agent-relay-sdk";
4
4
  import {
5
5
  profileAllowsRelayFeature,
6
6
  profileIsVanillaBase,
@@ -20,8 +20,11 @@ import {
20
20
  claudeMcpLaunchArgs,
21
21
  codexProvisionedMcpArgs,
22
22
  relayMcpCodexConfigArgs,
23
+ resolveSharedMcpUrl,
24
+ sharedMcpBridgeServer,
23
25
  tomlString,
24
26
  RELAY_MCP_SERVER_NAME,
27
+ SHARED_MCP_SERVER_NAME,
25
28
  } from "./relay-mcp";
26
29
 
27
30
  // #557 — THE single launch-assembly contract. Given a resolved AgentProfile (+ its
@@ -84,6 +87,14 @@ export interface AssembledMcp {
84
87
  * the ambient host MCP (callmux/qmd/gh) instead of dropping it. Empty for non-host mcp
85
88
  * modes, isolated bases (own config home, never had host MCP), and Codex. */
86
89
  hostServers: string[];
90
+ /** #672 — when the profile/spawn opted into `sharedMcp`, the agent's NON-IDENTITY MCP
91
+ * (tokenlean, github) is routed through one orchestrator-owned shared `callmux` listener via
92
+ * a per-agent `callmux bridge --cwd "$WORKTREE"` (session-cwd isolation), injected through
93
+ * the same `--mcp-config` / `-c` channel so it survives strict. `url` is the listener the
94
+ * bridge connects to. Undefined when projection is OFF (the default). The bridge is opaque
95
+ * at assembly time — its server SET is owned by the listener config, not enumerated here.
96
+ * The identity-bearing relay endpoint is never on the shared listener (#215). */
97
+ sharedListener?: { url: string };
87
98
  }
88
99
 
89
100
  export interface AssembledLaunch {
@@ -220,7 +231,21 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
220
231
  ),
221
232
  );
222
233
  const projectServers = Object.keys(projectMcp);
223
- const declaredMcp = { ...projectMcp, ...provisionedMcp };
234
+
235
+ // #672 — shared host-listener MCP (opt-in, default OFF). When on, route the agent's
236
+ // NON-IDENTITY MCP (tokenlean, github) through ONE orchestrator-owned shared `callmux`
237
+ // listener via a per-agent `callmux bridge --url <listener> --cwd "$WORKTREE"` stdio entry:
238
+ // the MCP session survives a listener/downstream restart, and --cwd gives each isolated
239
+ // worktree session-cwd isolation. It's a plain stdio server descriptor, so it rides the same
240
+ // controlled --mcp-config channel as provisioned servers (survives --strict-mcp-config) — no
241
+ // ambient discovery, no modal. Relay-owned infra → authoritative on its name (spread last);
242
+ // the relay endpoint stays per-agent (#215), NEVER on the shared listener.
243
+ const sharedListenerOn = Boolean(config.agentProfile?.sharedMcp);
244
+ const sharedListenerUrl = sharedListenerOn ? resolveSharedMcpUrl(config.sharedMcpUrl) : undefined;
245
+ const sharedMcp: Record<string, ProvisioningMcpServer> = sharedListenerUrl
246
+ ? { [SHARED_MCP_SERVER_NAME]: sharedMcpBridgeServer(sharedListenerUrl, config.cwd) }
247
+ : {};
248
+ const declaredMcp = { ...projectMcp, ...provisionedMcp, ...sharedMcp };
224
249
 
225
250
  // #662 — EVERY managed Claude launch is --strict-mcp-config, not just vanilla. A managed
226
251
  // agent runs headless (no human at the keyboard), so Claude's blocking "N new MCP servers
@@ -278,7 +303,10 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
278
303
  statusLine,
279
304
  skills: skills.map((s) => ({ name: s.name, dir: s.dir })),
280
305
  plugins: provisionedPlugins.map((p) => ({ name: p.name, dir: p.dir })),
281
- mcp: { relayEndpoint, strict: strictMcp, servers: mcpServers, projectServers, hostServers: hostMcpServers },
306
+ mcp: {
307
+ relayEndpoint, strict: strictMcp, servers: mcpServers, projectServers, hostServers: hostMcpServers,
308
+ ...(sharedListenerUrl ? { sharedListener: { url: sharedListenerUrl } } : {}),
309
+ },
282
310
  instructions,
283
311
  args,
284
312
  env: configHome ? { CLAUDE_CONFIG_DIR: configHome } : {},
@@ -308,7 +336,18 @@ function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfi
308
336
  ),
309
337
  );
310
338
  const projectServers = Object.keys(projectMcp);
311
- const declaredMcp = { ...projectMcp, ...provisionedMcp };
339
+
340
+ // #672 — shared host-listener MCP (opt-in, default OFF), same as Claude: a per-agent
341
+ // `callmux bridge --url <listener> --cwd "$WORKTREE"` stdio entry routes the agent's
342
+ // NON-IDENTITY MCP (tokenlean, github) through the shared listener (session survives a
343
+ // restart; --cwd gives session-cwd isolation). Emitted via the same `-c mcp_servers.*`
344
+ // overrides as provisioned servers. The relay endpoint stays per-agent (#215).
345
+ const sharedListenerOn = Boolean(config.agentProfile?.sharedMcp);
346
+ const sharedListenerUrl = sharedListenerOn ? resolveSharedMcpUrl(config.sharedMcpUrl) : undefined;
347
+ const sharedMcp: Record<string, ProvisioningMcpServer> = sharedListenerUrl
348
+ ? { [SHARED_MCP_SERVER_NAME]: sharedMcpBridgeServer(sharedListenerUrl, config.cwd) }
349
+ : {};
350
+ const declaredMcp = { ...projectMcp, ...provisionedMcp, ...sharedMcp };
312
351
 
313
352
  const instructions = assembleInstructions(config);
314
353
 
@@ -333,7 +372,10 @@ function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfi
333
372
  statusLine: false,
334
373
  skills: provisionedSkills.map((s) => ({ name: s.name, dir: s.dir })),
335
374
  plugins: [],
336
- mcp: { relayEndpoint, strict: false, servers: mcpServers, projectServers, hostServers: [] },
375
+ mcp: {
376
+ relayEndpoint, strict: false, servers: mcpServers, projectServers, hostServers: [],
377
+ ...(sharedListenerUrl ? { sharedListener: { url: sharedListenerUrl } } : {}),
378
+ },
337
379
  instructions,
338
380
  args,
339
381
  env: configHome ? { CODEX_HOME: configHome } : {},
@@ -34,6 +34,7 @@ export function agentProfileProjectionReport(input: AgentProfileProjectionInput)
34
34
  add(provisioningPluginsEntry(assembled));
35
35
  add(provisioningMcpEntry(assembled));
36
36
  add(projectMcpEntry(assembled));
37
+ add(sharedMcpEntry(assembled));
37
38
  if (profile) add(filesystemEntry(profile));
38
39
  add(configHomeEntry(assembled));
39
40
  if (assembled.vanilla) add(vanillaLaunchEntry(provider));
@@ -158,6 +159,19 @@ function projectMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
158
159
  : `Project .mcp.json servers emitted as -c mcp_servers.* overrides (${names.join(", ")}).`);
159
160
  }
160
161
 
162
+ // #672 — shared host-listener MCP provenance. `not-applicable` when the profile/spawn didn't
163
+ // opt in (the default); `applied` reports the per-agent `callmux bridge` routing the agent's
164
+ // non-identity MCP (tokenlean, github) through the shared listener it points at. The server SET
165
+ // is owned by the listener config, so this surfaces the route + URL, not an enumerated list.
166
+ function sharedMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
167
+ const optedIn = Boolean(a.profile?.sharedMcp);
168
+ const listener = a.mcp.sharedListener;
169
+ if (!optedIn || !listener) return notApplicable("sharedMcp", "off", "Shared host-listener MCP is off (opt in via the sharedMcp profile/spawn option); non-identity MCP is injected per-agent.");
170
+ return applied("sharedMcp", "on", a.provider === "claude"
171
+ ? `Non-identity MCP (e.g. tokenlean, github) routed through the shared callmux listener (${listener.url}) via a per-agent 'callmux bridge --cwd "$WORKTREE"', injected into --mcp-config (survives --strict-mcp-config); session survives a listener restart, session-cwd isolated. Relay endpoint stays per-agent (#215).`
172
+ : `Non-identity MCP (e.g. tokenlean, github) routed through the shared callmux listener (${listener.url}) via a per-agent 'callmux bridge --cwd "$WORKTREE"' emitted as -c mcp_servers.* overrides; session survives a listener restart, session-cwd isolated. Relay endpoint stays per-agent (#215).`);
173
+ }
174
+
161
175
  function globalInstructionsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
162
176
  const disposition = a.instructions.global;
163
177
  if (disposition === "allow") return applied("instructions.global", "allow", "Host/global provider instructions are left available.");
package/src/relay-mcp.ts CHANGED
@@ -19,6 +19,24 @@ export const RELAY_MCP_SERVER_NAME = "agent-relay";
19
19
  export const RELAY_MCP_PATH = "/api/mcp";
20
20
  export const RELAY_MCP_TOKEN_ENV = "AGENT_RELAY_SESSION_TOKEN";
21
21
 
22
+ // #672 — shared host-listener MCP. The agent connects to ONE orchestrator-owned shared
23
+ // `callmux` listener via a per-agent `callmux bridge`, fronting the host's NON-IDENTITY MCP
24
+ // (tokenlean, github). The bridge is a stdio child (auto-reconnect + retry-once on a
25
+ // listener/downstream hiccup; `--cwd` injects `x-callmux-cwd` for per-worktree session-cwd
26
+ // isolation), so it rides the SAME provider injection as any provisioned stdio server. The
27
+ // identity-bearing relay endpoint is NEVER routed here — it stays per-agent on #215.
28
+ export const SHARED_MCP_SERVER_NAME = "callmux";
29
+ // callmux's documented manual-listener endpoint (`callmux --listen 4860`, Streamable HTTP at
30
+ // /mcp). In managed launches the orchestrator owns the real listener URL and passes it through
31
+ // AGENT_RELAY_SHARED_MCP_URL; this fallback is for direct runner/manual-listener use.
32
+ export const DEFAULT_SHARED_MCP_URL = "http://localhost:4860/mcp";
33
+ export const SHARED_MCP_URL_ENV = "AGENT_RELAY_SHARED_MCP_URL";
34
+
35
+ /** Resolve the shared-listener URL: explicit runner option → env → documented default. */
36
+ export function resolveSharedMcpUrl(explicit?: string): string {
37
+ return explicit ?? process.env[SHARED_MCP_URL_ENV] ?? DEFAULT_SHARED_MCP_URL;
38
+ }
39
+
22
40
  export function relayMcpEndpoint(relayUrl: string): string {
23
41
  return `${relayUrl.replace(/\/+$/, "")}${RELAY_MCP_PATH}`;
24
42
  }
@@ -51,6 +69,13 @@ export function tomlString(value: string): string {
51
69
  // strict then preserves exactly that set and nothing else.
52
70
  import type { ProvisioningMcpServer } from "agent-relay-sdk";
53
71
 
72
+ // The shared-listener bridge as a provider-neutral stdio server descriptor: a `callmux bridge`
73
+ // child fronting the host's shared listener, with per-agent `--cwd "$WORKTREE"` for session-cwd
74
+ // isolation (#672). It flows through the existing claude/codex stdio injection unchanged.
75
+ export function sharedMcpBridgeServer(url: string, cwd: string): ProvisioningMcpServer {
76
+ return { command: "callmux", args: ["bridge", "--url", url, "--cwd", cwd] };
77
+ }
78
+
54
79
  function claudeMcpServerEntry(server: ProvisioningMcpServer): Record<string, unknown> {
55
80
  const entry: Record<string, unknown> = {};
56
81
  if (server.type) entry.type = server.type;
@@ -17,7 +17,7 @@ import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureR
17
17
  import { Outbox, type OutboxRecord } from "./outbox";
18
18
  import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, countTranscriptEntries, extractFinalAssistantMessage, extractFinalAssistantMessageAfterEntry, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
19
19
  import { profileUsesHostProviderGlobals } from "./profile-home";
20
- import { RELAY_MCP_TOKEN_ENV, relayMcpEndpoint } from "./relay-mcp";
20
+ import { RELAY_MCP_TOKEN_ENV, relayMcpEndpoint, resolveSharedMcpUrl } from "./relay-mcp";
21
21
  import { RelayMcpProxy } from "./relay-mcp-proxy";
22
22
  import { runtimeMetadata } from "./version";
23
23
  import { logger, parseLogLevel } from "./logger";
@@ -581,6 +581,9 @@ export class AgentRunner {
581
581
  // Stage 2 (#215): the MCP endpoint the agent's client should target — the runner-local
582
582
  // proxy when active, undefined when disabled (adapters fall back to the direct relay URL).
583
583
  ...(this.mcpProxyEndpoint ? { relayMcpEndpoint: this.mcpProxyEndpoint } : {}),
584
+ // #672: the shared host-listener URL the `callmux bridge` connects to (env → default).
585
+ // Always resolved; the assembler only emits the bridge when `agentProfile.sharedMcp` is on.
586
+ sharedMcpUrl: resolveSharedMcpUrl(),
584
587
  monitor: {
585
588
  deliver: (messages) => this.control!.deliverToMonitor(messages),
586
589
  },