agent-relay-server 0.120.4 → 0.120.6

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 (109) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +3 -3
  3. package/public/assets/{MessageBubble-Cqi3XsZ7.js → MessageBubble-So86mU9-.js} +2 -2
  4. package/public/assets/{MessageBubble-Cqi3XsZ7.js.map → MessageBubble-So86mU9-.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-D6Um05oW.js → PlanGraphPanel-DBZYAsww.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-D6Um05oW.js.map → PlanGraphPanel-DBZYAsww.js.map} +1 -1
  7. package/public/assets/{activity-B0LsQHEz.js → activity-CZNwmXg1.js} +2 -2
  8. package/public/assets/{activity-B0LsQHEz.js.map → activity-CZNwmXg1.js.map} +1 -1
  9. package/public/assets/{agent-profiles-O--pe5Ic.js → agent-profiles-BhadoGwi.js} +2 -2
  10. package/public/assets/{agent-profiles-O--pe5Ic.js.map → agent-profiles-BhadoGwi.js.map} +1 -1
  11. package/public/assets/agents-DWn1xx9x.js +2 -0
  12. package/public/assets/{agents-Ii6XlVwR.js.map → agents-DWn1xx9x.js.map} +1 -1
  13. package/public/assets/{analytics-BCzdYWRT.js → analytics-DifGnk7q.js} +2 -2
  14. package/public/assets/{analytics-BCzdYWRT.js.map → analytics-DifGnk7q.js.map} +1 -1
  15. package/public/assets/{automation-Crr3Xzui.js → automation-BExrtKkb.js} +2 -2
  16. package/public/assets/{automation-Crr3Xzui.js.map → automation-BExrtKkb.js.map} +1 -1
  17. package/public/assets/{branch-state-badge-Bhw2rloy.js → branch-state-badge-CyC5Qrhc.js} +2 -2
  18. package/public/assets/{branch-state-badge-Bhw2rloy.js.map → branch-state-badge-CyC5Qrhc.js.map} +1 -1
  19. package/public/assets/{channels-CKs9rvsn.js → channels-BLkdt2XR.js} +2 -2
  20. package/public/assets/{channels-CKs9rvsn.js.map → channels-BLkdt2XR.js.map} +1 -1
  21. package/public/assets/chat-C7OhKtr6.js +2 -0
  22. package/public/assets/{chat-BCwEUj5R.js.map → chat-C7OhKtr6.js.map} +1 -1
  23. package/public/assets/{connectors-D_kQR_vw.js → connectors-C0n12Je8.js} +2 -2
  24. package/public/assets/{connectors-D_kQR_vw.js.map → connectors-C0n12Je8.js.map} +1 -1
  25. package/public/assets/{formatted-body-q_qzt1L-.js → formatted-body-BCQT2qSb.js} +3 -3
  26. package/public/assets/{formatted-body-q_qzt1L-.js.map → formatted-body-BCQT2qSb.js.map} +1 -1
  27. package/public/assets/{formatted-body-impl-CSRGuMcM.js → formatted-body-impl-Bk7wnESl.js} +2 -2
  28. package/public/assets/{formatted-body-impl-CSRGuMcM.js.map → formatted-body-impl-Bk7wnESl.js.map} +1 -1
  29. package/public/assets/index-CRraCeo_.js +25 -0
  30. package/public/assets/index-CRraCeo_.js.map +1 -0
  31. package/public/assets/{insights-DUhXqZTZ.js → insights-DEgES6jz.js} +2 -2
  32. package/public/assets/{insights-DUhXqZTZ.js.map → insights-DEgES6jz.js.map} +1 -1
  33. package/public/assets/{integrations-lbg5CjtE.js → integrations-v14ehskT.js} +2 -2
  34. package/public/assets/{integrations-lbg5CjtE.js.map → integrations-v14ehskT.js.map} +1 -1
  35. package/public/assets/{maintenance-CQgneSVf.js → maintenance-BgVB6_gO.js} +2 -2
  36. package/public/assets/{maintenance-CQgneSVf.js.map → maintenance-BgVB6_gO.js.map} +1 -1
  37. package/public/assets/{managed-agents-DsnYf6fm.js → managed-agents-8_NKSkhI.js} +3 -3
  38. package/public/assets/{managed-agents-DsnYf6fm.js.map → managed-agents-8_NKSkhI.js.map} +1 -1
  39. package/public/assets/{markdown-preview-impl-C1ZtGJp6.js → markdown-preview-impl-yxQkrd2M.js} +2 -2
  40. package/public/assets/{markdown-preview-impl-C1ZtGJp6.js.map → markdown-preview-impl-yxQkrd2M.js.map} +1 -1
  41. package/public/assets/{memory-SSLUTOI2.js → memory-CJD8eekg.js} +2 -2
  42. package/public/assets/{memory-SSLUTOI2.js.map → memory-CJD8eekg.js.map} +1 -1
  43. package/public/assets/{messages-BY26OP4K.js → messages-DLHMZbHZ.js} +2 -2
  44. package/public/assets/{messages-BY26OP4K.js.map → messages-DLHMZbHZ.js.map} +1 -1
  45. package/public/assets/{orchestrators-OY7yPzVI.js → orchestrators-DEOep7FX.js} +2 -2
  46. package/public/assets/{orchestrators-OY7yPzVI.js.map → orchestrators-DEOep7FX.js.map} +1 -1
  47. package/public/assets/{overview-Dj0u1tPG.js → overview-BgpfO6mt.js} +2 -2
  48. package/public/assets/{overview-Dj0u1tPG.js.map → overview-BgpfO6mt.js.map} +1 -1
  49. package/public/assets/{pairs-CrUR50ou.js → pairs-8NVKHh96.js} +2 -2
  50. package/public/assets/{pairs-CrUR50ou.js.map → pairs-8NVKHh96.js.map} +1 -1
  51. package/public/assets/{projects-B_AtxHtH.js → projects-CZJW35C7.js} +2 -2
  52. package/public/assets/{projects-B_AtxHtH.js.map → projects-CZJW35C7.js.map} +1 -1
  53. package/public/assets/{prompt-settings-CPtYSban.js → prompt-settings-fqyLwUa-.js} +2 -2
  54. package/public/assets/{prompt-settings-CPtYSban.js.map → prompt-settings-fqyLwUa-.js.map} +1 -1
  55. package/public/assets/{registry-BgUALWn5.js → registry-BKbLL4mO.js} +2 -2
  56. package/public/assets/{registry-BgUALWn5.js.map → registry-BKbLL4mO.js.map} +1 -1
  57. package/public/assets/{security-5lHfGk7g.js → security-BlTFTbq0.js} +2 -2
  58. package/public/assets/{security-5lHfGk7g.js.map → security-BlTFTbq0.js.map} +1 -1
  59. package/public/assets/{select-Cgx4QSLj.js → select-Zd_qh0uU.js} +2 -2
  60. package/public/assets/{select-Cgx4QSLj.js.map → select-Zd_qh0uU.js.map} +1 -1
  61. package/public/assets/{settings-DFGgxyun.js → settings-nHuSVRkL.js} +2 -2
  62. package/public/assets/{settings-DFGgxyun.js.map → settings-nHuSVRkL.js.map} +1 -1
  63. package/public/assets/store-nXGxgz2Q.js +9 -0
  64. package/public/assets/store-nXGxgz2Q.js.map +1 -0
  65. package/public/assets/{tasks-BvH5rluu.js → tasks-C3JhQ8vB.js} +2 -2
  66. package/public/assets/{tasks-BvH5rluu.js.map → tasks-C3JhQ8vB.js.map} +1 -1
  67. package/public/assets/{teams-VZOIOdoE.js → teams-C9E9Yt39.js} +2 -2
  68. package/public/assets/{teams-VZOIOdoE.js.map → teams-C9E9Yt39.js.map} +1 -1
  69. package/public/assets/{terminal-viewer-impl-DYY3Wmoe.js → terminal-viewer-impl-FP0JlsvS.js} +22 -22
  70. package/public/assets/{terminal-viewer-impl-DYY3Wmoe.js.map → terminal-viewer-impl-FP0JlsvS.js.map} +1 -1
  71. package/public/assets/types-uVOXgvMT.js.map +1 -1
  72. package/public/assets/{work-queue-B6Xk7QNe.js → work-queue-Ema7Hy3s.js} +2 -2
  73. package/public/assets/{work-queue-B6Xk7QNe.js.map → work-queue-Ema7Hy3s.js.map} +1 -1
  74. package/public/assets/{workspaces-CPIzAHAe.js → workspaces-DI317fMk.js} +2 -2
  75. package/public/assets/{workspaces-CPIzAHAe.js.map → workspaces-DI317fMk.js.map} +1 -1
  76. package/public/index.html +3 -3
  77. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  78. package/src/auto-spawn-dispatch.ts +2 -2
  79. package/src/commands-db.ts +14 -0
  80. package/src/db/delivery.ts +85 -3
  81. package/src/db/index.ts +2 -0
  82. package/src/db/migrations.ts +2 -1
  83. package/src/db/provisioning-callmux.ts +180 -0
  84. package/src/db/provisioning-registry.ts +45 -374
  85. package/src/db/provisioning-resolve.ts +245 -0
  86. package/src/db/stats.ts +10 -8
  87. package/src/db/task-entity.ts +7 -8
  88. package/src/lifecycle-manager.ts +11 -12
  89. package/src/maintenance/jobs.ts +3 -1
  90. package/src/maintenance/workspaces.ts +28 -47
  91. package/src/managed-policy-escalation.ts +47 -0
  92. package/src/mcp-task-tools.ts +4 -5
  93. package/src/routes/commands.ts +8 -245
  94. package/src/routes/tasks.ts +3 -5
  95. package/src/routes/workspaces.ts +58 -22
  96. package/src/services/issue-lifecycle.ts +23 -8
  97. package/src/services/task-entity.ts +54 -2
  98. package/src/task-dispatcher.ts +12 -3
  99. package/src/workspace-auto-merge.ts +94 -34
  100. package/src/workspace-command-settle.ts +227 -0
  101. package/src/workspace-escalation.ts +45 -0
  102. package/src/workspace-merge-progress.ts +16 -0
  103. package/src/workspace-orphans.ts +96 -4
  104. package/public/assets/agents-Ii6XlVwR.js +0 -2
  105. package/public/assets/chat-BCwEUj5R.js +0 -2
  106. package/public/assets/index-Ceiwd02X.js +0 -25
  107. package/public/assets/index-Ceiwd02X.js.map +0 -1
  108. package/public/assets/store-BM4zM89B.js +0 -9
  109. package/public/assets/store-BM4zM89B.js.map +0 -1
