agent-relay-orchestrator 0.112.0 → 0.113.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-orchestrator",
3
- "version": "0.112.0",
3
+ "version": "0.113.0",
4
4
  "description": "Agent Relay orchestrator — manages agent lifecycle across hosts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "agent-relay-providers": "0.104.1",
20
- "agent-relay-sdk": "0.2.95",
20
+ "agent-relay-sdk": "0.2.96",
21
21
  "callmux": "0.23.0"
22
22
  },
23
23
  "devDependencies": {
@@ -15,7 +15,6 @@ export const SHARED_CALLMUX_ENABLE_ENV = "AGENT_RELAY_SHARED_CALLMUX_ENABLE";
15
15
 
16
16
  export const DEFAULT_SHARED_CALLMUX_HOST = "127.0.0.1";
17
17
  export const DEFAULT_SHARED_CALLMUX_PORT = 4861;
18
- export const SHARED_CALLMUX_SERVER_NAMES = ["tokenlean", "github"] as const;
19
18
 
20
19
  const CONFIG_SCHEMA = "callmux/schema.json";
21
20
  const GITHUB_TOOLS = [
@@ -44,6 +43,7 @@ export interface SharedCallmuxSupervisorDeps {
44
43
  clearInterval(timer: Timer): void;
45
44
  setTimeout(fn: () => void, ms: number): Timer;
46
45
  clearTimeout(timer: Timer): void;
46
+ fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
47
47
  log(message: string): void;
48
48
  report(snapshot: SharedCallmuxHealthSnapshot): void;
49
49
  }
@@ -67,7 +67,7 @@ export function sharedCallmuxOptionsFromEnv(env: Record<string, string | undefin
67
67
  url,
68
68
  configPath: env[SHARED_CALLMUX_CONFIG_ENV] || join(agentRelayHome(), "callmux", "shared-listener.json"),
69
69
  sourceConfigPath: env[SHARED_CALLMUX_SOURCE_CONFIG_ENV] || env.CALLMUX_CONFIG || join(homedir(), ".config", "callmux", "config.json"),
70
- enabled: !envOff(env[SHARED_CALLMUX_ENABLE_ENV]),
70
+ enabled: !envOff(env[SHARED_CALLMUX_ENABLE_ENV]),
71
71
  };
72
72
  }
73
73
 
@@ -75,13 +75,12 @@ export function sharedMcpListenerUrl(): string {
75
75
  return sharedCallmuxOptionsFromEnv().url;
76
76
  }
77
77
 
78
- export function writeSharedCallmuxConfig(opts: Pick<SharedCallmuxOptions, "configPath" | "sourceConfigPath">): CallmuxConfig {
78
+ export function writeSharedCallmuxConfig(opts: Pick<SharedCallmuxOptions, "configPath" | "sourceConfigPath"> & { registryServers?: CallmuxConfig["servers"] }): CallmuxConfig {
79
79
  const source = readJsonObject(opts.sourceConfigPath);
80
80
  const sourceServers = isRecord(source.servers) ? source.servers : {};
81
- const servers: CallmuxConfig["servers"] = {
82
- tokenlean: cloneServer(sourceServers.tokenlean) ?? defaultTokenleanServer(),
83
- github: cloneServer(sourceServers.github) ?? defaultGithubServer(),
84
- };
81
+ const servers = opts.registryServers && Object.keys(opts.registryServers).length > 0
82
+ ? cloneServers(opts.registryServers)
83
+ : fallbackSharedServers(sourceServers);
85
84
  const generated: CallmuxConfig = {
86
85
  servers,
87
86
  cacheTtlSeconds: numberFromRecord(source, "cacheTtlSeconds") ?? 10,
@@ -97,6 +96,36 @@ export function writeSharedCallmuxConfig(opts: Pick<SharedCallmuxOptions, "confi
97
96
  return generated;
98
97
  }
99
98
 
99
+ export async function fetchSharedCallmuxRegistryServers(
100
+ config: Pick<OrchestratorConfig, "relayUrl" | "token">,
101
+ deps: Pick<SharedCallmuxSupervisorDeps, "fetch" | "log"> = { fetch: globalThis.fetch.bind(globalThis), log: () => {} },
102
+ ): Promise<CallmuxConfig["servers"] | undefined> {
103
+ try {
104
+ const url = new URL("/api/provisioning/capabilities?kind=mcp", config.relayUrl);
105
+ const headers: Record<string, string> = {};
106
+ if (config.token) headers.Authorization = `Bearer ${config.token}`;
107
+ const res = await deps.fetch(url, { headers });
108
+ if (!res.ok) {
109
+ deps.log(`[orchestrator] Shared callmux registry fetch failed: ${res.status}`);
110
+ return undefined;
111
+ }
112
+ const payload = await res.json().catch(() => null) as unknown;
113
+ const capabilities = isRecord(payload) && Array.isArray(payload.capabilities) ? payload.capabilities : [];
114
+ const servers: CallmuxConfig["servers"] = {};
115
+ for (const capability of capabilities) {
116
+ if (!isRecord(capability) || !Array.isArray(capability.variants)) continue;
117
+ for (const variant of capability.variants) {
118
+ const entry = callmuxServerFromProvisioningVariant(variant);
119
+ if (entry) servers[entry.name] = entry.server;
120
+ }
121
+ }
122
+ return Object.keys(servers).length > 0 ? servers : undefined;
123
+ } catch (err) {
124
+ deps.log(`[orchestrator] Shared callmux registry fetch failed: ${errMessage(err)}`);
125
+ return undefined;
126
+ }
127
+ }
128
+
100
129
  export class SharedCallmuxSupervisor {
101
130
  private listener: ProgrammaticListener | null = null;
102
131
  private readonly onStatus = (snapshot: ListenerHealthSnapshot) => this.reportCallmuxHealth(snapshot);
@@ -159,8 +188,10 @@ export class SharedCallmuxSupervisor {
159
188
  private async open(): Promise<void> {
160
189
  if (this.stopping || this.listener) return;
161
190
  try {
162
- const persistedConfig = writeSharedCallmuxConfig(this.opts);
191
+ const registryServers = await fetchSharedCallmuxRegistryServers(this.config, this.deps);
192
+ const persistedConfig = writeSharedCallmuxConfig({ ...this.opts, ...(registryServers ? { registryServers } : {}) });
163
193
  const config = withRuntimeEnv(persistedConfig, {
194
+ ...process.env,
164
195
  ...this.config.env,
165
196
  [SHARED_MCP_URL_ENV]: this.opts.url,
166
197
  });
@@ -234,6 +265,7 @@ function defaultDeps(): SharedCallmuxSupervisorDeps {
234
265
  clearInterval: (timer) => clearInterval(timer),
235
266
  setTimeout: (fn, ms) => setTimeout(fn, ms),
236
267
  clearTimeout: (timer) => clearTimeout(timer),
268
+ fetch: globalThis.fetch.bind(globalThis),
237
269
  log: (message) => console.error(message),
238
270
  report: (snapshot) => console.error(`[orchestrator] shared callmux status ${snapshot.state}${snapshot.reason ? `: ${snapshot.reason}` : ""}`),
239
271
  };
@@ -262,20 +294,72 @@ function cloneServer(value: unknown): CallmuxConfig["servers"][string] | undefin
262
294
  return JSON.parse(JSON.stringify(value)) as CallmuxConfig["servers"][string];
263
295
  }
264
296
 
265
- function withRuntimeEnv(config: CallmuxConfig, env: Record<string, string>): CallmuxConfig {
266
- const envEntries = Object.entries(env).filter(([, value]) => typeof value === "string");
267
- if (envEntries.length === 0) return config;
268
- const inheritedEnv = Object.fromEntries(envEntries);
297
+ function cloneServers(value: CallmuxConfig["servers"]): CallmuxConfig["servers"] {
298
+ return Object.fromEntries(Object.entries(value).map(([name, server]) => [name, cloneServer(server) ?? server]));
299
+ }
300
+
301
+ function fallbackSharedServers(sourceServers: Record<string, unknown>): CallmuxConfig["servers"] {
302
+ return {
303
+ tokenlean: cloneServer(sourceServers.tokenlean) ?? defaultTokenleanServer(),
304
+ github: cloneServer(sourceServers.github) ?? defaultGithubServer(),
305
+ };
306
+ }
307
+
308
+ function callmuxServerFromProvisioningVariant(value: unknown): { name: string; server: CallmuxConfig["servers"][string] } | null {
309
+ if (!isRecord(value) || value.enabled === false || value.approvalStatus === "pending" || value.validationStatus === "invalid") return null;
310
+ if (typeof value.name !== "string" || !value.name) return null;
311
+ const definition = value.definition;
312
+ if (!isRecord(definition) || definition.kind !== "mcp" || !isRecord(definition.server)) return null;
313
+ const metadata = isRecord(definition.metadata) ? definition.metadata : {};
314
+ const provenance = isRecord(value.provenance) ? value.provenance : {};
315
+ if (metadata.sharedListenerEligible !== true && provenance.sharedListenerEligible !== true) return null;
316
+ const callmuxServer = isRecord(metadata.callmuxServer) ? metadata.callmuxServer : callmuxServerFromMcpServer(definition.server);
317
+ const server = cloneServer(callmuxServer);
318
+ return server ? { name: value.name, server } : null;
319
+ }
320
+
321
+ function callmuxServerFromMcpServer(server: Record<string, unknown>): Record<string, unknown> {
322
+ const out: Record<string, unknown> = {};
323
+ if (typeof server.command === "string") {
324
+ out.command = server.command;
325
+ if (Array.isArray(server.args)) out.args = server.args.filter((arg): arg is string => typeof arg === "string");
326
+ if (isRecord(server.env)) out.env = server.env;
327
+ } else if (typeof server.url === "string") {
328
+ out.url = server.url;
329
+ if (server.type === "sse") out.transport = "sse";
330
+ if (isRecord(server.headers)) out.headers = server.headers;
331
+ }
332
+ return out;
333
+ }
334
+
335
+ function withRuntimeEnv(config: CallmuxConfig, env: Record<string, string | undefined>): CallmuxConfig {
336
+ const inheritedEnv: Record<string, string> = {};
337
+ for (const [key, value] of Object.entries(env)) {
338
+ if (typeof value === "string") inheritedEnv[key] = value;
339
+ }
340
+ if (Object.keys(inheritedEnv).length === 0) return config;
269
341
  const servers = Object.fromEntries(Object.entries(config.servers).map(([name, server]) => {
270
- if (!("command" in server)) return [name, server];
342
+ const rawHeaders = "headers" in server && isRecord(server.headers) ? server.headers as Record<string, string> : undefined;
343
+ const headers = rawHeaders ? expandStringRecordPlaceholders(rawHeaders, inheritedEnv) : undefined;
344
+ if (!("command" in server)) return [name, { ...server, ...(headers ? { headers } : {}) }];
345
+ const serverEnv = isRecord(server.env) ? expandStringRecordPlaceholders(server.env as Record<string, string>, inheritedEnv) : {};
271
346
  return [name, {
272
347
  ...server,
273
- env: { ...inheritedEnv, ...(isRecord(server.env) ? server.env as Record<string, string> : {}) },
348
+ env: { ...inheritedEnv, ...serverEnv },
349
+ ...(headers ? { headers } : {}),
274
350
  }];
275
351
  }));
276
352
  return { ...config, servers };
277
353
  }
278
354
 
355
+ function expandStringRecordPlaceholders(value: Record<string, string>, env: Record<string, string>): Record<string, string> {
356
+ return Object.fromEntries(Object.entries(value).map(([key, raw]) => [key, expandEnvPlaceholders(raw, env)]));
357
+ }
358
+
359
+ function expandEnvPlaceholders(value: string, env: Record<string, string>): string {
360
+ return value.replace(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g, (match, key: string) => env[key] ?? match);
361
+ }
362
+
279
363
  function defaultTokenleanServer(): CallmuxConfig["servers"][string] {
280
364
  return {
281
365
  command: "tl-mcp",