agent-relay-server 0.16.0 → 0.18.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/src/mcp.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { randomUUID } from "node:crypto";
3
- import { isAbsolute, relative, resolve } from "node:path";
4
2
  import { getArtifactStorage, maxArtifactBytes, normalizeDigest } from "./artifact-storage";
5
3
  import { createCommand } from "./commands-db";
4
+ import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams, type SpawnModelParams } from "./spawn-command";
5
+ import { isPathWithinBase } from "./utils";
6
6
  import { MAX_BODY_BYTES, VERSION } from "./config";
7
7
  import { getManagedAgentState, getSpawnPolicy, listSpawnPolicies } from "./config-store";
8
8
  import {
@@ -33,7 +33,8 @@ import {
33
33
  isIntegrationAllowed,
34
34
  } from "./security";
35
35
  import type { ActivityKind, AgentCard, ArtifactKind, ArtifactSensitivity, AttachmentRef, Command, SendMessageInput, Message, SpawnApprovalMode, SpawnProvider } from "./types";
36
- import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
36
+ import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
37
+ import { isRecord, SPAWN_PROVIDERS, APPROVAL_MODES, VALID_EFFORTS } from "agent-relay-sdk";
37
38
  import { childRunnerRuntimeTokenEnv, runnerRuntimeTokenEnv } from "./runtime-tokens";
38
39
 
39
40
  type JsonRpcId = string | number | null;
@@ -66,9 +67,7 @@ const VALID_ARTIFACT_KINDS = ["image", "audio", "video", "document", "archive",
66
67
  const VALID_ARTIFACT_SENSITIVITIES = ["public", "normal", "sensitive", "secret"] as const;
67
68
  const VALID_ARTIFACT_ROLES = ["media", "patch", "report", "log", "output", "input"] as const;
68
69
  const VALID_ARTIFACT_ENTITY_TYPES = ["message", "task", "recipeRun", "recipeStep", "channelEvent"] as const;
69
- const VALID_SPAWN_PROVIDERS = ["claude", "codex"] as const;
70
- const VALID_SPAWN_APPROVALS = ["open", "guarded", "read-only"] as const;
71
- const VALID_PROVIDER_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
70
+
72
71
 
73
72
  const TOOLS: ToolDefinition[] = [
74
73
  {
@@ -201,13 +200,13 @@ const TOOLS: ToolDefinition[] = [
201
200
  inputSchema: {
202
201
  type: "object",
203
202
  properties: {
204
- provider: { type: "string", enum: VALID_SPAWN_PROVIDERS },
203
+ provider: { type: "string", enum: SPAWN_PROVIDERS },
205
204
  orchestratorId: { type: "string" },
206
205
  cwd: { type: "string" },
207
206
  label: { type: "string" },
208
207
  model: { type: "string" },
209
- effort: { type: "string", enum: VALID_PROVIDER_EFFORTS },
210
- approvalMode: { type: "string", enum: VALID_SPAWN_APPROVALS },
208
+ effort: { type: "string", enum: VALID_EFFORTS },
209
+ approvalMode: { type: "string", enum: APPROVAL_MODES },
211
210
  prompt: { type: "string" },
212
211
  systemPromptAppend: { type: "string" },
213
212
  tags: { type: "array", items: { type: "string" } },
@@ -519,16 +518,16 @@ function relayAgentStatus(args: Record<string, unknown>): Record<string, unknown
519
518
  }
520
519
 
521
520
  function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknown>): Record<string, unknown> {
522
- const provider = enumField(args.provider, "provider", VALID_SPAWN_PROVIDERS) as SpawnProvider;
521
+ const provider = enumField(args.provider, "provider", SPAWN_PROVIDERS) as SpawnProvider;
523
522
  const cwd = optionalString(args.cwd, "cwd", 500);
524
523
  const orchestrator = selectSpawnOrchestrator(provider, optionalString(args.orchestratorId, "orchestratorId", 200), cwd);
525
524
  const resolvedCwd = cwd || orchestrator.baseDir;
526
- if (cwd && !pathWithinBase(cwd, orchestrator.baseDir)) {
525
+ if (cwd && !isPathWithinBase(cwd, orchestrator.baseDir)) {
527
526
  throw new ValidationError(`cwd must be within orchestrator base directory: ${orchestrator.baseDir}`);
528
527
  }
529
528
  const selection = providerSelection(provider, args);
530
- const approvalMode = optionalEnum(args.approvalMode, "approvalMode", VALID_SPAWN_APPROVALS) as SpawnApprovalMode | undefined ?? "guarded";
531
- const spawnRequestId = optionalString(args.spawnRequestId, "spawnRequestId", 160) ?? `sp_${randomUUID()}`;
529
+ const approvalMode = optionalEnum(args.approvalMode, "approvalMode", APPROVAL_MODES) as SpawnApprovalMode | undefined ?? "guarded";
530
+ const spawnRequestId = optionalString(args.spawnRequestId, "spawnRequestId", 160) ?? generateSpawnRequestId();
532
531
  const label = optionalString(args.label, "label", 120);
533
532
  const policyName = optionalString(args.policyName, "policyName", 120);
534
533
  assertComponentResourceAllowed(auth, {
@@ -558,12 +557,9 @@ function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknown>): R
558
557
  source: "system",
559
558
  target: orchestrator.agentId,
560
559
  correlationId: spawnRequestId,
561
- params: {
562
- action: "spawn",
560
+ params: buildSpawnCommand({
563
561
  provider,
564
- model: selection.model,
565
- providerModel: selection.providerModel,
566
- effort: selection.effort,
562
+ modelParams: selection,
567
563
  cwd: resolvedCwd,
568
564
  label,
569
565
  tags: optionalStringArray(args.tags, "tags") ?? [],
@@ -580,7 +576,7 @@ function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknown>): R
580
576
  requestedVia: "mcp",
581
577
  requestedAt: Date.now(),
582
578
  orchestratorId: orchestrator.id,
583
- },
579
+ }),
584
580
  });
585
581
  emitCommand(command);
586
582
  return { ok: true, orchestratorId: orchestrator.id, provider, command };
@@ -714,7 +710,7 @@ function selectSpawnOrchestrator(provider: SpawnProvider, orchestratorId?: strin
714
710
  }
715
711
  const candidates = listOrchestrators().filter((item) => item.status === "online" && item.providers.includes(provider));
716
712
  if (cwd) {
717
- const match = candidates.find((item) => pathWithinBase(cwd, item.baseDir));
713
+ const match = candidates.find((item) => isPathWithinBase(cwd, item.baseDir));
718
714
  if (match) return match;
719
715
  }
720
716
  const orchestrator = candidates[0];
@@ -771,26 +767,10 @@ function findManagedOrchestrator(input: {
771
767
  )) ?? null;
772
768
  }
773
769
 
774
- function providerSelection(provider: SpawnProvider, args: Record<string, unknown>): { model?: string; effort?: ProviderEffort; providerModel?: string } {
770
+ function providerSelection(provider: SpawnProvider, args: Record<string, unknown>): SpawnModelParams {
775
771
  const model = optionalString(args.model, "model", 120);
776
- const effort = optionalEnum(args.effort, "effort", VALID_PROVIDER_EFFORTS) as ProviderEffort | undefined;
777
- try {
778
- const resolved = resolveProviderSelection({ provider, model, effort });
779
- return {
780
- model: resolved.modelAlias,
781
- providerModel: resolved.providerModel,
782
- effort: resolved.effort,
783
- };
784
- } catch (error) {
785
- throw new ValidationError(error instanceof Error ? error.message : String(error));
786
- }
787
- }
788
-
789
- function pathWithinBase(path: string, baseDir: string): boolean {
790
- const base = resolve(baseDir);
791
- const target = resolve(path);
792
- const rel = relative(base, target);
793
- return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
772
+ const effort = optionalEnum(args.effort, "effort", VALID_EFFORTS) as ProviderEffort | undefined;
773
+ return resolveSpawnModelParams(provider, model, effort);
794
774
  }
795
775
 
796
776
  function bytesToStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
@@ -937,10 +917,6 @@ function auditToolCall(
937
917
  }
938
918
  }
939
919
 
940
- function isRecord(value: unknown): value is Record<string, unknown> {
941
- return typeof value === "object" && value !== null && !Array.isArray(value);
942
- }
943
-
944
920
  function recordField(value: unknown, field: string): Record<string, unknown> {
945
921
  if (!isRecord(value)) throw new ValidationError(`${field} must be an object`);
946
922
  return value;
@@ -1,3 +1,5 @@
1
+ import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
2
+
1
3
  interface MemoryBrokerSmokeOptions {
2
4
  baseUrl?: string;
3
5
  token?: string;
@@ -39,7 +41,7 @@ export async function runMemoryBrokerSmoke(options: MemoryBrokerSmokeOptions = {
39
41
 
40
42
  const request = async <T>(method: string, path: string, body?: unknown): Promise<T> => {
41
43
  const headers: Record<string, string> = {};
42
- if (token) headers["X-Agent-Relay-Token"] = token;
44
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
43
45
  if (body !== undefined) headers["Content-Type"] = "application/json";
44
46
  const response = await fetchImpl(new URL(path, baseUrl), {
45
47
  method,
@@ -20,6 +20,7 @@ import {
20
20
  normalizeMemorySearchResult,
21
21
  } from "./memory-broker-contract";
22
22
  import { normalizeContextPackage } from "./memory-http-broker";
23
+ import { isRecord } from "agent-relay-sdk";
23
24
 
24
25
  const DEFAULT_TIMEOUT_MS = 10_000;
25
26
 
@@ -155,7 +156,3 @@ function numberField(value: unknown, field: string): number {
155
156
  if (typeof value !== "number" || !Number.isFinite(value)) throw new MemoryBrokerContractError(`${field} must be a number`);
156
157
  return value;
157
158
  }
158
-
159
- function isRecord(value: unknown): value is Record<string, unknown> {
160
- return typeof value === "object" && value !== null && !Array.isArray(value);
161
- }
@@ -19,6 +19,7 @@ import {
19
19
  normalizeMemoryBrokerCapabilities,
20
20
  normalizeMemorySearchResult,
21
21
  } from "./memory-broker-contract";
22
+ import { isRecord } from "agent-relay-sdk";
22
23
 
23
24
  const DEFAULT_TIMEOUT_MS = 10_000;
24
25
 
@@ -182,7 +183,3 @@ function requireRecord(value: unknown, field: string): Record<string, unknown> {
182
183
  if (!isRecord(value)) throw new MemoryBrokerContractError(`${field} must be an object`);
183
184
  return value;
184
185
  }
185
-
186
- function isRecord(value: unknown): value is Record<string, unknown> {
187
- return typeof value === "object" && value !== null && !Array.isArray(value);
188
- }
@@ -1,5 +1,6 @@
1
1
  import { createCommand } from "./commands-db";
2
2
  import { getAgent, ValidationError } from "./db";
3
+ import { isRecord } from "agent-relay-sdk";
3
4
  import { assembleContextPackage, contextBudgetForAgent, estimateMemoryTokens } from "./memory-broker";
4
5
  import { createMemoryBrokerRegistry, memoryBrokerConfigFromEnv } from "./memory-broker-registry";
5
6
  import { filterMemoriesForInjection, formatUntrustedMemoryBlock } from "./memory-broker-contract";
@@ -349,7 +350,3 @@ function stringArray(value: unknown): string[] | undefined {
349
350
  function isPositiveInteger(value: unknown): value is number {
350
351
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
351
352
  }
352
-
353
- function isRecord(value: unknown): value is Record<string, unknown> {
354
- return typeof value === "object" && value !== null && !Array.isArray(value);
355
- }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { Database } from "bun:sqlite";
3
3
  import { getDb, ValidationError } from "./db";
4
+ import { parseJson } from "./utils";
4
5
  import type {
5
6
  ActiveMemoryClearReason,
6
7
  ContextPackage,
@@ -327,14 +328,6 @@ function rowToMemory(row: MemoryRow): Memory {
327
328
  });
328
329
  }
329
330
 
330
- function parseJson<T>(raw: string, fallback: T): T {
331
- try {
332
- return JSON.parse(raw) as T;
333
- } catch {
334
- return fallback;
335
- }
336
- }
337
-
338
331
  function tagOverlap(tags: string[], queryTags: Set<string>): number {
339
332
  if (queryTags.size === 0) return 0;
340
333
  const memoryTags = new Set(tags.map((tag) => tag.toLowerCase()));
@@ -3,10 +3,10 @@ import {
3
3
  type ProviderCatalogEntry,
4
4
  type ProviderModelCatalogEntry,
5
5
  } from "agent-relay-sdk/provider-catalog";
6
+ import { SPAWN_PROVIDERS } from "agent-relay-sdk";
6
7
  import type { SpawnProvider } from "./types";
7
8
  import { getDb, ValidationError } from "./db";
8
-
9
- const VALID_PROVIDERS: SpawnProvider[] = ["claude", "codex"];
9
+ import { parseJson } from "./utils";
10
10
 
11
11
  interface ProviderModelOverrideRow {
12
12
  provider: SpawnProvider;
@@ -111,7 +111,7 @@ export function mergeProviderCatalog(
111
111
  }
112
112
 
113
113
  function normalizeOverrideInput(input: ProviderModelOverrideInput): ProviderModelOverrideInput {
114
- if (!VALID_PROVIDERS.includes(input.provider)) throw new ValidationError("provider must be claude or codex");
114
+ if (!SPAWN_PROVIDERS.includes(input.provider)) throw new ValidationError("provider must be claude or codex");
115
115
  const alias = input.alias.trim();
116
116
  if (!alias) throw new ValidationError("model alias is required");
117
117
  if (input.entry.alias !== alias) throw new ValidationError("model entry alias must match override alias");
@@ -154,11 +154,3 @@ function cloneProviderEntry(entry: ProviderCatalogEntry): ProviderCatalogEntry {
154
154
  function cloneModelEntry(entry: ProviderModelCatalogEntry): ProviderModelCatalogEntry {
155
155
  return JSON.parse(JSON.stringify(entry)) as ProviderModelCatalogEntry;
156
156
  }
157
-
158
- function parseJson<T>(raw: string, fallback: T): T {
159
- try {
160
- return JSON.parse(raw) as T;
161
- } catch {
162
- return fallback;
163
- }
164
- }
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { basename, join, resolve } from "node:path";
3
3
  import { validateRecipe } from "./recipe-validator";
4
4
  import type { Recipe } from "./types";
5
+ import { isRecord } from "agent-relay-sdk";
5
6
 
6
7
  interface LoadedRecipe {
7
8
  name: string;
@@ -113,7 +114,3 @@ function parseScalar(value: string): unknown {
113
114
  if (/^\d+$/.test(trimmed)) return Number(trimmed);
114
115
  return trimmed.replace(/^["']|["']$/g, "");
115
116
  }
116
-
117
- function isRecord(value: unknown): value is Record<string, unknown> {
118
- return typeof value === "object" && value !== null && !Array.isArray(value);
119
- }
@@ -1,5 +1,6 @@
1
1
  import type { MemoryType, Recipe, RecipeAgent, RecipeMemoryPolicy } from "./types";
2
2
  import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
3
+ import { isRecord } from "agent-relay-sdk";
3
4
 
4
5
  class RecipeValidationError extends Error {}
5
6
 
@@ -131,7 +132,3 @@ function optionalPositiveInteger(value: unknown, field: string): number | undefi
131
132
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new RecipeValidationError(`${field} must be a positive integer`);
132
133
  return value;
133
134
  }
134
-
135
- function isRecord(value: unknown): value is Record<string, unknown> {
136
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
137
- }