agent-relay-runner 0.90.0 → 0.91.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 +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/adapter.ts +4 -0
- package/src/launch-assembly.ts +47 -5
- package/src/profile-projection.ts +14 -0
- package/src/relay-mcp.ts +25 -0
- package/src/runner-core.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.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.70"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
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
|
};
|
package/src/launch-assembly.ts
CHANGED
|
@@ -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
|
-
|
|
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: {
|
|
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
|
-
|
|
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: {
|
|
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;
|
package/src/runner-core.ts
CHANGED
|
@@ -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
|
},
|