agent-relay-server 0.102.1 → 0.103.4

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 (132) 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/channels.ts +30 -2
  91. package/src/db/provider-quota-burn.ts +37 -5
  92. package/src/db/provider-quotas.ts +11 -2
  93. package/src/db/provisioning-registry.ts +49 -61
  94. package/src/managed-policy.ts +7 -2
  95. package/src/mcp/tools-spawn.ts +2 -2
  96. package/src/mcp-team-template-tools.ts +2 -2
  97. package/src/provider-catalog-store.ts +1 -1
  98. package/src/quota-band-transition.ts +59 -8
  99. package/src/recipe-validator.ts +4 -2
  100. package/src/routes/agent-profiles.ts +2 -2
  101. package/src/routes/agent-sessions.ts +18 -23
  102. package/src/routes/provider-config.ts +1 -1
  103. package/src/spawn-targets.ts +32 -4
  104. package/src/sse.ts +12 -1
  105. package/src/upgrade.ts +120 -44
  106. package/src/workspace-phase.ts +1 -0
  107. package/public/assets/MessageBubble-Dppo7pil.js +0 -4
  108. package/public/assets/agent-profiles-CT-vbfzd.js +0 -2
  109. package/public/assets/agent-profiles-CT-vbfzd.js.map +0 -1
  110. package/public/assets/agents-CLcgi8AD.js +0 -2
  111. package/public/assets/agents-CLcgi8AD.js.map +0 -1
  112. package/public/assets/automation-CE1i1zpw.js +0 -2
  113. package/public/assets/automation-CE1i1zpw.js.map +0 -1
  114. package/public/assets/chat-DHdFZvAg.js +0 -2
  115. package/public/assets/chat-DHdFZvAg.js.map +0 -1
  116. package/public/assets/display-DRiGi1zj.js +0 -3
  117. package/public/assets/display-DRiGi1zj.js.map +0 -1
  118. package/public/assets/index-CKhh9XRZ.css +0 -2
  119. package/public/assets/index-DeDosfVE.js +0 -23
  120. package/public/assets/index-DeDosfVE.js.map +0 -1
  121. package/public/assets/maintenance-DVx3SiC_.js +0 -2
  122. package/public/assets/managed-agents-DUyOTdz1.js.map +0 -1
  123. package/public/assets/overview-D9BxevAq.js +0 -2
  124. package/public/assets/registry-BLjwVXCW.js +0 -2
  125. package/public/assets/registry-BLjwVXCW.js.map +0 -1
  126. package/public/assets/settings-BR30UpbJ.js +0 -6
  127. package/public/assets/settings-BR30UpbJ.js.map +0 -1
  128. package/public/assets/store-DNbkrqbU.js +0 -9
  129. package/public/assets/store-DNbkrqbU.js.map +0 -1
  130. package/public/assets/teams-B_HSkU9T.js +0 -2
  131. package/public/assets/types-Tv67voFu.js +0 -2
  132. 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));
@@ -284,7 +284,35 @@ function channelTargetHealth(binding: ChannelBinding, now: number = Date.now()):
284
284
  };
285
285
  }
286
286
 
