agent-relay-runner 0.89.2 → 0.90.0
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.
|
|
3
|
+
"version": "0.90.0",
|
|
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.
|
|
23
|
+
"agent-relay-sdk": "0.2.69"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/launch-assembly.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
resolveClaudeProvisionedPlugins,
|
|
14
14
|
resolveClaudeProvisionedSkills,
|
|
15
15
|
resolveCodexProvisionedSkills,
|
|
16
|
+
resolveProjectMcpServers,
|
|
16
17
|
profileProvisioning,
|
|
17
18
|
} from "./provisioning";
|
|
18
19
|
import {
|
|
@@ -73,6 +74,11 @@ export interface AssembledMcp {
|
|
|
73
74
|
strict: boolean;
|
|
74
75
|
/** Relay-provisioned server names (excludes the relay endpoint itself). */
|
|
75
76
|
servers: string[];
|
|
77
|
+
/** #667 — project `.mcp.json` server names projected from the spawn cwd's committed
|
|
78
|
+
* `.mcp.json` (via `--mcp-config`, so they survive strict). Sourced from the project,
|
|
79
|
+
* not ambient host MCP. Empty unless the profile/spawn opted into `projectMcp`, and a
|
|
80
|
+
* name already declared by relay-provisioned `servers` wins (not double-listed here). */
|
|
81
|
+
projectServers: string[];
|
|
76
82
|
/** #662 — host USER-scope MCP server names (`~/.claude.json` `mcpServers`) re-injected
|
|
77
83
|
* into `--mcp-config` for an `mcp.mode:"host"` profile, so `--strict-mcp-config` keeps
|
|
78
84
|
* the ambient host MCP (callmux/qmd/gh) instead of dropping it. Empty for non-host mcp
|
|
@@ -202,6 +208,20 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
202
208
|
const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
|
|
203
209
|
const relayEndpoint = profileAllowsRelayFeature(config, "mcp");
|
|
204
210
|
|
|
211
|
+
// #667 — project `.mcp.json` projection (opt-in). The spawn cwd's committed servers are
|
|
212
|
+
// injected through the SAME `--mcp-config` channel as relay-provisioned servers, so they
|
|
213
|
+
// survive `--strict-mcp-config` without the ambient project-discovery modal. A relay-
|
|
214
|
+
// provisioned server of the same name wins (the profile is the explicit, relay-owned
|
|
215
|
+
// source); the relay endpoint name is reserved. `declaredMcp` is the union relay injects.
|
|
216
|
+
const projectMcpRaw = resolveProjectMcpServers(config);
|
|
217
|
+
const projectMcp = Object.fromEntries(
|
|
218
|
+
Object.entries(projectMcpRaw).filter(
|
|
219
|
+
([name]) => name !== RELAY_MCP_SERVER_NAME && !(name in provisionedMcp),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
const projectServers = Object.keys(projectMcp);
|
|
223
|
+
const declaredMcp = { ...projectMcp, ...provisionedMcp };
|
|
224
|
+
|
|
205
225
|
// #662 — EVERY managed Claude launch is --strict-mcp-config, not just vanilla. A managed
|
|
206
226
|
// agent runs headless (no human at the keyboard), so Claude's blocking "N new MCP servers
|
|
207
227
|
// found in this project — select to enable" modal (raised on UNDECIDED cwd `.mcp.json`
|
|
@@ -224,7 +244,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
224
244
|
&& (!config.agentProfile || config.agentProfile.mcp.mode === "host");
|
|
225
245
|
const hostMcp = usesHostMcp ? hostUserScopeMcpServers() : {};
|
|
226
246
|
const hostMcpServers = Object.keys(hostMcp).filter(
|
|
227
|
-
(name) => name !== RELAY_MCP_SERVER_NAME && !(name in
|
|
247
|
+
(name) => name !== RELAY_MCP_SERVER_NAME && !(name in declaredMcp),
|
|
228
248
|
);
|
|
229
249
|
|
|
230
250
|
const instructions = assembleInstructions(config);
|
|
@@ -238,7 +258,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
238
258
|
relayUrl: config.relayUrl,
|
|
239
259
|
...(config.relayMcpEndpoint ? { endpoint: config.relayMcpEndpoint } : {}),
|
|
240
260
|
includeRelay: relayEndpoint,
|
|
241
|
-
servers:
|
|
261
|
+
servers: declaredMcp,
|
|
242
262
|
hostServers: hostMcp,
|
|
243
263
|
strict: strictMcp,
|
|
244
264
|
}),
|
|
@@ -258,7 +278,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
258
278
|
statusLine,
|
|
259
279
|
skills: skills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
260
280
|
plugins: provisionedPlugins.map((p) => ({ name: p.name, dir: p.dir })),
|
|
261
|
-
mcp: { relayEndpoint, strict: strictMcp, servers: mcpServers, hostServers: hostMcpServers },
|
|
281
|
+
mcp: { relayEndpoint, strict: strictMcp, servers: mcpServers, projectServers, hostServers: hostMcpServers },
|
|
262
282
|
instructions,
|
|
263
283
|
args,
|
|
264
284
|
env: configHome ? { CLAUDE_CONFIG_DIR: configHome } : {},
|
|
@@ -278,6 +298,18 @@ function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfi
|
|
|
278
298
|
const provisionedMcp = profileProvisioning(config).mcpServers;
|
|
279
299
|
const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
|
|
280
300
|
|
|
301
|
+
// #667 — project `.mcp.json` projection (opt-in), same union rule as Claude: a relay-
|
|
302
|
+
// provisioned server of the same name wins; the relay endpoint name is reserved. Codex has
|
|
303
|
+
// no `.mcp.json` discovery of its own, so this is purely additive (no strict/modal concern).
|
|
304
|
+
const projectMcpRaw = resolveProjectMcpServers(config);
|
|
305
|
+
const projectMcp = Object.fromEntries(
|
|
306
|
+
Object.entries(projectMcpRaw).filter(
|
|
307
|
+
([name]) => name !== RELAY_MCP_SERVER_NAME && !(name in provisionedMcp),
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
const projectServers = Object.keys(projectMcp);
|
|
311
|
+
const declaredMcp = { ...projectMcp, ...provisionedMcp };
|
|
312
|
+
|
|
281
313
|
const instructions = assembleInstructions(config);
|
|
282
314
|
|
|
283
315
|
// The skills/MCP/project-doc block; core launch config (model/approval/listen) stays
|
|
@@ -285,7 +317,7 @@ function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfi
|
|
|
285
317
|
const args = [
|
|
286
318
|
...bundledSkillConfigArgs(skillDirs),
|
|
287
319
|
...(relayEndpoint ? relayMcpCodexConfigArgs(config.relayUrl, config.relayMcpEndpoint) : []),
|
|
288
|
-
...codexProvisionedMcpArgs(
|
|
320
|
+
...codexProvisionedMcpArgs(declaredMcp),
|
|
289
321
|
...codexProjectDocSuppressionArgs(config),
|
|
290
322
|
];
|
|
291
323
|
|
|
@@ -301,7 +333,7 @@ function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfi
|
|
|
301
333
|
statusLine: false,
|
|
302
334
|
skills: provisionedSkills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
303
335
|
plugins: [],
|
|
304
|
-
mcp: { relayEndpoint, strict: false, servers: mcpServers, hostServers: [] },
|
|
336
|
+
mcp: { relayEndpoint, strict: false, servers: mcpServers, projectServers, hostServers: [] },
|
|
305
337
|
instructions,
|
|
306
338
|
args,
|
|
307
339
|
env: configHome ? { CODEX_HOME: configHome } : {},
|
|
@@ -33,6 +33,7 @@ export function agentProfileProjectionReport(input: AgentProfileProjectionInput)
|
|
|
33
33
|
add(provisioningSkillsEntry(assembled));
|
|
34
34
|
add(provisioningPluginsEntry(assembled));
|
|
35
35
|
add(provisioningMcpEntry(assembled));
|
|
36
|
+
add(projectMcpEntry(assembled));
|
|
36
37
|
if (profile) add(filesystemEntry(profile));
|
|
37
38
|
add(configHomeEntry(assembled));
|
|
38
39
|
if (assembled.vanilla) add(vanillaLaunchEntry(provider));
|
|
@@ -143,6 +144,20 @@ function provisioningMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
|
143
144
|
: `Relay-provisioned MCP servers emitted as -c mcp_servers.* overrides (${names.join(", ")}).`);
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
// #667 — project `.mcp.json` projection provenance. `not-applicable` when the profile/spawn
|
|
148
|
+
// didn't opt in (the default); `applied` lists the spawn-cwd servers relay injected through
|
|
149
|
+
// the controlled --mcp-config / -c path (sourced from the project, not ambient host MCP).
|
|
150
|
+
function projectMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
151
|
+
const optedIn = Boolean(a.profile?.projectMcp);
|
|
152
|
+
const names = a.mcp.projectServers;
|
|
153
|
+
if (!optedIn) return notApplicable("projectMcp", "off", "Project .mcp.json projection is off (opt in via the projectMcp profile/spawn option).");
|
|
154
|
+
if (!names.length) return applied("projectMcp", "none", "Project .mcp.json projection is on, but the spawn cwd declared no injectable MCP servers.");
|
|
155
|
+
const label = `${names.length} server${names.length === 1 ? "" : "s"}`;
|
|
156
|
+
return applied("projectMcp", label, a.provider === "claude"
|
|
157
|
+
? `Project .mcp.json servers composed into --mcp-config (${names.join(", ")})${a.mcp.strict ? "; survives --strict-mcp-config" : ""}.`
|
|
158
|
+
: `Project .mcp.json servers emitted as -c mcp_servers.* overrides (${names.join(", ")}).`);
|
|
159
|
+
}
|
|
160
|
+
|
|
146
161
|
function globalInstructionsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
147
162
|
const disposition = a.instructions.global;
|
|
148
163
|
if (disposition === "allow") return applied("instructions.global", "allow", "Host/global provider instructions are left available.");
|
package/src/provisioning.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
4
|
-
import type { ProvisioningAssetFile, ResolvedProvisioning } from "agent-relay-sdk";
|
|
4
|
+
import type { ProvisioningAssetFile, ProvisioningMcpServer, ResolvedProvisioning } from "agent-relay-sdk";
|
|
5
5
|
import type { RunnerSpawnConfig } from "./adapter";
|
|
6
6
|
|
|
7
7
|
// Runner-side materialization of the relay-resolved provisioning bundle
|
|
@@ -24,6 +24,56 @@ export function profileProvisioning(config: Pick<RunnerSpawnConfig, "agentProfil
|
|
|
24
24
|
return { skills: p.skills ?? [], plugins: p.plugins ?? [], mcpServers: p.mcpServers ?? {} };
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// The `mcpServers` keys a Claude `.mcp.json` entry may carry; coerced into the
|
|
28
|
+
// provider-neutral ProvisioningMcpServer shape so the launch assembler projects them the
|
|
29
|
+
// same way it does relay-provisioned servers (Claude `--mcp-config`, Codex `-c mcp_servers.*`).
|
|
30
|
+
function coerceProjectMcpServer(raw: unknown): ProvisioningMcpServer | null {
|
|
31
|
+
if (!raw || typeof raw !== "object") return null;
|
|
32
|
+
const r = raw as Record<string, unknown>;
|
|
33
|
+
const server: ProvisioningMcpServer = {};
|
|
34
|
+
if (r.type === "stdio" || r.type === "http" || r.type === "sse") server.type = r.type;
|
|
35
|
+
if (typeof r.command === "string") server.command = r.command;
|
|
36
|
+
if (Array.isArray(r.args) && r.args.every((a) => typeof a === "string")) server.args = r.args as string[];
|
|
37
|
+
if (r.env && typeof r.env === "object") server.env = r.env as Record<string, string>;
|
|
38
|
+
if (typeof r.url === "string") server.url = r.url;
|
|
39
|
+
if (r.headers && typeof r.headers === "object") server.headers = r.headers as Record<string, string>;
|
|
40
|
+
// Drop an entry that names no transport (neither a command nor a url) — nothing to launch.
|
|
41
|
+
return server.command || server.url ? server : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// #667 — project `.mcp.json` MCP projection. OPT-IN via the resolved profile's `projectMcp`
|
|
45
|
+
// (set per-spawn or by profile, relay-side). Reads the spawn cwd's COMMITTED `.mcp.json` and
|
|
46
|
+
// returns its `mcpServers` as a provider-neutral record, so the launch assembler injects them
|
|
47
|
+
// through the CONTROLLED `--mcp-config` (Claude) / `-c mcp_servers.*` (Codex) path that
|
|
48
|
+
// survives `--strict-mcp-config` — the project's own servers travel with the repo like they do
|
|
49
|
+
// for a human who clones it, WITHOUT enabling Claude's ambient project-discovery trust modal
|
|
50
|
+
// (which wedges headless agents, the reason #662 made every managed launch strict). The runner
|
|
51
|
+
// is the filesystem authority for the spawn cwd (relay may run on another host), so the read
|
|
52
|
+
// lands here. Best-effort: a missing/malformed file (or one with no `mcpServers`) yields {} and
|
|
53
|
+
// never aborts the launch. Returns {} when projection is OFF.
|
|
54
|
+
export function resolveProjectMcpServers(
|
|
55
|
+
config: Pick<RunnerSpawnConfig, "agentProfile" | "cwd">,
|
|
56
|
+
): Record<string, ProvisioningMcpServer> {
|
|
57
|
+
if (!config.agentProfile?.projectMcp) return {};
|
|
58
|
+
const file = join(config.cwd, ".mcp.json");
|
|
59
|
+
if (!existsSync(file)) return {};
|
|
60
|
+
let parsed: unknown;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.warn(`project .mcp.json projection: failed to parse ${file}: ${(err as Error).message}`);
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
const servers = (parsed as { mcpServers?: unknown } | null)?.mcpServers;
|
|
68
|
+
if (!servers || typeof servers !== "object") return {};
|
|
69
|
+
const out: Record<string, ProvisioningMcpServer> = {};
|
|
70
|
+
for (const [name, raw] of Object.entries(servers as Record<string, unknown>)) {
|
|
71
|
+
const server = coerceProjectMcpServer(raw);
|
|
72
|
+
if (server) out[name] = server;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
27
77
|
// A resolved provisioning asset: WHERE it lands (`dir`) and HOW it materializes
|
|
28
78
|
// (`source`). `files` writes the inline set into `dir`; `link` symlinks a pre-existing
|
|
29
79
|
// source dir at `dir`; `inPlace` uses the source dir as-is (no write, dir IS the source).
|
|
@@ -39,6 +39,7 @@ function launchInjectionEvents(input: RunnerLaunchInjectionInput): KeyedRelayInj
|
|
|
39
39
|
if (!supportsLaunchAssembly(input.provider)) return events;
|
|
40
40
|
const assembled = assembleLaunch(input.provider, input.config, input.providerConfig);
|
|
41
41
|
const toolCount = assembled.skills.length + assembled.plugins.length + assembled.mcp.servers.length
|
|
42
|
+
+ assembled.mcp.projectServers.length
|
|
42
43
|
+ (assembled.relaySkills ? 1 : 0)
|
|
43
44
|
+ (assembled.relayPlugin ? 1 : 0)
|
|
44
45
|
+ (assembled.mcp.relayEndpoint ? 1 : 0)
|
|
@@ -52,6 +53,7 @@ function launchInjectionEvents(input: RunnerLaunchInjectionInput): KeyedRelayInj
|
|
|
52
53
|
assembled.skills.length ? `${assembled.skills.length} skill${assembled.skills.length === 1 ? "" : "s"}` : "",
|
|
53
54
|
assembled.plugins.length ? `${assembled.plugins.length} plugin${assembled.plugins.length === 1 ? "" : "s"}` : "",
|
|
54
55
|
assembled.mcp.servers.length ? `${assembled.mcp.servers.length} MCP server${assembled.mcp.servers.length === 1 ? "" : "s"}` : "",
|
|
56
|
+
assembled.mcp.projectServers.length ? `${assembled.mcp.projectServers.length} project MCP server${assembled.mcp.projectServers.length === 1 ? "" : "s"}` : "",
|
|
55
57
|
].filter(Boolean);
|
|
56
58
|
events.push({
|
|
57
59
|
key: "tools",
|