agent-relay-runner 0.88.3 → 0.89.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.88.3",
3
+ "version": "0.89.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.67"
23
+ "agent-relay-sdk": "0.2.68"
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.88.3",
4
+ "version": "0.89.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -1287,7 +1287,10 @@ function codexRelayContextEnabled(process: ManagedProcess): boolean {
1287
1287
  return config ? profileAllowsRelayFeature(config, "context") : true;
1288
1288
  }
1289
1289
 
1290
- function codexLaunchContext(process: ManagedProcess): string | undefined {
1290
+ // Exported for integration coverage (#462): Codex has no `--append-system-prompt`
1291
+ // flag — the composed systemPromptAppend rides in as the launch-context message
1292
+ // prepended to the first turn. This is the Codex twin of Claude's append arg.
1293
+ export function codexLaunchContext(process: ManagedProcess): string | undefined {
1291
1294
  const config = process.meta?.config as RunnerSpawnConfig | undefined;
1292
1295
  const text = config?.systemPromptAppend?.trim();
1293
1296
  if (!text || process.meta?.systemPromptAppendSent) return undefined;
@@ -7,7 +7,7 @@ import {
7
7
  type ProviderConfig,
8
8
  type RunnerSpawnConfig,
9
9
  } from "./adapter";
10
- import { profileUsesHostProviderGlobals, providerHomePathFor } from "./profile-home";
10
+ import { hostUserScopeMcpServers, profileUsesHostProviderGlobals, providerHomePathFor } from "./profile-home";
11
11
  import {
12
12
  materializeResolvedAssets,
13
13
  resolveClaudeProvisionedPlugins,
@@ -66,10 +66,18 @@ export interface AssembledInstructions {
66
66
  export interface AssembledMcp {
67
67
  /** The identity-bearing relay HTTP MCP endpoint is injected. */
68
68
  relayEndpoint: boolean;
69
- /** Only the assembled servers may load (Claude --strict-mcp-config) the vanilla floor. */
69
+ /** Only the assembled servers may load (Claude --strict-mcp-config). #662: true for ALL
70
+ * managed Claude launches so the project `.mcp.json` enable-modal can't wedge a headless
71
+ * agent; relay's endpoint + provisioned servers (via --mcp-config) are preserved. Always
72
+ * false for Codex (no equivalent discovery modal). */
70
73
  strict: boolean;
71
74
  /** Relay-provisioned server names (excludes the relay endpoint itself). */
72
75
  servers: string[];
76
+ /** #662 — host USER-scope MCP server names (`~/.claude.json` `mcpServers`) re-injected
77
+ * into `--mcp-config` for an `mcp.mode:"host"` profile, so `--strict-mcp-config` keeps
78
+ * the ambient host MCP (callmux/qmd/gh) instead of dropping it. Empty for non-host mcp
79
+ * modes, isolated bases (own config home, never had host MCP), and Codex. */
80
+ hostServers: string[];
73
81
  }
74
82
 
75
83
  export interface AssembledLaunch {
@@ -194,6 +202,31 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
194
202
  const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
195
203
  const relayEndpoint = profileAllowsRelayFeature(config, "mcp");
196
204
 
205
+ // #662 — EVERY managed Claude launch is --strict-mcp-config, not just vanilla. A managed
206
+ // agent runs headless (no human at the keyboard), so Claude's blocking "N new MCP servers
207
+ // found in this project — select to enable" modal (raised on UNDECIDED cwd `.mcp.json`
208
+ // servers) wedges the session forever, burning quota. Relay owns MCP: everything relay
209
+ // decides should load is injected via --mcp-config, which strict PRESERVES; strict only
210
+ // blocks what relay DIDN'T inject — above all the cwd project `.mcp.json` (the modal's
211
+ // source), which stays excluded. The relay plugin (monitor + turn hooks) loads via
212
+ // --plugin-dir and ships no MCP server, so it is untouched. Was `vanilla`-only, which left
213
+ // host/minimal/isolated bases exposed to the modal wedge.
214
+ const strictMcp = true;
215
+
216
+ // #662 follow-up — strict drops ALL on-disk config, including the host user-scope
217
+ // `~/.claude.json` MCP that a host-base agent loaded ambiently before strict. That
218
+ // over-reached: it stopped honoring `mcp.mode:"host"`, so default-relay workers/reviewers
219
+ // lost callmux/qmd/gh. Re-inject the host USER-scope servers (NOT the project `.mcp.json`,
220
+ // still excluded) when the launch both uses the host config home AND its mcp mode is host.
221
+ // An isolated/minimal base has its own config home and never had the real host's user MCP,
222
+ // so it stays relay+provisioned only even if a profile sets mode:"host".
223
+ const usesHostMcp = profileUsesHostProviderGlobals(config)
224
+ && (!config.agentProfile || config.agentProfile.mcp.mode === "host");
225
+ const hostMcp = usesHostMcp ? hostUserScopeMcpServers() : {};
226
+ const hostMcpServers = Object.keys(hostMcp).filter(
227
+ (name) => name !== RELAY_MCP_SERVER_NAME && !(name in provisionedMcp),
228
+ );
229
+
197
230
  const instructions = assembleInstructions(config);
198
231
 
199
232
  // Launch order matches the historical claude buildSpawnArgs block exactly so existing
@@ -206,7 +239,8 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
206
239
  ...(config.relayMcpEndpoint ? { endpoint: config.relayMcpEndpoint } : {}),
207
240
  includeRelay: relayEndpoint,
208
241
  servers: provisionedMcp,
209
- strict: vanilla,
242
+ hostServers: hostMcp,
243
+ strict: strictMcp,
210
244
  }),
211
245
  ...(statusLine ? sessionStatusLineSettingsArgs(hostDefaultArgs, config.providerArgs) : []),
212
246
  ...(config.systemPromptAppend ? ["--append-system-prompt", config.systemPromptAppend] : []),
@@ -224,7 +258,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
224
258
  statusLine,
225
259
  skills: skills.map((s) => ({ name: s.name, dir: s.dir })),
226
260
  plugins: provisionedPlugins.map((p) => ({ name: p.name, dir: p.dir })),
227
- mcp: { relayEndpoint, strict: vanilla, servers: mcpServers },
261
+ mcp: { relayEndpoint, strict: strictMcp, servers: mcpServers, hostServers: hostMcpServers },
228
262
  instructions,
229
263
  args,
230
264
  env: configHome ? { CLAUDE_CONFIG_DIR: configHome } : {},
@@ -267,7 +301,7 @@ function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfi
267
301
  statusLine: false,
268
302
  skills: provisionedSkills.map((s) => ({ name: s.name, dir: s.dir })),
269
303
  plugins: [],
270
- mcp: { relayEndpoint, strict: false, servers: mcpServers },
304
+ mcp: { relayEndpoint, strict: false, servers: mcpServers, hostServers: [] },
271
305
  instructions,
272
306
  args,
273
307
  env: configHome ? { CODEX_HOME: configHome } : {},
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { sanitizeFsName } from "agent-relay-sdk/fs-name";
5
+ import type { ProvisioningMcpServer } from "agent-relay-sdk";
5
6
  import { profileAllowsRelayFeature, type RunnerSpawnConfig } from "./adapter";
6
7
  import { CLAUDE_RELAY_MANUAL } from "./relay-instructions";
7
8
  import { providerHomeRootFromEnv } from "./config";
@@ -117,6 +118,20 @@ function readHostClaudeConfig(): Record<string, unknown> | undefined {
117
118
  }
118
119
  }
119
120
 
121
+ // #662 — the host USER-scope MCP servers: the top-level `mcpServers` of `~/.claude.json`.
122
+ // These are the ambient host MCP a host-base Claude auto-loads on EVERY launch when NOT
123
+ // strict (user scope is global across all projects — callmux/qmd/gh on this host). Since
124
+ // the #662 wedge fix makes every managed launch `--strict-mcp-config` (so Claude ignores
125
+ // all on-disk config and never renders the project-`.mcp.json` enable modal), an
126
+ // `mcp.mode:"host"` profile must re-inject these explicitly into `--mcp-config` to keep
127
+ // honoring host mode. Deliberately NOT the project-cwd `.mcp.json` (project scope — the
128
+ // modal's source, which must stay excluded) nor `projects[cwd].mcpServers` (local scope).
129
+ export function hostUserScopeMcpServers(): Record<string, ProvisioningMcpServer> {
130
+ const servers = readHostClaudeConfig()?.mcpServers;
131
+ if (!servers || typeof servers !== "object") return {};
132
+ return servers as Record<string, ProvisioningMcpServer>;
133
+ }
134
+
120
135
  function providerHomePath(provider: "claude" | "codex", config: RunnerSpawnConfig): string {
121
136
  const root = providerHomeRootFromEnv();
122
137
  const profileName = sanitizePathPart(config.agentProfile?.name || config.profile || "profile");
package/src/relay-mcp.ts CHANGED
@@ -43,10 +43,12 @@ export function tomlString(value: string): string {
43
43
  }
44
44
 
45
45
  // #555 — compose the launch MCP set from (the relay endpoint, if relay.mcp) PLUS the
46
- // profile's relay-provisioned servers. One `--mcp-config` JSON carries them all.
47
- // `strict` (vanilla) adds `--strict-mcp-config` so ONLY these load host `.mcp.json`
48
- // / `~/.claude.json` are ignored even if a stray config exists (belt-and-suspenders
49
- // on top of the `--setting-sources "user"` scope that already excludes repo MCP).
46
+ // profile's relay-provisioned servers, PLUS (#662) the host USER-scope servers for an
47
+ // `mcp.mode:"host"` profile. One `--mcp-config` JSON carries them all. `strict` adds
48
+ // `--strict-mcp-config` so ONLY these load Claude reads no on-disk config, so the
49
+ // project-cwd `.mcp.json` is never discovered (its enable modal can't wedge a headless
50
+ // agent). Whatever the assembler decides host mode should keep is injected here explicitly;
51
+ // strict then preserves exactly that set and nothing else.
50
52
  import type { ProvisioningMcpServer } from "agent-relay-sdk";
51
53
 
52
54
  function claudeMcpServerEntry(server: ProvisioningMcpServer): Record<string, unknown> {
@@ -65,9 +67,21 @@ export function claudeMcpLaunchArgs(opts: {
65
67
  endpoint?: string;
66
68
  includeRelay: boolean;
67
69
  servers?: Record<string, ProvisioningMcpServer>;
70
+ /** #662 — host USER-scope MCP servers (`~/.claude.json` `mcpServers`), re-injected for
71
+ * `mcp.mode:"host"` so `--strict-mcp-config` PRESERVES them instead of dropping the
72
+ * ambient host MCP a host-base agent loaded before strict. Lowest precedence: the relay
73
+ * endpoint and relay-provisioned servers win on a name collision (relay decides). */
74
+ hostServers?: Record<string, ProvisioningMcpServer>;
68
75
  strict?: boolean;
69
76
  }): string[] {
70
77
  const mcpServers: Record<string, unknown> = {};
78
+ // Host user-scope servers go in first so relay endpoint + provisioned servers below can
79
+ // override them by name (relay-owned config is authoritative over ambient host MCP).
80
+ for (const [name, server] of Object.entries(opts.hostServers ?? {})) {
81
+ if (name === RELAY_MCP_SERVER_NAME) continue;
82
+ const entry = claudeMcpServerEntry(server);
83
+ if (Object.keys(entry).length) mcpServers[name] = entry;
84
+ }
71
85
  if (opts.includeRelay) {
72
86
  mcpServers[RELAY_MCP_SERVER_NAME] = {
73
87
  type: "http",