@@ -1,15 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { Buffer } from "node:buffer";
3
3
  import { existsSync, readdirSync, readFileSync } from "node:fs";
4
- import { homedir } from "node:os";
5
4
  import { basename, join, posix, resolve } from "node:path";
6
- import { getManifest, type ProviderManifest } from "agent-relay-providers";
5
+ import { getManifest } from "agent-relay-providers";
7
6
  import { getDb, ValidationError } from "./connection.ts";
7
+ import { bundledSkillRefs, stringRecord } from "./provisioning-resolve.ts";
8
8
  import { parseJson } from "../utils";
9
9
  import { errMessage, isRecord, SPAWN_PROVIDERS } from "agent-relay-sdk";
10
10
  import type {
11
- AgentProfile,
12
- AgentProfileAssetRef,
13
11
  AgentProfileProvider,
14
12
  McpAssetDefinition,
15
13
  PluginAssetDefinition,
@@ -17,11 +15,8 @@ import type {
17
15
  ProvisioningAsset,
18
16
  ProvisioningAssetKind,
19
17
  ProvisioningMcpServer,
20
- ProvisioningSkip,
21
18
  ResolvedAssetDefinition,
22
- ResolvedProvisioning,
23
19
  SkillAssetDefinition,
24
- SpawnProvider,
25
20
  } from "agent-relay-sdk";
26
21
 
27
22
  // Relay-owned provisioning registry (#554/#555/#556). One table, one shape across
@@ -37,15 +32,13 @@ const GITHUB_API = "https://api.github.com";
37
32
  const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
38
33
  const DEFAULT_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
39
34
  const ENV_PLACEHOLDER = /^\$?\{?[A-Z_][A-Z0-9_]*\}?$/;
35
+ const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
36
+ const SECRETISH_ENV_NAME = /(^|_)(TOKEN|SECRET|PASSWORD|PASSWD|KEY|COOKIE|DSN|PRIVATE|AUTH|SESSION|CREDENTIALS?)(_|$)/i;
40
37
  const BUNDLED_SOURCE_TYPE = "bundled";
41
- const HOST_CALLMUX_SOURCE_TYPE = "host-callmux";
42
- export const SHARED_CALLMUX_SEED_SERVER_NAMES = ["tokenlean", "github", "vent", "reasoning", "scribe", "searxng", "qmd"] as const;
43
- export const SHARED_CALLMUX_SOURCE_CONFIG_ENV = "AGENT_RELAY_SHARED_CALLMUX_SOURCE_CONFIG";
44
- // Servers seeded from the host callmux config but NOT eligible for the shared listener (deno not provisioned, etc.).
45
- const SHARED_CALLMUX_SEED_NON_LISTENER_NAMES: ReadonlySet<string> = new Set(["scribe"]);
46
- const SHARED_CALLMUX_CONFIG_ENV_ALLOWLIST: Readonly<Record<string, readonly string[]>> = { vent: ["VENT_REPO"] };
47
-
48
- type ProviderBundledSkillSetDescriptor = NonNullable<NonNullable<ProviderManifest["provisioning"]>["bundledSkills"]>;
38
+ // Per-server allowlist of non-secret literal configEnv keys. Enforced by
39
+ // assertConfigEnv below and consumed by the shared-callmux seed scrub
40
+ // (src/db/provisioning-callmux.ts) when splitting literals from placeholders.
41
+ export const SHARED_CALLMUX_CONFIG_ENV_ALLOWLIST: Readonly<Record<string, readonly string[]>> = { vent: ["VENT_REPO"] };
49
42
 
50
43
  interface ProvisioningAssetRow {
51
44
  id: string;
@@ -145,23 +138,12 @@ interface ProvisioningCapabilityRow {
145
138
  updated_at?: number;
146
139
  }
147
140
 
148
- interface RegistryResolution {
149
- definition: ResolvedAssetDefinition | null;
150
- skip?: ProvisioningSkip;
151
- }
152
-
153
141
  interface BundledFile {
154
142
  path: string;
155
143
  content: string;
156
144
  size: number;
157
145
  }
158
146
 
159
- export interface SharedCallmuxSeedResult {
160
- sourceConfigPath: string;
161
- seeded: string[];
162
- skipped: Array<{ name: string; reason: string }>;
163
- }
164
-
165
147
  type BundledFileReadOptions = { omitFiles?: string[]; fileOverrides?: Array<{ path: string; sourcePath: string }> };
166
148
 
167
149
  export interface ProvisioningCapabilityWithVariants {
@@ -229,7 +211,7 @@ function assertDefinition(kind: ProvisioningAssetKind, definition: ResolvedAsset
229
211
  if (!isRecord(def.server)) throw new ValidationError("mcp definition requires a server object");
230
212
  assertMcpSecretPlaceholders(def.server, def.name);
231
213
  const metadata = isRecord(def.metadata) ? def.metadata : {};
232
- if (isRecord(metadata.callmuxServer)) assertMcpSecretPlaceholders(metadata.callmuxServer as ProvisioningMcpServer, def.name);
214
+ if (isRecord(metadata.callmuxServer)) assertCallmuxServerPlaceholders(metadata.callmuxServer as ProvisioningMcpServer, def.name);
233
215
  }
234
216
  if ((kind === "skill" || kind === "plugin")) {
235
217
  const def = definition as SkillAssetDefinition | PluginAssetDefinition;
@@ -378,23 +360,39 @@ function assertPlugin(files: VendoredFile[], provider: AgentProfileProvider): vo
378
360
  }
379
361
  }
380
362
 
381
- function isEnvPlaceholder(value: string): boolean {
363
+ export function isEnvPlaceholder(value: string): boolean {
382
364
  return ENV_PLACEHOLDER.test(value);
383
365
  }
384
366
 
367
+ function isSecretLikeEnvName(key: string): boolean {
368
+ return SECRETISH_ENV_NAME.test(key);
369
+ }
370
+
371
+ function assertConfigEnv(
372
+ value: unknown,
373
+ opts: { serverName?: string; allowlist?: readonly string[] } = {},
374
+ ): void {
375
+ if (value === undefined) return;
376
+ if (!isRecord(value)) throw new ValidationError("mcp configEnv must be an object");
377
+ for (const [key, raw] of Object.entries(value)) {
378
+ if (!ENV_NAME.test(key)) throw new ValidationError(`mcp configEnv key ${key} is not a valid env name`);
379
+ if (typeof raw !== "string") throw new ValidationError(`mcp configEnv ${key} must be a string`);
380
+ if (isEnvPlaceholder(raw)) throw new ValidationError(`mcp configEnv ${key} must be a literal value; use env for placeholders`);
381
+ if (opts.allowlist && !opts.allowlist.includes(key)) {
382
+ throw new ValidationError(`mcp configEnv ${key} is not allowlisted for ${opts.serverName ?? "this server"}`);
383
+ }
384
+ if (isSecretLikeEnvName(key)) {
385
+ throw new ValidationError(`mcp configEnv ${key} looks secret; use env placeholders instead`);
386
+ }
387
+ }
388
+ }
389
+
385
390
  function assertMcpSecretPlaceholders(server: ProvisioningMcpServer, serverName?: string): void {
386
391
  for (const [key, value] of Object.entries(server.env ?? {})) {
387
- if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) throw new ValidationError(`mcp env key ${key} is not a valid env name`);
392
+ if (!ENV_NAME.test(key)) throw new ValidationError(`mcp env key ${key} is not a valid env name`);
388
393
  if (!isEnvPlaceholder(value)) throw new ValidationError(`mcp env ${key} must reference an env placeholder name`);
389
394
  }
390
- const configEnv = (server as ProvisioningMcpServer & { configEnv?: unknown }).configEnv;
391
- if (configEnv !== undefined) {
392
- if (!isRecord(configEnv)) throw new ValidationError("mcp configEnv must be an object");
393
- for (const [key, value] of Object.entries(configEnv)) {
394
- if (typeof value !== "string") throw new ValidationError(`mcp configEnv ${key} must be a string`);
395
- if (!SHARED_CALLMUX_CONFIG_ENV_ALLOWLIST[serverName ?? ""]?.includes(key)) throw new ValidationError(`mcp configEnv ${key} is not allowlisted for ${serverName ?? "this server"}`);
396
- }
397
- }
395
+ assertConfigEnv(server.configEnv);
398
396
  for (const [key, value] of Object.entries(server.headers ?? {})) {
399
397
  if (/authorization|token|secret|key|api/i.test(key) && !/^Bearer \$\{[A-Z_][A-Z0-9_]*\}$/.test(value) && !isEnvPlaceholder(value)) {
400
398
  throw new ValidationError(`mcp header ${key} must reference an env placeholder name`);
@@ -402,6 +400,11 @@ function assertMcpSecretPlaceholders(server: ProvisioningMcpServer, serverName?:
402
400
  }
403
401
  }
404
402
 
403
+ function assertCallmuxServerPlaceholders(server: ProvisioningMcpServer, serverName?: string): void {
404
+ assertMcpSecretPlaceholders({ ...server, configEnv: undefined }, serverName);
405
+ assertConfigEnv(server.configEnv, { serverName, allowlist: SHARED_CALLMUX_CONFIG_ENV_ALLOWLIST[serverName ?? ""] });
406
+ }
407
+
405
408
  function cleanStringRecord(value: unknown, label: string): Record<string, string> | undefined {
406
409
  if (value === undefined) return undefined;
407
410
  if (!isRecord(value)) throw new ValidationError(`${label} must be an object`);
@@ -433,8 +436,10 @@ function coerceMcpServer(raw: unknown): ProvisioningMcpServer {
433
436
  server.url = raw.url;
434
437
  }
435
438
  const env = cleanStringRecord(raw.env, "mcp env");
439
+ const configEnv = cleanStringRecord(raw.configEnv, "mcp configEnv");
436
440
  const headers = cleanStringRecord(raw.headers, "mcp headers");
437
441
  if (env) server.env = env;
442
+ if (configEnv) server.configEnv = configEnv;
438
443
  if (headers) server.headers = headers;
439
444
  if (!server.command && !server.url) throw new ValidationError("mcp server requires command or url");
440
445
  if (server.command && server.url) throw new ValidationError("mcp server cannot set both command and url");
@@ -539,14 +544,6 @@ function upsertBundledProvisioningAsset(input: {
539
544
  updateBundledAssetMetadata(asset, input.root, input.files, input.title, input.description);
540
545
  }
541
546
 
542
- function bundledSkillRefs(descriptor: ProviderBundledSkillSetDescriptor): string[] {
543
- const skillsDir = resolve(import.meta.dir, "../../", descriptor.dir);
544
- const entryFile = descriptor.entryFile ?? "SKILL.md";
545
- if (!existsSync(skillsDir)) return [];
546
- return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync(join(skillsDir, entry.name, entryFile)))
547
- .map((entry) => `${descriptor.refPrefix}${entry.name}`).sort();
548
- }
549
-
550
547
  export function seedBundledProvisioningAssets(): void {
551
548
  for (const provider of SPAWN_PROVIDERS) {
552
549
  const provisioning = getManifest(provider)?.provisioning;
@@ -565,150 +562,9 @@ export function seedBundledProvisioningAssets(): void {
565
562
  }
566
563
  }
567
564
 
568
- export function sharedCallmuxSourceConfigPath(env: Record<string, string | undefined> = process.env): string {
569
- return env[SHARED_CALLMUX_SOURCE_CONFIG_ENV] || env.CALLMUX_CONFIG || join(homedir(), ".config", "callmux", "config.json");
570
- }
571
-
572
- function cloneJsonRecord(value: Record<string, unknown>): Record<string, unknown> {
573
- return JSON.parse(JSON.stringify(value)) as Record<string, unknown>;
574
- }
575
-
576
- function scrubEnvPlaceholders(serverName: string, value: unknown): { env: Record<string, string> | undefined; configEnv: Record<string, string> | undefined } {
577
- if (!isRecord(value)) return { env: undefined, configEnv: undefined };
578
- const env: Record<string, string> = {};
579
- const configEnv: Record<string, string> = {};
580
- for (const [key, raw] of Object.entries(value)) {
581
- if (typeof raw !== "string") continue;
582
- if (isEnvPlaceholder(raw)) {
583
- env[key] = raw;
584
- } else if (SHARED_CALLMUX_CONFIG_ENV_ALLOWLIST[serverName]?.includes(key)) {
585
- configEnv[key] = raw;
586
- } else {
587
- env[key] = `$${key}`;
588
- }
589
- }
590
- return {
591
- env: Object.keys(env).length ? env : undefined,
592
- configEnv: Object.keys(configEnv).length ? configEnv : undefined,
593
- };
594
- }
595
-
596
- function envPlaceholderForLiteral(value: string, env: Record<string, string | undefined>): string | undefined {
597
- for (const [key, candidate] of Object.entries(env)) {
598
- if (!candidate || candidate !== value || !/^[A-Z_][A-Z0-9_]*$/.test(key)) continue;
599
- return `$${key}`;
600
- }
601
- return undefined;
602
- }
603
-
604
- function envPlaceholderName(value: string): string | undefined {
605
- const match = value.match(/^\$?\{?([A-Z_][A-Z0-9_]*)\}?$/);
606
- return match?.[1];
607
- }
608
-
609
- function scrubHeaderPlaceholders(value: unknown, env: Record<string, string | undefined>): Record<string, string> | undefined {
610
- if (!isRecord(value)) return undefined;
611
- const headers: Record<string, string> = {};
612
- for (const [key, raw] of Object.entries(value)) {
613
- if (typeof raw !== "string") continue;
614
- if (!/authorization|token|secret|key|api/i.test(key) || isEnvPlaceholder(raw) || /^Bearer \$\{[A-Z_][A-Z0-9_]*\}$/.test(raw)) {
615
- headers[key] = raw;
616
- continue;
617
- }
618
- const bearer = raw.match(/^Bearer\s+(.+)$/i);
619
- const bearerValue = bearer?.[1];
620
- const bearerPlaceholderName = bearerValue ? envPlaceholderName(bearerValue) : undefined;
621
- if (bearerPlaceholderName) {
622
- headers[key] = `Bearer \${${bearerPlaceholderName}}`;
623
- continue;
624
- }
625
- const placeholder = envPlaceholderForLiteral(bearerValue ?? raw, env);
626
- if (placeholder) headers[key] = bearer ? `Bearer ${placeholder.startsWith("${") ? placeholder : `\${${placeholder.slice(1)}}`}` : placeholder;
627
- }
628
- return Object.keys(headers).length ? headers : undefined;
629
- }
630
-
631
- function scrubCallmuxServerSecrets(serverName: string, server: Record<string, unknown>, env: Record<string, string | undefined>): Record<string, unknown> {
632
- const scrubbed = cloneJsonRecord(server);
633
- const { env: scrubbedEnv, configEnv } = scrubEnvPlaceholders(serverName, scrubbed.env);
634
- const scrubbedHeaders = scrubHeaderPlaceholders(scrubbed.headers, env);
635
- if (scrubbedEnv !== undefined) scrubbed.env = scrubbedEnv;
636
- else delete scrubbed.env;
637
- if (configEnv !== undefined) scrubbed.configEnv = configEnv;
638
- else delete scrubbed.configEnv;
639
- if (scrubbedHeaders) scrubbed.headers = scrubbedHeaders;
640
- else delete scrubbed.headers;
641
- return scrubbed;
642
- }
643
-
644
- function mcpServerFromCallmuxServer(server: Record<string, unknown>): ProvisioningMcpServer {
645
- const projected: ProvisioningMcpServer = {};
646
- if (typeof server.command === "string") {
647
- projected.type = "stdio";
648
- projected.command = server.command;
649
- if (Array.isArray(server.args)) projected.args = server.args.filter((arg): arg is string => typeof arg === "string");
650
- if (isRecord(server.env)) projected.env = stringRecord(server.env);
651
- } else if (typeof server.url === "string") {
652
- projected.type = server.transport === "sse" ? "sse" : "http";
653
- projected.url = server.url;
654
- if (isRecord(server.headers)) projected.headers = stringRecord(server.headers);
655
- }
656
- return projected;
657
- }
658
-
659
- function updateSharedCallmuxAssetMetadata(asset: ProvisioningAsset, sourceConfigPath: string, callmuxServer: Record<string, unknown>, sharedListenerEligible = true): void {
660
- if (!asset.capabilityId) throw new ValidationError("shared callmux seed missing capability id");
661
- const now = Date.now();
662
- const provenance = {
663
- importedBy: "agent-relay-shared-callmux-seed",
664
- sharedListenerEligible,
665
- callmux: { sourceConfigPath, serverName: asset.name },
666
- };
667
- getDb().query(`
668
- UPDATE provisioning_assets
669
- SET source_type = ?, source_ref = ?, provenance = ?, updated_at = ?
670
- WHERE id = ?
671
- `).run(HOST_CALLMUX_SOURCE_TYPE, sourceConfigPath, JSON.stringify(provenance), now, asset.id);
672
- getDb().query("UPDATE provisioning_capabilities SET title = ?, description = ?, updated_at = ? WHERE id = ?")
673
- .run(asset.name, `Host callmux MCP server ${asset.name}`, now, asset.capabilityId);
674
- }
675
-
676
- export function seedSharedCallmuxProvisioningAssets(
677
- input: { sourceConfigPath?: string; env?: Record<string, string | undefined> } = {},
678
- ): SharedCallmuxSeedResult {
679
- const sourceConfigPath = input.sourceConfigPath ?? sharedCallmuxSourceConfigPath(input.env);
680
- const result: SharedCallmuxSeedResult = { sourceConfigPath, seeded: [], skipped: [] };
681
- if (!existsSync(sourceConfigPath)) return result;
682
- const source = parseJson(readFileSync(sourceConfigPath, "utf8"), {}) as unknown;
683
- const sourceServers = isRecord(source) && isRecord(source.servers) ? source.servers : {};
684
- for (const name of SHARED_CALLMUX_SEED_SERVER_NAMES) {
685
- const rawServer = sourceServers[name];
686
- if (!isRecord(rawServer)) {
687
- result.skipped.push({ name, reason: "missing" });
688
- continue;
689
- }
690
- const callmuxServer = scrubCallmuxServerSecrets(name, rawServer, input.env ?? process.env);
691
- const server = mcpServerFromCallmuxServer(callmuxServer);
692
- try {
693
- const sharedListenerEligible = !SHARED_CALLMUX_SEED_NON_LISTENER_NAMES.has(name);
694
- const asset = upsertProvisioningAsset({
695
- kind: "mcp",
696
- name,
697
- definition: {
698
- kind: "mcp",
699
- name,
700
- server,
701
- metadata: { sharedListenerEligible, callmuxServer },
702
- },
703
- });
704
- updateSharedCallmuxAssetMetadata(asset, sourceConfigPath, callmuxServer, sharedListenerEligible);
705
- result.seeded.push(name);
706
- } catch (err) {
707
- result.skipped.push({ name, reason: errMessage(err) });
708
- }
709
- }
710
- return result;
711
- }
565
+ // Shared-callmux seeding (sharedCallmuxSourceConfigPath, the secret scrub, and
566
+ // seedSharedCallmuxProvisioningAssets) lives in src/db/provisioning-callmux.ts
567
+ // split out of this file under the file-size ratchet (epic #291).
712
568
 
713
569
  function upsertImportedAsset(
714
570
  input: ImportProvisioningAssetFromGitHubInput,
@@ -1157,188 +1013,3 @@ export function deleteProvisioningAsset(
1157
1013
  .run(kind, name, provider);
1158
1014
  return result.changes > 0;
1159
1015
  }
1160
-
1161
- function skip(kind: ProvisioningAssetKind, ref: string, provider: SpawnProvider, reason: ProvisioningSkip["reason"], detail: string): ProvisioningSkip {
1162
- return { kind, ref, provider, reason, detail };
1163
- }
1164
-
1165
- function variantSkipReason(row: ProvisioningAssetRow): Pick<ProvisioningSkip, "reason" | "detail"> | null {
1166
- if (row.enabled !== 1) return { reason: "disabled", detail: "Registry variant is disabled." };
1167
- if (row.approval_status !== "approved") return { reason: "unapproved", detail: `Registry variant approval_status is ${row.approval_status ?? "unset"}.` };
1168
- if (row.validation_status !== "valid") {
1169
- const errors = parseJson<unknown[]>(row.validation_errors ?? "[]", []);
1170
- const detail = errors.length ? `Registry variant validation_status is ${row.validation_status}; ${JSON.stringify(errors)}` : `Registry variant validation_status is ${row.validation_status ?? "unset"}.`;
1171
- return { reason: "invalid", detail };
1172
- }
1173
- return null;
1174
- }
1175
-
1176
- // Resolve a registry ref for a target provider: an approved/valid/enabled exact
1177
- // provider match wins, else provider-neutral "any". MCP is intentionally
1178
- // provider-neutral in v1, so only "any" MCP variants are candidates.
1179
- function resolveRegistryDefinition(kind: ProvisioningAssetKind, ref: string, provider: SpawnProvider): RegistryResolution {
1180
- const capability = getDb()
1181
- .query("SELECT id, slug, kind, status FROM provisioning_capabilities WHERE kind = ? AND slug = ?")
1182
- .get(kind, ref) as ProvisioningCapabilityRow | undefined;
1183
- if (!capability) {
1184
- return { definition: null, skip: skip(kind, ref, provider, "missing", "No provisioning capability exists for this ref.") };
1185
- }
1186
-
1187
- const rows = getDb()
1188
- .query("SELECT * FROM provisioning_assets WHERE capability_id = ? ORDER BY CASE provider WHEN ? THEN 0 WHEN 'any' THEN 1 ELSE 2 END, provider")
1189
- .all(capability.id, provider) as ProvisioningAssetRow[];
1190
- const providerCandidates = rows.filter((row) => kind === "mcp" ? row.provider === "any" : row.provider === provider || row.provider === "any");
1191
- if (!providerCandidates.length) {
1192
- const label = getManifest(provider)?.label ?? provider;
1193
- return { definition: null, skip: skip(kind, ref, provider, "provider_mismatch", `No ${label} variant exists for capability ${ref}.`) };
1194
- }
1195
-
1196
- for (const row of providerCandidates) {
1197
- if (!variantSkipReason(row)) return { definition: rowToAsset(row).definition };
1198
- }
1199
-
1200
- const blocked = variantSkipReason(providerCandidates[0]!);
1201
- return { definition: null, skip: skip(kind, ref, provider, blocked?.reason ?? "missing", blocked?.detail ?? "No resolvable registry variant exists.") };
1202
- }
1203
-
1204
- // A ref applies to a launch iff it's enabled and its provider scope includes the
1205
- // target ("any"/undefined → all providers).
1206
- function refAppliesToProvider(ref: AgentProfileAssetRef, provider: SpawnProvider): boolean {
1207
- return ref.enabled && (!ref.provider || ref.provider === "any" || ref.provider === provider);
1208
- }
1209
-
1210
- function hasAssetRef(refs: AgentProfileAssetRef[], source: AgentProfileAssetRef["source"], ref: string): boolean {
1211
- return refs.some((item) => item.source === source && item.ref === ref);
1212
- }
1213
-
1214
- function relayBundledProvisioningRefs(profile: AgentProfile, provider: SpawnProvider): Pick<AgentProfile, "skills" | "plugins"> {
1215
- const skills = [...(profile.skills ?? [])];
1216
- const plugins = [...(profile.plugins ?? [])];
1217
- const provisioning = getManifest(provider)?.provisioning;
1218
- if (profile.relay.skills !== false && provisioning?.bundledSkills) {
1219
- for (const ref of bundledSkillRefs(provisioning.bundledSkills)) {
1220
- if (!hasAssetRef(skills, "relay", ref)) skills.push({ source: "relay", ref, enabled: true, provider });
1221
- }
1222
- }
1223
- if (profile.relay.plugins !== false && provisioning?.bundledPlugin && !hasAssetRef(plugins, "relay", provisioning.bundledPlugin.ref)) {
1224
- plugins.push({ source: "relay", ref: provisioning.bundledPlugin.ref, enabled: true, provider });
1225
- }
1226
- return { skills, plugins };
1227
- }
1228
-
1229
- function withRelayBundledProvisioningRefs(profile: AgentProfile, provider: SpawnProvider): AgentProfile {
1230
- const { skills, plugins } = relayBundledProvisioningRefs(profile, provider);
1231
- return { ...profile, skills, plugins };
1232
- }
1233
-
1234
- function inlineFilesFromMeta(meta: Record<string, unknown> | undefined): { path: string; content: string }[] | undefined {
1235
- const raw = meta?.files;
1236
- if (!Array.isArray(raw)) return undefined;
1237
- const files = raw
1238
- .filter(isRecord)
1239
- .filter((f) => typeof f.path === "string" && typeof f.content === "string")
1240
- .map((f) => ({ path: f.path as string, content: f.content as string }));
1241
- return files.length ? files : undefined;
1242
- }
1243
-
1244
- // Resolve a skill/plugin asset ref into a concrete definition. relay → registry;
1245
- // inline → built from ref.meta (files or dir); repo → a cwd-relative dir token the
1246
- // runner resolves against the launch cwd; provider → skipped (provider built-in).
1247
- function resolveDirAssetRef(
1248
- kind: "skill" | "plugin",
1249
- ref: AgentProfileAssetRef,
1250
- provider: SpawnProvider,
1251
- ): { definition: SkillAssetDefinition | PluginAssetDefinition | null; skip?: ProvisioningSkip } {
1252
- if (ref.source === "relay") {
1253
- const resolved = resolveRegistryDefinition(kind, ref.ref, provider);
1254
- const def = resolved.definition;
1255
- return { definition: def && def.kind === kind ? def : null, skip: resolved.skip };
1256
- }
1257
- if (ref.source === "inline") {
1258
- const files = inlineFilesFromMeta(ref.meta);
1259
- const dir = typeof ref.meta?.dir === "string" ? ref.meta.dir : undefined;
1260
- if (!files && !dir) return { definition: null, skip: skip(kind, ref.ref, provider, "invalid_inline", `Inline ${kind} ref requires files or dir metadata.`) };
1261
- return { definition: { kind, name: ref.ref, ...(files ? { files } : {}), ...(dir ? { dir } : {}) } };
1262
- }
1263
- if (ref.source === "repo") {
1264
- return { definition: { kind, name: ref.ref, dir: ref.ref } };
1265
- }
1266
- return { definition: null };
1267
- }
1268
-
1269
- // A profile.mcp.servers entry is either an inline server spec or a registry
1270
- // reference (`{ source: "relay", ref }`). Returns the concrete server spec.
1271
- function resolveMcpEntry(name: string, value: unknown, provider: SpawnProvider): { server: ProvisioningMcpServer | null; skip?: ProvisioningSkip } {
1272
- if (!isRecord(value)) return { server: null };
1273
- if (value.source === "relay" && typeof value.ref === "string") {
1274
- const resolved = resolveRegistryDefinition("mcp", value.ref, provider);
1275
- const def = resolved.definition;
1276
- return { server: def && def.kind === "mcp" ? def.server : null, skip: resolved.skip };
1277
- }
1278
- if (value.enabled === false) return { server: null };
1279
- // Inline spec: keep the recognized server fields only.
1280
- const server: ProvisioningMcpServer = {};
1281
- if (typeof value.type === "string") server.type = value.type as ProvisioningMcpServer["type"];
1282
- if (typeof value.command === "string") server.command = value.command;
1283
- if (Array.isArray(value.args)) server.args = value.args.filter((a): a is string => typeof a === "string");
1284
- if (isRecord(value.env)) server.env = stringRecord(value.env);
1285
- if (typeof value.url === "string") server.url = value.url;
1286
- if (isRecord(value.headers)) server.headers = stringRecord(value.headers);
1287
- return { server: server.command || server.url ? server : null, skip: server.command || server.url ? undefined : skip("mcp", name, provider, "invalid_inline", "Inline MCP server requires command or url.") };
1288
- }
1289
-
1290
- function stringRecord(value: Record<string, unknown>): Record<string, string> {
1291
- const out: Record<string, string> = {};
1292
- for (const [k, v] of Object.entries(value)) {
1293
- if (typeof v === "string") out[k] = v;
1294
- }
1295
- return out;
1296
- }
1297
-
1298
- // Resolve a profile's declared skill/plugin/MCP refs against the registry into the
1299
- // concrete bundle the runner materializes. Relay-side (needs the DB); the result is
1300
- // attached to the in-flight profile (profile.provisioning) before it's serialized
1301
- // into the spawn env. Pure read — never mutates the registry.
1302
- export function resolveProfileProvisioning(profile: AgentProfile, provider: SpawnProvider): ResolvedProvisioning {
1303
- const skills: SkillAssetDefinition[] = [];
1304
- const skipped: ProvisioningSkip[] = [];
1305
- for (const ref of profile.skills ?? []) {
1306
- if (!refAppliesToProvider(ref, provider)) {
1307
- if (ref.enabled && ref.source === "relay") skipped.push(skip("skill", ref.ref, provider, "provider_mismatch", `Profile ref is scoped to provider ${ref.provider}.`));
1308
- continue;
1309
- }
1310
- const resolved = resolveDirAssetRef("skill", ref, provider);
1311
- if (resolved.definition) skills.push(resolved.definition as SkillAssetDefinition);
1312
- else if (resolved.skip) skipped.push(resolved.skip);
1313
- }
1314
-
1315
- const plugins: PluginAssetDefinition[] = [];
1316
- for (const ref of profile.plugins ?? []) {
1317
- if (!refAppliesToProvider(ref, provider)) {
1318
- if (ref.enabled && ref.source === "relay") skipped.push(skip("plugin", ref.ref, provider, "provider_mismatch", `Profile ref is scoped to provider ${ref.provider}.`));
1319
- continue;
1320
- }
1321
- const resolved = resolveDirAssetRef("plugin", ref, provider);
1322
- if (resolved.definition) plugins.push(resolved.definition as PluginAssetDefinition);
1323
- else if (resolved.skip) skipped.push(resolved.skip);
1324
- }
1325
-
1326
- const mcpServers: Record<string, ProvisioningMcpServer> = {};
1327
- for (const [name, value] of Object.entries(profile.mcp?.servers ?? {})) {
1328
- const resolved = resolveMcpEntry(name, value, provider);
1329
- if (resolved.server) mcpServers[name] = resolved.server;
1330
- else if (resolved.skip) skipped.push(resolved.skip);
1331
- }
1332
-
1333
- return { skills, plugins, mcpServers, ...(skipped.length ? { skipped } : {}) };
1334
- }
1335
-
1336
- // Attach the resolved bundle to a spawn-bound profile. Returns a shallow copy so the
1337
- // cached config-store value is never mutated. undefined passes through (an unresolved
1338
- // profile name stays undefined so callers' not-found guards still fire); the runner
1339
- // treats an absent and an empty bundle identically.
1340
- export function withResolvedProvisioning(profile: AgentProfile | undefined, provider: SpawnProvider): AgentProfile | undefined {
1341
- if (!profile) return undefined;
1342
- const composed = withRelayBundledProvisioningRefs(profile, provider);
1343
- return { ...composed, provisioning: resolveProfileProvisioning(composed, provider) };
1344
- }