agent-relay-server 0.102.2 → 0.104.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.
Files changed (128) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +4 -2
  3. package/public/assets/MessageBubble-BpiTbbnp.js +4 -0
  4. package/public/assets/{MessageBubble-Dppo7pil.js.map → MessageBubble-BpiTbbnp.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-DO5OY2qa.js → PlanGraphPanel-k8Aa98g5.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-DO5OY2qa.js.map → PlanGraphPanel-k8Aa98g5.js.map} +1 -1
  7. package/public/assets/{activity-BhuOs5ha.js → activity-CxKi5Erb.js} +2 -2
  8. package/public/assets/{activity-BhuOs5ha.js.map → activity-CxKi5Erb.js.map} +1 -1
  9. package/public/assets/agent-profiles-DMZXdN6K.js +2 -0
  10. package/public/assets/agent-profiles-DMZXdN6K.js.map +1 -0
  11. package/public/assets/agents-BrvpH3gn.js +2 -0
  12. package/public/assets/agents-BrvpH3gn.js.map +1 -0
  13. package/public/assets/{analytics-BxGRljQc.js → analytics-1fvh-9ok.js} +2 -2
  14. package/public/assets/{analytics-BxGRljQc.js.map → analytics-1fvh-9ok.js.map} +1 -1
  15. package/public/assets/automation-BCgX5WOH.js +2 -0
  16. package/public/assets/automation-BCgX5WOH.js.map +1 -0
  17. package/public/assets/{branch-state-badge-olhanssg.js → branch-state-badge-DYA7bU0i.js} +2 -2
  18. package/public/assets/{branch-state-badge-olhanssg.js.map → branch-state-badge-DYA7bU0i.js.map} +1 -1
  19. package/public/assets/{channels-yHkm1dux.js → channels-CK80oJYG.js} +2 -2
  20. package/public/assets/{channels-yHkm1dux.js.map → channels-CK80oJYG.js.map} +1 -1
  21. package/public/assets/chat-D_2PEz06.js +2 -0
  22. package/public/assets/chat-D_2PEz06.js.map +1 -0
  23. package/public/assets/{connectors-DpR0V2hy.js → connectors-ChGyht5m.js} +2 -2
  24. package/public/assets/{connectors-DpR0V2hy.js.map → connectors-ChGyht5m.js.map} +1 -1
  25. package/public/assets/display-D1xB8NJZ.js +3 -0
  26. package/public/assets/display-D1xB8NJZ.js.map +1 -0
  27. package/public/assets/{formatted-body-impl-BTeCfK4I.js → formatted-body-impl-BJ4WCF5J.js} +2 -2
  28. package/public/assets/{formatted-body-impl-BTeCfK4I.js.map → formatted-body-impl-BJ4WCF5J.js.map} +1 -1
  29. package/public/assets/index-BMO_Sblt.css +2 -0
  30. package/public/assets/index-CN7QZlMb.js +23 -0
  31. package/public/assets/index-CN7QZlMb.js.map +1 -0
  32. package/public/assets/{insights-C4aYlgsY.js → insights-BCZRV0hX.js} +2 -2
  33. package/public/assets/{insights-C4aYlgsY.js.map → insights-BCZRV0hX.js.map} +1 -1
  34. package/public/assets/{integrations-CEmYa5mW.js → integrations-C7UGs00U.js} +2 -2
  35. package/public/assets/{integrations-CEmYa5mW.js.map → integrations-C7UGs00U.js.map} +1 -1
  36. package/public/assets/maintenance-ChYno3Fm.js +2 -0
  37. package/public/assets/{maintenance-DVx3SiC_.js.map → maintenance-ChYno3Fm.js.map} +1 -1
  38. package/public/assets/{managed-agents-DUyOTdz1.js → managed-agents-DjxZQNIR.js} +4 -4
  39. package/public/assets/managed-agents-DjxZQNIR.js.map +1 -0
  40. package/public/assets/{markdown-preview-impl-D3hYfzGD.js → markdown-preview-impl-DFa5HF1H.js} +2 -2
  41. package/public/assets/{markdown-preview-impl-D3hYfzGD.js.map → markdown-preview-impl-DFa5HF1H.js.map} +1 -1
  42. package/public/assets/{memory-2IfPgcnN.js → memory-CTw6kadT.js} +2 -2
  43. package/public/assets/{memory-2IfPgcnN.js.map → memory-CTw6kadT.js.map} +1 -1
  44. package/public/assets/{messages-CkOwABhw.js → messages-Cjr816D_.js} +2 -2
  45. package/public/assets/{messages-CkOwABhw.js.map → messages-Cjr816D_.js.map} +1 -1
  46. package/public/assets/{orchestrators-CEKO3eQ1.js → orchestrators-CdmFKA7K.js} +2 -2
  47. package/public/assets/{orchestrators-CEKO3eQ1.js.map → orchestrators-CdmFKA7K.js.map} +1 -1
  48. package/public/assets/overview-DBOnjfpP.js +2 -0
  49. package/public/assets/{overview-D9BxevAq.js.map → overview-DBOnjfpP.js.map} +1 -1
  50. package/public/assets/{pairs-CqT4D1uL.js → pairs-BkcdKdF7.js} +2 -2
  51. package/public/assets/{pairs-CqT4D1uL.js.map → pairs-BkcdKdF7.js.map} +1 -1
  52. package/public/assets/{projects-C_zwR-Fn.js → projects-ClKbDQB1.js} +2 -2
  53. package/public/assets/{projects-C_zwR-Fn.js.map → projects-ClKbDQB1.js.map} +1 -1
  54. package/public/assets/registry-Bsf3RtOO.js +2 -0
  55. package/public/assets/registry-Bsf3RtOO.js.map +1 -0
  56. package/public/assets/{security-8a5_4U3S.js → security-Iyu5bhJw.js} +2 -2
  57. package/public/assets/{security-8a5_4U3S.js.map → security-Iyu5bhJw.js.map} +1 -1
  58. package/public/assets/{select-s4BE-T62.js → select-Bi6tNJDx.js} +2 -2
  59. package/public/assets/{select-s4BE-T62.js.map → select-Bi6tNJDx.js.map} +1 -1
  60. package/public/assets/settings-CbHp2F9r.js +6 -0
  61. package/public/assets/settings-CbHp2F9r.js.map +1 -0
  62. package/public/assets/store-zeyDiSKE.js +9 -0
  63. package/public/assets/store-zeyDiSKE.js.map +1 -0
  64. package/public/assets/{tasks-Bcr17zR3.js → tasks-CPlf-5Tr.js} +2 -2
  65. package/public/assets/{tasks-Bcr17zR3.js.map → tasks-CPlf-5Tr.js.map} +1 -1
  66. package/public/assets/teams-D2uUNhlY.js +2 -0
  67. package/public/assets/{teams-B_HSkU9T.js.map → teams-D2uUNhlY.js.map} +1 -1
  68. package/public/assets/{terminal-viewer-impl-Ctp4csJ3.js → terminal-viewer-impl-uRzbrlXy.js} +2 -2
  69. package/public/assets/{terminal-viewer-impl-Ctp4csJ3.js.map → terminal-viewer-impl-uRzbrlXy.js.map} +1 -1
  70. package/public/assets/types-DorDDvEb.js +2 -0
  71. package/public/assets/types-DorDDvEb.js.map +1 -0
  72. package/public/assets/{user-avatar-RliaJcaG.js → user-avatar-GFWMuAGK.js} +2 -2
  73. package/public/assets/{user-avatar-RliaJcaG.js.map → user-avatar-GFWMuAGK.js.map} +1 -1
  74. package/public/assets/{work-queue-C0hpmlv8.js → work-queue-B30SNmuq.js} +2 -2
  75. package/public/assets/{work-queue-C0hpmlv8.js.map → work-queue-B30SNmuq.js.map} +1 -1
  76. package/public/assets/{workspaces-QXbQx_AG.js → workspaces-CSpmgbPT.js} +3 -3
  77. package/public/assets/{workspaces-QXbQx_AG.js.map → workspaces-CSpmgbPT.js.map} +1 -1
  78. package/public/index.html +5 -5
  79. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  80. package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +238 -16
  81. package/runner/src/config.ts +8 -3
  82. package/src/automations/validation.ts +3 -3
  83. package/src/cli/orchestrator.ts +7 -5
  84. package/src/cli/provider.ts +26 -10
  85. package/src/cli/upgrade.ts +5 -4
  86. package/src/commands-db.ts +1 -1
  87. package/src/config-store.ts +3 -3
  88. package/src/config.ts +3 -3
  89. package/src/context-advisory.ts +3 -3
  90. package/src/db/provider-quotas.ts +11 -2
  91. package/src/db/provisioning-registry.ts +49 -61
  92. package/src/managed-policy.ts +7 -2
  93. package/src/mcp/tools-spawn.ts +2 -2
  94. package/src/mcp-team-template-tools.ts +2 -2
  95. package/src/provider-catalog-store.ts +1 -1
  96. package/src/recipe-validator.ts +4 -2
  97. package/src/routes/agent-profiles.ts +2 -2
  98. package/src/routes/agent-sessions.ts +18 -23
  99. package/src/routes/provider-config.ts +1 -1
  100. package/src/spawn-targets.ts +2 -2
  101. package/src/upgrade.ts +120 -44
  102. package/src/workspace-phase.ts +1 -0
  103. package/public/assets/MessageBubble-Dppo7pil.js +0 -4
  104. package/public/assets/agent-profiles-CT-vbfzd.js +0 -2
  105. package/public/assets/agent-profiles-CT-vbfzd.js.map +0 -1
  106. package/public/assets/agents-CLcgi8AD.js +0 -2
  107. package/public/assets/agents-CLcgi8AD.js.map +0 -1
  108. package/public/assets/automation-CE1i1zpw.js +0 -2
  109. package/public/assets/automation-CE1i1zpw.js.map +0 -1
  110. package/public/assets/chat-DHdFZvAg.js +0 -2
  111. package/public/assets/chat-DHdFZvAg.js.map +0 -1
  112. package/public/assets/display-DRiGi1zj.js +0 -3
  113. package/public/assets/display-DRiGi1zj.js.map +0 -1
  114. package/public/assets/index-CKhh9XRZ.css +0 -2
  115. package/public/assets/index-DeDosfVE.js +0 -23
  116. package/public/assets/index-DeDosfVE.js.map +0 -1
  117. package/public/assets/maintenance-DVx3SiC_.js +0 -2
  118. package/public/assets/managed-agents-DUyOTdz1.js.map +0 -1
  119. package/public/assets/overview-D9BxevAq.js +0 -2
  120. package/public/assets/registry-BLjwVXCW.js +0 -2
  121. package/public/assets/registry-BLjwVXCW.js.map +0 -1
  122. package/public/assets/settings-BR30UpbJ.js +0 -6
  123. package/public/assets/settings-BR30UpbJ.js.map +0 -1
  124. package/public/assets/store-DNbkrqbU.js +0 -9
  125. package/public/assets/store-DNbkrqbU.js.map +0 -1
  126. package/public/assets/teams-B_HSkU9T.js +0 -2
  127. package/public/assets/types-Tv67voFu.js +0 -2
  128. package/public/assets/types-Tv67voFu.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { errMessage, VALID_WORKSPACE_MODES } from "agent-relay-sdk";