287
+ // #797 — outbound (egress-only) channel liveness. Bidirectional channels (e.g. Telegram)
288
+ // have a backing pool agent that heartbeats, so their agent `lastSeen`/`status` track
289
+ // reality. Outbound-only channels (e.g. ntfy) are just a channels-host daemon SSE
290
+ // subscription with NO agent behind them to heartbeat — so the registration's `lastSeen`
291
+ // goes stale and the offline sweep flips it offline even while the daemon's subscription is
292
+ // live and delivery works. For these, liveness is "is the channels-host subscribed for this
293
+ // channel agent on the SSE bus?", not heartbeat freshness. A late-bound probe (not a direct
294
+ // import) because sse.ts imports the db barrel — sse registers this at startup, mirroring
295
+ // `onWorkspaceChange`.
296
+ type ChannelSubscriberProbe = (channelAgentId: string) => boolean;
297
+ let channelSubscriberProbe: ChannelSubscriberProbe | null = null;
298
+ export function setChannelSubscriberProbe(probe: ChannelSubscriberProbe | null): void {
299
+ channelSubscriberProbe = probe;
300
+ }
301
+ function hasActiveChannelSubscriber(channelAgentId: string): boolean {
302
+ try {
303
+ return channelSubscriberProbe ? channelSubscriberProbe(channelAgentId) : false;
304
+ } catch {
305
+ return false;
306
+ }
307
+ }
308
+
287
309
  function rowToChannelSummary(row: any): ChannelSummary {
310
+ // Outbound channels report online iff their channels-host SSE subscription is active,
311
+ // overriding the heartbeat-derived agent status/ready (which is meaningless for an
312
+ // agentless egress channel). Inbound/bidirectional keep the backing agent's signal.
313
+ const subscriberLive = row.direction === "outbound" ? hasActiveChannelSubscriber(row.agent_id) : null;
314
+ const status: ChannelSummary["status"] = subscriberLive === null ? row.agent_status : (subscriberLive ? "online" : "offline");
315
+ const ready = subscriberLive === null ? row.agent_ready === 1 : subscriberLive;
288
316
  const binding = row.binding_id ? rowToChannelBinding({
289
317
  id: row.binding_id,
290
318
  channel_id: row.id,
@@ -308,8 +336,8 @@ function rowToChannelSummary(row: any): ChannelSummary {
308
336
  transport: row.transport,
309
337
  agentId: row.agent_id,
310
338
  accountId: row.account_id,
311
- status: row.agent_status,
312
- ready: row.agent_ready === 1,
339
+ status,
340
+ ready,
313
341
  direction: row.direction,
314
342
  target: binding ? bindingTargetToLegacyTarget(binding.target) : undefined,
315
343
  binding,
@@ -15,6 +15,17 @@ const WINDOW_NOMINAL_MS: Record<ProviderQuotaBurnRole, number> = {
15
15
  // observed rate (and burnRatio) back toward 0 instead of freezing the endpoint slope.
16
16
  const RECENCY_HALFLIFE_FRACTION = 0.25;
17
17
 
18
+ // #798 — idle de-bias: once a provider's utilization has held flat for this long, any residual
19
+ // forward-burn rate is a stale-burst artifact (the recency half-life decays it, but for the 7d
20
+ // window — half-life ~42h — far too slowly: codex idle ~24h still projected ~1.07 at reset,
21
+ // walking the band boundary tick-to-tick). When idle this long the forward-burn term is collapsed
22
+ // so projectedUtilAtReset → current utilization. The threshold is absolute (NOT window-scaled): a
23
+ // 5h window resets long before reaching it, so this only de-biases the slow-decaying 7d window;
24
+ // shorter idle stretches keep the graceful #760 recency decay. The over-target avoid is preserved
25
+ // by the position penalty in spawn-targets (a stable, position-based floor), not by this rate.
26
+ const IDLE_FORWARD_COLLAPSE_MS = 6 * 3_600_000;
27
+ const IDLE_UTILIZATION_EPSILON = 1e-9;
28
+
18
29
  type QuotaWindow = QuotaState["windows"][number];
19
30
 
20
31
  // Provider-agnostic: window role is expressed by name only. Each adapter uses the
@@ -80,6 +91,19 @@ function recencyWeightedRatePerMs(
80
91
  return Math.max(0, weightedDelta / weightedDt);
81
92
  }
82
93
 
94
+ // #798 — when did utilization last differ from its current value? Scanning back from the newest
95
+ // sample, the first sample that differs marks the last change; everything after it (up to now) is
96
+ // the flat idle stretch. If every sample equals the current value the window has been flat since
97
+ // the oldest sample. Samples are assumed sorted ascending by capturedAt.
98
+ function lastUtilizationChangeAt(samples: Array<{ capturedAt: number; utilization: number }>, current: number): number {
99
+ for (let i = samples.length - 1; i >= 0; i--) {
100
+ if (Math.abs(samples[i]!.utilization - current) > IDLE_UTILIZATION_EPSILON) {
101
+ return samples[i + 1]?.capturedAt ?? samples[i]!.capturedAt;
102
+ }
103
+ }
104
+ return samples[0]?.capturedAt ?? 0;
105
+ }
106
+
83
107
  function deriveProviderQuotaBurnWindow(input: {
84
108
  provider: string;
85
109
  currentWindow: QuotaWindow;
@@ -137,17 +161,25 @@ function deriveProviderQuotaBurnWindow(input: {
137
161
  ? [...samples, { capturedAt: input.now, utilization: utilizationNow }]
138
162
  : samples;
139
163
  const observedRatePerMs = recencyWeightedRatePerMs(rateSamples, input.now, halfLifeMs);
164
+ // #798 — idle de-bias: if utilization has held flat (no change vs. now) for IDLE_FORWARD_COLLAPSE_MS,
165
+ // treat the forward-burn term as a stale-burst artifact and collapse it to 0 so the projection
166
+ // pins to current utilization instead of walking the band boundary. lastChangeAt is the moment
167
+ // utilization last differed from the current value (the synthetic "still X as of now" interval is
168
+ // what makes a deduped idle stretch observable here).
169
+ const lastChangeAt = lastUtilizationChangeAt(samples, utilizationNow);
170
+ const effectiveRate = input.now - lastChangeAt >= IDLE_FORWARD_COLLAPSE_MS ? 0 : observedRatePerMs;
140
171
  // #760 (Defect 2) — pace against a configurable target (<100%); slack carries the underburn
141
172
  // signal. The query layer (spawn-targets) may re-derive these against a per-provider target;
142
173
  // the ingest-time values use the default so direct burn consumers (dashboard) have a number.
143
174
  const pace = quotaPaceMetrics({
144
175
  utilization: utilizationNow,
145
- observedRatePerMs,
176
+ observedRatePerMs: effectiveRate,
146
177
  timeToResetMs,
147
178
  target: DEFAULT_QUOTA_TARGET_UTILIZATION,
148
179
  });
149
- if (observedRatePerMs <= 0) {
150
- // Idle (or decayed-to-idle): no projected wall, but slack still flags the headroom.
180
+ if (effectiveRate <= 0) {
181
+ // Idle (decayed-to-idle, or #798 idle-collapsed): no projected wall, but slack still flags the
182
+ // headroom (positive = underburn) or, when over target, the position the band-floor avoids.
151
183
  return {
152
184
  ...low,
153
185
  confidence: "high",
@@ -158,13 +190,13 @@ function deriveProviderQuotaBurnWindow(input: {
158
190
  target: pace.target,
159
191
  };
160
192
  }
161
- const timeToWallMs = remainingQuota / observedRatePerMs;
193
+ const timeToWallMs = remainingQuota / effectiveRate;
162
194
  const projectedWallAt = input.now + timeToWallMs;
163
195
  return {
164
196
  ...low,
165
197
  confidence: "high",
166
198
  burnRatio: pace.burnRatio,
167
- observedRatePerMs,
199
+ observedRatePerMs: effectiveRate,
168
200
  timeToWallMs,
169
201
  projectedWallAt,
170
202
  // #760 (Defect 2) — symmetric: negative means the wall lands AFTER reset (headroom left).
@@ -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