1
+ import { errMessage, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES, type SpawnProvider } from "agent-relay-sdk";
2
2
  import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
3
3
  import { getAgentProfile } from "../config-store";
4
4
  import { ValidationError } from "../db";
@@ -85,7 +85,7 @@ function normalizeTargetPolicy(value: unknown): AutomationTargetPolicy {
85
85
  return {
86
86
  mode,
87
87
  selector: {
88
- provider: cleanString(selectorInput.provider, "targetPolicy.selector.provider", { max: 40 }) as "claude" | "codex" | undefined,
88
+ provider: cleanString(selectorInput.provider, "targetPolicy.selector.provider", { max: 40 }) as SpawnProvider | undefined,
89
89
  label: cleanString(selectorInput.label, "targetPolicy.selector.label", { max: 120 }),
90
90
  tags: cleanStringArray(selectorInput.tags, "targetPolicy.selector.tags", { itemMax: 100 }),
91
91
  capabilities: cleanStringArray(selectorInput.capabilities, "targetPolicy.selector.capabilities", { itemMax: 100 }),
@@ -93,7 +93,7 @@ function normalizeTargetPolicy(value: unknown): AutomationTargetPolicy {
93
93
  ifNoMatch: optionalEnum(input.ifNoMatch, "targetPolicy.ifNoMatch", ["fail", "spawn"] as const, "fail"),
94
94
  };
95
95
  }
96
- const provider = optionalEnum(input.provider, "targetPolicy.provider", ["claude", "codex"] as const, "codex");
96
+ const provider = optionalEnum(input.provider, "targetPolicy.provider", SPAWN_PROVIDERS, "codex");
97
97
  const model = cleanString(input.model, "targetPolicy.model", { max: 120 });
98
98
  const effort = cleanString(input.effort, "targetPolicy.effort", { max: 120 }) as ProviderEffort | undefined;
99
99
  const profile = cleanString(input.profile, "targetPolicy.profile", { max: 120 });
@@ -3,6 +3,7 @@
3
3
  import { chmodSync, existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
4
4
  import { hostname as osHostname, homedir } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
6
+ import { getAllManifests, getManifest } from "agent-relay-providers";
6
7
  import {
7
8
  createDaemonPlan,
8
9
  detectDaemonEnvironment,
@@ -28,7 +29,7 @@ export async function handleOrchestratorCommand(args: string[]): Promise<void> {
28
29
  let bootstrapToken = "";
29
30
  let id = sanitizeOrchestratorId(osHostname());
30
31
  let baseDir = join(homedir(), "projects");
31
- let providers = "claude,codex";
32
+ let providers = getAllManifests().map((manifest) => manifest.id).join(",");
32
33
  let apiPort = 4860;
33
34
  let runtimePrefix = defaultRuntimePrefix();
34
35
  let serviceName = "agent-relay-orchestrator";
@@ -65,9 +66,9 @@ export async function handleOrchestratorCommand(args: string[]): Promise<void> {
65
66
 
66
67
  const providerList = providers.split(",").map((p) => p.trim()).filter(Boolean);
67
68
  for (const provider of providerList) {
68
- if (provider !== "claude" && provider !== "codex") throw new Error(`Unknown provider "${provider}". Expected claude or codex.`);
69
+ if (!getManifest(provider)) throw new Error(`Unknown provider "${provider}". Expected one of ${getAllManifests().map((manifest) => manifest.id).join(", ")}.`);
69
70
  }
70
- const detectedPathPrefix = [...pathPrefix, ...detectProviderPathPrefixes(providerList as Array<"claude" | "codex">)];
71
+ const detectedPathPrefix = [...pathPrefix, ...detectProviderPathPrefixes(providerList)];
71
72
  const uniquePathPrefix = [...new Set(detectedPathPrefix)];
72
73
  const envToken = token || (dryRun || json ? "<runtime-token-from-bootstrap>" : await exchangeOrchestratorBootstrapToken(relayUrl, bootstrapToken, id, baseDir));
73
74
  const envFile = defaultOrchestratorEnvFile();
@@ -165,10 +166,11 @@ function sanitizeOrchestratorId(value: string): string {
165
166
  return clean.slice(0, 120);
166
167
  }
167
168
 
168
- function detectProviderPathPrefixes(providers: Array<"claude" | "codex">): string[] {
169
+ function detectProviderPathPrefixes(providers: string[]): string[] {
169
170
  const prefixes: string[] = [];
170
171
  for (const provider of providers) {
171
- const path = resolveExecutableOnPath(provider);
172
+ const command = getManifest(provider)?.probe?.command ?? provider;
173
+ const path = resolveExecutableOnPath(command);
172
174
  if (path) prefixes.push(dirname(path));
173
175
  }
174
176
  return prefixes;
@@ -5,16 +5,19 @@
5
5
  import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { dirname, join } from "node:path";
8
+ import { getAllManifests, getManifest } from "agent-relay-providers";
8
9
 
9
10
  export async function handleProviderCommand(args: string[]): Promise<void> {
10
11
  const action = args[0];
11
12
  const provider = args[1];
12
- if ((action !== "wrap" && action !== "unwrap") || (provider !== "claude" && provider !== "codex")) {
13
- throw new Error("Usage: agent-relay provider <wrap|unwrap> <claude|codex>");
13
+ const providerList = shimmableProviderIds();
14
+ const manifest = typeof provider === "string" ? getManifest(provider) : undefined;
15
+ if ((action !== "wrap" && action !== "unwrap") || !provider || !manifest?.shim) {
16
+ throw new Error(`Usage: agent-relay provider <wrap|unwrap> <${providerList.join("|")}>`);
14
17
  }
15
18
  const dir = join(process.env.HOME || homedir(), ".agent-relay", "bin");
16
19
  const shims = providerShimPaths(provider);
17
- if (provider === "codex") cleanLegacyCodexSessionStartHook();
20
+ if (manifest.shim.cleanLegacyHooks) cleanLegacyCodexSessionStartHook();
18
21
  if (action === "wrap") {
19
22
  for (const shim of shims) {
20
23
  mkdirSync(dirname(shim), { recursive: true });
@@ -31,6 +34,12 @@ export async function handleProviderCommand(args: string[]): Promise<void> {
31
34
  }
32
35
  }
33
36
 
37
+ function shimmableProviderIds(): string[] {
38
+ return getAllManifests()
39
+ .filter((manifest) => manifest.shim)
40
+ .map((manifest) => manifest.id);
41
+ }
42
+
34
43
  function cleanLegacyCodexSessionStartHook(): void {
35
44
  const configPath = join(process.env.HOME || homedir(), ".codex", "config.toml");
36
45
  if (!existsSync(configPath)) return;
@@ -111,16 +120,23 @@ function isLegacyCodexSessionStartHook(text: string): boolean {
111
120
  /agent-relay.*(SessionStart|UserPromptSubmit|Stop).*hook/i.test(text);
112
121
  }
113
122
 
114
- function providerShimPaths(provider: "claude" | "codex"): string[] {
123
+ function providerShimPaths(provider: string): string[] {
124
+ const manifest = getManifest(provider);
125
+ if (!manifest?.shim) throw new Error(`Provider "${provider}" does not support PATH shims.`);
115
126
  const root = join(process.env.HOME || homedir(), ".agent-relay");
116
- const paths = [join(root, "bin", provider)];
117
- if (provider === "codex") paths.push(join(root, "codex", "bin", "codex"));
118
- return paths;
127
+ return [
128
+ join(root, "bin", provider),
129
+ ...(manifest.shim.extraBinPaths ?? []).map((path: string) => join(root, path)),
130
+ ];
119
131
  }
120
132
 
121
- function providerShimContent(provider: "claude" | "codex"): string {
122
- if (provider === "claude") {
123
- return `#!/usr/bin/env bash\nexec claude-relay claude -- "$@"\n`;
133
+ function providerShimContent(provider: string): string {
134
+ const shimKind = getManifest(provider)?.shim?.kind;
135
+ if (shimKind === "claude-relay") {
136
+ return `#!/usr/bin/env bash\nexec claude-relay ${provider} -- "$@"\n`;
137
+ }
138
+ if (shimKind !== "codex-relay") {
139
+ throw new Error(`Provider "${provider}" does not define a shim template.`);
124
140
  }
125
141
  return `#!/usr/bin/env bash
126
142
  set -e
@@ -1,5 +1,6 @@
1
1
  // Upgrade commands — auto-split from cli.ts (#294). Local runtime upgrade plus
2
2
  // remote-host fan-out over the relay command bus (#210).
3
+ import { getAllManifests, getManifest } from "agent-relay-providers";
3
4
  import {
4
5
  createUpgradePlan,
5
6
  detectUpgradeSnapshot,
@@ -27,6 +28,7 @@ export async function handleUpgradeCommand(args: string[]): Promise<void> {
27
28
 
28
29
  for (let i = 0; i < args.length; i++) {
29
30
  const arg = args[i];
31
+ const providerFlag = arg?.startsWith("--") ? arg.slice(2) : undefined;
30
32
  if (arg === "--version" && i + 1 < args.length) targetVersion = args[++i];
31
33
  else if (arg === "--runtime-prefix" && i + 1 < args.length) runtimePrefix = args[++i];
32
34
  else if (arg === "--providers" && i + 1 < args.length) {
@@ -34,8 +36,7 @@ export async function handleUpgradeCommand(args: string[]): Promise<void> {
34
36
  } else if (arg === "--provider" && i + 1 < args.length) providers.push(parseUpgradeProvider(args[++i]!));
35
37
  else if (arg === "--host" && i + 1 < args.length) hosts.push(args[++i]!);
36
38
  else if (arg === "--all-hosts") allHosts = true;
37
- else if (arg === "--codex") providers.push("codex");
38
- else if (arg === "--claude") providers.push("claude");
39
+ else if (providerFlag && getManifest(providerFlag)) providers.push(parseUpgradeProvider(providerFlag));
39
40
  else if (arg === "--orchestrator") providers.push("orchestrator");
40
41
  else if (arg === "--all") providers.push("all");
41
42
  else if (arg === "--dry-run") dryRun = true;
@@ -188,6 +189,6 @@ async function runRemoteOrchestratorUpgrades(opts: {
188
189
  }
189
190
 
190
191
  function parseUpgradeProvider(value: string): UpgradeProvider {
191
- if (value === "auto" || value === "all" || value === "codex" || value === "claude" || value === "orchestrator") return value;
192
- throw new Error(`Unknown upgrade provider "${value}". Expected auto, all, codex, claude, or orchestrator.`);
192
+ if (value === "auto" || value === "all" || value === "orchestrator" || getManifest(value)) return value;
193
+ throw new Error(`Unknown upgrade provider "${value}". Expected auto, all, orchestrator, or one of ${getAllManifests().map((manifest) => manifest.id).join(", ")}.`);
193
194
  }
@@ -131,7 +131,7 @@ export function expireCommands(now: number = Date.now()): Command[] {
131
131
  }
132
132
 
133
133
  function defaultExpiresAt(type: string, now: number): number | undefined {
134
- if (type === "agent.spawn") return now + 60_000;
134
+ if (type === "agent.spawn") return now + 180_000;
135
135
  if (type === "agent.shutdown" || type === "agent.restart" || type === "agent.reconnect" || type === "agent.kill" || type === "agent.compact" || type === "agent.clearContext" || type === "agent.injectContext") return now + 30_000;
136
136
  return undefined;
137
137
  }
@@ -16,7 +16,7 @@ import { PROJECT_SETTINGS_POLICY_NAMES } from "./project-settings";
16
16
  import { PROJECT_SKILLS_POLICY_NAMES } from "./project-skills";
17
17
  import { SHARED_MCP_POLICY_NAMES } from "./shared-mcp";
18
18
  import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
19
- import { errMessage, isRecord, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES } from "agent-relay-sdk"; import { effectiveProviderCatalogList } from "./provider-catalog-store";
19
+ import { errMessage, isRecord, isSpawnProvider, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES } from "agent-relay-sdk"; import { effectiveProviderCatalogList } from "./provider-catalog-store";
20
20
  import type {
21
21
  AgentProfile,
22
22
  AgentProfileBase,
@@ -50,7 +50,7 @@ const WORKSPACE_KEY = "default";
50
50
  const LANDING_NAMESPACE = "landing";
51
51
  const LANDING_KEY = "default";
52
52
  const VALID_LANDING_STRATEGIES = ["direct", "pr"] as const;
53
- const VALID_PROFILE_PROVIDERS = ["any", "claude", "codex"] as const;
53
+ const VALID_PROFILE_PROVIDERS = ["any", ...SPAWN_PROVIDERS] as const;
54
54
  const VALID_PROFILE_BASES = ["host", "minimal", "isolated", "vanilla"] as const;
55
55
  const VALID_PROFILE_INSTRUCTION_POLICIES = ["allow", "ignore"] as const;
56
56
  const VALID_PROFILE_CATEGORY_MODES = ["host", "profile", "repo", "none"] as const;
@@ -957,7 +957,7 @@ function listManagedAgentStates(): ManagedAgentState[] {
957
957
 
958
958
  export function upsertManagedAgentState(input: ManagedAgentStateInput): ManagedAgentState {
959
959
  if (!VALID_MANAGED_STATUSES.includes(input.status)) throw new ValidationError("status must be a managed-agent status");
960
- if (!SPAWN_PROVIDERS.includes(input.provider)) throw new ValidationError("provider must be claude or codex");
960
+ if (!isSpawnProvider(input.provider)) throw new ValidationError(`provider must be one of: ${SPAWN_PROVIDERS.join(", ")}`);
961
961
  const now = input.updatedAt ?? Date.now();
962
962
  getDb().query(`
963
963
  INSERT INTO managed_agent_state (
package/src/config.ts CHANGED
@@ -48,9 +48,9 @@ function envTrimmedOrUndefined(name: string): string | undefined {
48
48
 
49
49
  export const STALE_TTL_MS = envPositiveInt("STALE_TTL_MS", 120_000); // 2min without heartbeat → offline
50
50
  // Backstop for agent-initiated spawns whose orchestrator command succeeded but whose runner never
51
- // registered with Relay. Keep this above the runner's own 60s registration timeout so the runner's
52
- // direct failure path wins first; this is the relay-side dead-man switch.
53
- export const SPAWN_REGISTRATION_DEADLINE_MS = envPositiveInt("AGENT_RELAY_SPAWN_REGISTRATION_DEADLINE_MS", 120_000);
51
+ // registered with Relay. Keep this above the command TTL (180s) so the command's direct failure
52
+ // path wins first; this is the relay-side dead-man switch.
53
+ export const SPAWN_REGISTRATION_DEADLINE_MS = envPositiveInt("AGENT_RELAY_SPAWN_REGISTRATION_DEADLINE_MS", 300_000);
54
54
  export const SPAWN_REGISTRATION_NOTIFY_MAX_AGE_MS = envPositiveInt("AGENT_RELAY_SPAWN_REGISTRATION_NOTIFY_MAX_AGE_MS", 15 * 60_000);
55
55
  // #451 — default TTL for fire-and-forget FAN-OUT messages (broadcast/tag/cap/label/team) that the
56
56
  // sender didn't bound. A stale fan-out push must not wake an agent that matches the target hours
@@ -1,5 +1,5 @@
1
1
  import { deriveContextThresholdFromWindowTokens, findProviderCatalogModel, resolveProviderContextThreshold, type ProviderModelCatalogEntry, type ResolvedProviderContextThreshold } from "agent-relay-sdk/provider-catalog";
2
- import { isRecord, SPAWN_PROVIDERS } from "agent-relay-sdk";
2
+ import { isRecord, isSpawnProvider } from "agent-relay-sdk";
3
3
  import { getConfig } from "./config-store";
4
4
  import { validateContextThresholdConfig } from "./context-threshold-config";
5
5
  import { getResolvedNotificationsConfig } from "./notification-settings-store";
@@ -170,8 +170,8 @@ function hasExplicitContextThreshold(agent: AgentCard): boolean {
170
170
 
171
171
  function findAgentCatalogModel(agent: AgentCard, observedWindowTokens: number | undefined): ProviderModelCatalogEntry | undefined {
172
172
  const runtime = agent.providerCapabilities?.model;
173
- const provider = runtime?.provider;
174
- if (!provider || !(SPAWN_PROVIDERS as readonly string[]).includes(provider)) return undefined;
173
+ if (!runtime || !isSpawnProvider(runtime.provider)) return undefined;
174
+ const provider = runtime.provider;
175
175
  const catalog = effectiveProviderCatalogList().find((entry) => entry.provider === provider);
176
176
  if (!catalog) return undefined;
177
177
  const ids = [runtime.alias, runtime.id, runtime.providerModel].filter((value): value is string => Boolean(value));
@@ -1,5 +1,6 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { DAY_MS, QUOTA_HISTORY_RETENTION_MS } from "../config";
2
- import { credentialAccountKey } from "agent-relay-sdk/provider-quota";
3
+ import { quotaAccountKeyAliases, registerAccountKeyAliases } from "agent-relay-providers";
3
4
  import { parseJson } from "../utils";
4
5
  import { getDb } from "./connection.ts";
5
6
  import { deriveProviderQuotaBurn } from "./provider-quota-burn.ts";
@@ -12,6 +13,14 @@ const QUOTA_RESET_BOUNDARY_ADVANCE_MS = 5_000;
12
13
  const QUOTA_RESET_UTILIZATION_MAX = 0.01;
13
14
  const FIVE_HOUR_MS = 5 * 60 * 60_000;
14
15
 
16
+ registerAccountKeyAliases("codex", (accountKey) => {
17
+ if (accountKey.startsWith("account:")) {
18
+ const hash = createHash("sha256").update(accountKey.slice("account:".length)).digest("hex").slice(0, 16);
19
+ return [`credential:${hash}`];
20
+ }
21
+ return [];
22
+ });
23
+
15
24
  export interface ProviderQuotaAdvisoryInput {
16
25
  provider: string;
17
26
  accountKey: string;
@@ -101,7 +110,7 @@ function quotaRowsForProvider(provider: string): ProviderQuotaRow[] {
101
110
  function supersededAccountKeys(provider: string, accountKey: string, rawAccountKey: string, sourceAgentId: string | undefined): string[] {
102
111
  const candidates = new Set<string>();
103
112
  if (rawAccountKey && rawAccountKey !== accountKey) candidates.add(rawAccountKey);
104
- if (provider === "codex" && accountKey.startsWith("account:")) candidates.add(credentialAccountKey(accountKey.slice("account:".length)));
113
+ for (const alias of quotaAccountKeyAliases(provider, accountKey)) candidates.add(alias);
105
114
  const host = hostFromAccountKey(accountKey) ?? sourceAgentHost(sourceAgentId);
106
115
  if (host) {
107
116
  candidates.add(`host:${host}:${provider}`);
@@ -2,9 +2,12 @@ import { randomUUID } from "node:crypto";
2
2
  import { Buffer } from "node:buffer";
3
3
  import { existsSync, readdirSync, readFileSync } from "node:fs";
4
4
  import { basename, join, posix, resolve } from "node:path";
5
+ import "agent-relay-providers";
6
+ import { getManifest } from "agent-relay-providers/registry";
7
+ import type { ProviderManifest } from "agent-relay-providers/types";
5
8
  import { getDb, ValidationError } from "./connection.ts";
6
9
  import { parseJson } from "../utils";
7
- import { errMessage, isRecord } from "agent-relay-sdk";
10
+ import { errMessage, isRecord, SPAWN_PROVIDERS } from "agent-relay-sdk";
8
11
  import type {
9
12
  AgentProfile,
10
13
  AgentProfileAssetRef,
@@ -30,16 +33,14 @@ import type {
30
33
  // this stays the single home for asset definition + resolution — not three modules.
31
34
 
32
35
  const PROVISIONING_ASSET_KINDS = ["skill", "mcp", "plugin"] as const;
33
- const PROVISIONING_PROVIDERS = ["claude", "codex", "any"] as const;
36
+ const PROVISIONING_PROVIDERS = [...SPAWN_PROVIDERS, "any"];
34
37
  const GITHUB_API = "https://api.github.com";
35
38
  const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
36
39
  const DEFAULT_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
37
40
  const ENV_PLACEHOLDER = /^\$?\{?[A-Z_][A-Z0-9_]*\}?$/;
38
41
  const BUNDLED_SOURCE_TYPE = "bundled";
39
- const BUNDLED_RELAY_CLAUDE_PLUGIN_REF = "agent-relay-claude-plugin";
40
- const BUNDLED_CODEX_SKILL_PREFIX = "agent-relay-codex-";
41
- const BUNDLED_CLAUDE_PLUGIN_DIR = resolve(import.meta.dir, "../../runner/plugins/claude");
42
- const BUNDLED_CODEX_SKILLS_DIR = resolve(import.meta.dir, "../../runner/plugins/codex/skills");
42
+
43
+ type ProviderBundledSkillSetDescriptor = NonNullable<NonNullable<ProviderManifest["provisioning"]>["bundledSkills"]>;
43
44
 
44
45
  interface ProvisioningAssetRow {
45
46
  id: string;
@@ -150,6 +151,8 @@ interface BundledFile {
150
151
  size: number;
151
152
  }
152
153
 
154
+ type BundledFileReadOptions = { omitFiles?: string[]; fileOverrides?: Array<{ path: string; sourcePath: string }> };
155
+
153
156
  export interface ProvisioningCapabilityWithVariants {
154
157
  capability: ProvisioningCapability;
155
158
  variants: ProvisioningAsset[];
@@ -345,11 +348,11 @@ function parseJsonFile(file: VendoredFile, label: string): Record<string, unknow
345
348
  }
346
349
 
347
350
  function assertPlugin(files: VendoredFile[], provider: AgentProfileProvider): void {
348
- const manifests = [
349
- { provider: "claude" as const, path: ".claude-plugin/plugin.json" },
350
- { provider: "codex" as const, path: ".codex-plugin/plugin.json" },
351
- ];
351
+ const manifests = SPAWN_PROVIDERS.flatMap((manifestProvider) => getManifest(manifestProvider)?.provisioning?.pluginManifestPath
352
+ ? [{ provider: manifestProvider, path: getManifest(manifestProvider)!.provisioning!.pluginManifestPath! }]
353
+ : []);
352
354
  const expected = provider === "any" ? manifests : manifests.filter((m) => m.provider === provider);
355
+ if (!expected.length) throw new ValidationError(`plugin import has no manifest descriptor configured for provider ${provider}`);
353
356
  const present = expected.filter((manifest) => files.some((file) => file.path === manifest.path));
354
357
  if (!present.length) throw new ValidationError(`plugin import requires ${expected.map((m) => m.path).join(" or ")}`);
355
358
  for (const manifest of present) {
@@ -455,22 +458,22 @@ function fileProvenance(files: VendoredFile[]): Array<{ path: string; sha: strin
455
458
  return files.map(({ path, sha, size }) => ({ path, sha, size }));
456
459
  }
457
460
 
458
- function readBundledFiles(root: string): BundledFile[] {
461
+ function readBundledFiles(root: string, options: BundledFileReadOptions = {}): BundledFile[] {
459
462
  if (!existsSync(root)) return [];
460
463
  const files: BundledFile[] = [];
464
+ const omitted = new Set(options.omitFiles ?? []);
465
+ const overrides = new Map((options.fileOverrides ?? []).map((item) => [item.path, item.sourcePath]));
461
466
  const visit = (dir: string, prefix = "") => {
462
467
  const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
463
468
  for (const entry of entries) {
464
469
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
465
470
  const cleanRel = cleanRelativePath(rel, "file path");
466
- if (root === BUNDLED_CLAUDE_PLUGIN_DIR && cleanRel === "monitors/relay-monitor.provisioned.mjs") continue;
471
+ if (omitted.has(cleanRel)) continue;
467
472
  const abs = join(dir, entry.name);
468
- if (entry.isDirectory()) {
469
- visit(abs, rel);
470
- continue;
471
- }
473
+ if (entry.isDirectory()) { visit(abs, rel); continue; }
472
474
  if (!entry.isFile()) continue;
473
- const content = root === BUNDLED_CLAUDE_PLUGIN_DIR && cleanRel === "monitors/relay-monitor.ts" ? readFileSync(join(root, "monitors/relay-monitor.provisioned.mjs"), "utf8") : readFileSync(abs, "utf8");
475
+ const overrideSource = overrides.get(cleanRel);
476
+ const content = overrideSource ? readFileSync(join(root, overrideSource), "utf8") : readFileSync(abs, "utf8");
474
477
  files.push({ path: cleanRel, content, size: Buffer.byteLength(content, "utf8") });
475
478
  }
476
479
  };
@@ -507,51 +510,35 @@ function upsertBundledProvisioningAsset(input: {
507
510
  description: string;
508
511
  }): void {
509
512
  if (!input.files.length) return;
510
- const definition = {
511
- kind: input.kind,
512
- name: input.name,
513
- files: input.files.map(({ path, content }) => ({ path, content })),
514
- } as SkillAssetDefinition | PluginAssetDefinition;
513
+ const definition = { kind: input.kind, name: input.name, files: input.files.map(({ path, content }) => ({ path, content })) } as SkillAssetDefinition | PluginAssetDefinition;
515
514
  assertDefinition(input.kind, definition);
516
- const asset = upsertProvisioningAsset({
517
- kind: input.kind,
518
- name: input.name,
519
- provider: input.provider,
520
- definition,
521
- });
515
+ const asset = upsertProvisioningAsset({ kind: input.kind, name: input.name, provider: input.provider, definition });
522
516
  updateBundledAssetMetadata(asset, input.root, input.files, input.title, input.description);
523
517
  }
524
518
 
525
- function bundledCodexSkillRefs(): string[] {
526
- if (!existsSync(BUNDLED_CODEX_SKILLS_DIR)) return [];
527
- return readdirSync(BUNDLED_CODEX_SKILLS_DIR, { withFileTypes: true })
528
- .filter((entry) => entry.isDirectory() && existsSync(join(BUNDLED_CODEX_SKILLS_DIR, entry.name, "SKILL.md")))
529
- .map((entry) => `${BUNDLED_CODEX_SKILL_PREFIX}${entry.name}`)
530
- .sort();
519
+ function bundledSkillRefs(descriptor: ProviderBundledSkillSetDescriptor): string[] {
520
+ const skillsDir = resolve(import.meta.dir, "../../", descriptor.dir);
521
+ const entryFile = descriptor.entryFile ?? "SKILL.md";
522
+ if (!existsSync(skillsDir)) return [];
523
+ return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync(join(skillsDir, entry.name, entryFile)))
524
+ .map((entry) => `${descriptor.refPrefix}${entry.name}`).sort();
531
525
  }
532
526
 
533
527
  export function seedBundledProvisioningAssets(): void {
534
- upsertBundledProvisioningAsset({
535
- kind: "plugin",
536
- name: BUNDLED_RELAY_CLAUDE_PLUGIN_REF,
537
- provider: "claude",
538
- root: "runner/plugins/claude",
539
- files: readBundledFiles(BUNDLED_CLAUDE_PLUGIN_DIR),
540
- title: "Agent Relay Claude plugin",
541
- description: "Bundled Agent Relay Claude plugin, materialized whole through the provisioning registry.",
542
- });
543
-
544
- for (const ref of bundledCodexSkillRefs()) {
545
- const skillName = ref.slice(BUNDLED_CODEX_SKILL_PREFIX.length);
546
- upsertBundledProvisioningAsset({
547
- kind: "skill",
548
- name: ref,
549
- provider: "codex",
550
- root: `runner/plugins/codex/skills/${skillName}`,
551
- files: readBundledFiles(join(BUNDLED_CODEX_SKILLS_DIR, skillName)),
552
- title: `Agent Relay Codex skill: ${skillName}`,
553
- description: "Bundled Agent Relay Codex skill, materialized through the provisioning registry.",
554
- });
528
+ for (const provider of SPAWN_PROVIDERS) {
529
+ const provisioning = getManifest(provider)?.provisioning;
530
+ if (!provisioning) continue;
531
+ if (provisioning.bundledPlugin) {
532
+ const plugin = provisioning.bundledPlugin;
533
+ upsertBundledProvisioningAsset({ kind: "plugin", name: plugin.ref, provider, root: plugin.dir, files: readBundledFiles(resolve(import.meta.dir, "../../", plugin.dir), { omitFiles: plugin.omitFiles, fileOverrides: plugin.fileOverrides }), title: plugin.title, description: plugin.description });
534
+ }
535
+ if (!provisioning.bundledSkills) continue;
536
+ const skills = provisioning.bundledSkills;
537
+ const skillsDir = resolve(import.meta.dir, "../../", skills.dir);
538
+ for (const ref of bundledSkillRefs(skills)) {
539
+ const skillName = ref.slice(skills.refPrefix.length);
540
+ upsertBundledProvisioningAsset({ kind: "skill", name: ref, provider, root: posix.join(skills.dir, skillName), files: readBundledFiles(join(skillsDir, skillName)), title: `${skills.titlePrefix}${skillName}`, description: skills.description });
541
+ }
555
542
  }
556
543
  }
557
544
 
@@ -1034,7 +1021,7 @@ function resolveRegistryDefinition(kind: ProvisioningAssetKind, ref: string, pro
1034
1021
  .all(capability.id, provider) as ProvisioningAssetRow[];
1035
1022
  const providerCandidates = rows.filter((row) => kind === "mcp" ? row.provider === "any" : row.provider === provider || row.provider === "any");
1036
1023
  if (!providerCandidates.length) {
1037
- const label = provider === "codex" ? "Codex" : "Claude";
1024
+ const label = getManifest(provider)?.label ?? provider;
1038
1025
  return { definition: null, skip: skip(kind, ref, provider, "provider_mismatch", `No ${label} variant exists for capability ${ref}.`) };
1039
1026
  }
1040
1027
 
@@ -1059,13 +1046,14 @@ function hasAssetRef(refs: AgentProfileAssetRef[], source: AgentProfileAssetRef[
1059
1046
  function relayBundledProvisioningRefs(profile: AgentProfile, provider: SpawnProvider): Pick<AgentProfile, "skills" | "plugins"> {
1060
1047
  const skills = [...(profile.skills ?? [])];
1061
1048
  const plugins = [...(profile.plugins ?? [])];
1062
- if (provider === "codex" && profile.relay.skills !== false) {
1063
- for (const ref of bundledCodexSkillRefs()) {
1064
- if (!hasAssetRef(skills, "relay", ref)) skills.push({ source: "relay", ref, enabled: true, provider: "codex" });
1049
+ const provisioning = getManifest(provider)?.provisioning;
1050
+ if (profile.relay.skills !== false && provisioning?.bundledSkills) {
1051
+ for (const ref of bundledSkillRefs(provisioning.bundledSkills)) {
1052
+ if (!hasAssetRef(skills, "relay", ref)) skills.push({ source: "relay", ref, enabled: true, provider });
1065
1053
  }
1066
1054
  }
1067
- if (provider === "claude" && profile.relay.plugins !== false && !hasAssetRef(plugins, "relay", BUNDLED_RELAY_CLAUDE_PLUGIN_REF)) {
1068
- plugins.push({ source: "relay", ref: BUNDLED_RELAY_CLAUDE_PLUGIN_REF, enabled: true, provider: "claude" });
1055
+ if (profile.relay.plugins !== false && provisioning?.bundledPlugin && !hasAssetRef(plugins, "relay", provisioning.bundledPlugin.ref)) {
1056
+ plugins.push({ source: "relay", ref: provisioning.bundledPlugin.ref, enabled: true, provider });
1069
1057
  }
1070
1058
  return { skills, plugins };
1071
1059
  }
@@ -1,3 +1,4 @@
1
+ import { getManifest } from "agent-relay-providers";
1
2
  import { getAgentProfile } from "./config-store";
2
3
  import { withResolvedProvisioning } from "./db";
3
4
  import { runnerRuntimeTokenEnv } from "./runtime-tokens";
@@ -5,15 +6,19 @@ import { buildSpawnCommand, resolveSpawnModelParams } from "./spawn-command";
5
6
  import { applyDefaultSharedMcp } from "./shared-mcp";
6
7
  import type { SpawnPolicy, WorkspaceMode } from "./types";
7
8
 
9
+ function managedPolicyPromptMode(policy: SpawnPolicy): "prompt" | "append-system-prompt" {
10
+ return getManifest(policy.provider)?.launch?.managedPolicyPrompt?.alwaysOn ?? "prompt";
11
+ }
12
+
8
13
  export function managedPolicyProviderArgs(policy: SpawnPolicy): string[] {
9
- if (policy.provider === "claude" && policy.mode === "always-on" && policy.prompt?.trim()) {
14
+ if (policy.mode === "always-on" && policy.prompt?.trim() && managedPolicyPromptMode(policy) === "append-system-prompt") {
10
15
  return [...policy.providerArgs, "--append-system-prompt", policy.prompt.trim()];
11
16
  }
12
17
  return policy.providerArgs;
13
18
  }
14
19
 
15
20
  export function managedPolicyLaunchPrompt(policy: SpawnPolicy): string | undefined {
16
- if (policy.provider === "claude" && policy.mode === "always-on") return undefined;
21
+ if (policy.mode === "always-on" && managedPolicyPromptMode(policy) === "append-system-prompt") return undefined;
17
22
  return policy.prompt;
18
23
  }
19
24
 
@@ -71,7 +71,7 @@ export const SPAWN_TOOLS: ToolDefinition[] = [
71
71
  required: ["branch", "mode"],
72
72
  additionalProperties: false,
73
73
  },
74
- waitForRegistrationMs: { type: "integer", minimum: 0, maximum: 30000, description: "How long to wait for the spawned agent to register before returning, so the response carries its resolved agent id (default 8000; 0 = return immediately with just spawnRequestId)." },
74
+ waitForRegistrationMs: { type: "integer", minimum: 0, maximum: 30000, description: "How long to wait for the spawned agent to register before returning, so the response carries its resolved agent id (default 0 = fire-and-forget; you will be PUSHED agent.ready when the child is ready)." },
75
75
  },
76
76
  required: ["provider"],
77
77
  additionalProperties: false,
@@ -158,7 +158,7 @@ export async function relaySpawnAgent(auth: McpAuthContext, args: Record<string,
158
158
  providerArgs: optionalStringArray(args.providerArgs, "providerArgs"),
159
159
  spawnRequestId: optionalString(args.spawnRequestId, "spawnRequestId", 160),
160
160
  requestedVia: "mcp",
161
- waitForRegistrationMs: optionalNonNegativeInt(args.waitForRegistrationMs, "waitForRegistrationMs") ?? 8000,
161
+ waitForRegistrationMs: optionalNonNegativeInt(args.waitForRegistrationMs, "waitForRegistrationMs") ?? 0,
162
162
  resumeWorkspace: parseResumeWorkspace(args.resumeWorkspace),
163
163
  };
164
164
  const result = await spawnAgent(input, authContextFromMcp(auth));
@@ -1,4 +1,4 @@
1
- import { APPROVAL_MODES, VALID_AGENT_LIFECYCLES, VALID_WORKSPACE_MODES } from "agent-relay-sdk";
1
+ import { APPROVAL_MODES, SPAWN_PROVIDERS, VALID_AGENT_LIFECYCLES, VALID_WORKSPACE_MODES } from "agent-relay-sdk";
2
2
  import { McpAuthError } from "./mcp-errors";
3
3
  import { authContextFromMcp } from "./services/auth-context";
4
4
  import { deleteTeamTemplate, getTeamTemplate, instantiateTeamTemplate, listTeamTemplates, saveTeamTemplate } from "./services/team-templates";
@@ -9,7 +9,7 @@ const COMPOSITION_ENTRY_SCHEMA = {
9
9
  type: "object",
10
10
  properties: {
11
11
  label: { type: "string" },
12
- provider: { type: "string", enum: ["claude", "codex"] },
12
+ provider: { type: "string", enum: SPAWN_PROVIDERS },
13
13
  model: { type: "string" },
14
14
  effort: { type: "string" },
15
15
  profile: { type: "string" },
@@ -138,7 +138,7 @@ export function mergeProviderCatalog(
138
138
  }
139
139
 
140
140
  function normalizeOverrideInput(input: ProviderModelOverrideInput): ProviderModelOverrideInput {
141
- if (!SPAWN_PROVIDERS.includes(input.provider)) throw new ValidationError("provider must be claude or codex");
141
+ if (!SPAWN_PROVIDERS.includes(input.provider)) throw new ValidationError(`provider must be one of: ${SPAWN_PROVIDERS.join(", ")}`);
142
142
  const alias = input.alias.trim();
143
143
  if (!alias) throw new ValidationError("model alias is required");
144
144
  if (input.entry.alias !== alias) throw new ValidationError("model entry alias must match override alias");
@@ -1,6 +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 { errMessage, isRecord } from "agent-relay-sdk";
3
+ import { errMessage, isRecord, isSpawnProvider, SPAWN_PROVIDERS } from "agent-relay-sdk";
4
4
  import { effectiveProviderCatalogList } from "./provider-catalog-store";
5
5
 
6
6
  class RecipeValidationError extends Error {}
@@ -41,7 +41,9 @@ export function validateRecipe(value: unknown): Recipe {
41
41
  function validateAgent(value: unknown): RecipeAgent {
42
42
  if (!isRecord(value)) throw new RecipeValidationError("agent must be an object");
43
43
  const provider = value.provider;
44
- if (provider !== "claude" && provider !== "codex") throw new RecipeValidationError("agent.provider must be claude or codex");
44
+ if (!isSpawnProvider(provider)) {
45
+ throw new RecipeValidationError(`agent.provider must be one of: ${SPAWN_PROVIDERS.join(", ")}`);
46
+ }
45
47
  const capabilities = stringArray(value.capabilities, "agent.capabilities");
46
48
  if (capabilities.length === 0) throw new RecipeValidationError("agent.capabilities must not be empty");
47
49
  const approvalMode = value.approvalMode;
@@ -5,7 +5,7 @@ import { deleteAgentProfile, getAgentProfile, listAgentProfiles, setAgentProfile
5
5
  import { emitConfigChanged } from "../sse";
6
6
  import { error, isRootCredentialRequest, json, normalizeConfigPathParam, parseBody, type Handler } from "./_shared";
7
7
  import { getComponentAuth, hasComponentScope } from "../security";
8
- import { isRecord } from "agent-relay-sdk";
8
+ import { isRecord, isSpawnProvider } from "agent-relay-sdk";
9
9
  import { SPAWN_PROVIDERS, type AgentProfile, type SpawnProvider } from "../types";
10
10
 
11
11
  export const getAgentProfilesRoute: Handler = () => json(listAgentProfiles());
@@ -29,7 +29,7 @@ export const getResolvedAgentProfileRoute: Handler = (req, params) => {
29
29
  const entry = getAgentProfile(name);
30
30
  if (!entry) return error("agent profile not found", 404);
31
31
  const requestedProvider = new URL(req.url).searchParams.get("provider") ?? undefined;
32
- const profileProvider = entry.value.provider === "claude" || entry.value.provider === "codex" ? entry.value.provider : undefined;
32
+ const profileProvider = isSpawnProvider(entry.value.provider) ? entry.value.provider : undefined;
33
33
  const provider = optionalEnum(requestedProvider, "provider", SPAWN_PROVIDERS, profileProvider ?? "codex") as SpawnProvider;
34
34
  const profile = withResolvedProvisioning(entry.value, provider);
35
35
  return profile ? json({ provider, profile }) : error("agent profile not found", 404);