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.
- package/docs/openapi.json +1 -1
- package/package.json +3 -3
- package/public/assets/{MessageBubble-Cqi3XsZ7.js → MessageBubble-So86mU9-.js} +2 -2
- package/public/assets/{MessageBubble-Cqi3XsZ7.js.map → MessageBubble-So86mU9-.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-D6Um05oW.js → PlanGraphPanel-DBZYAsww.js} +2 -2
- package/public/assets/{PlanGraphPanel-D6Um05oW.js.map → PlanGraphPanel-DBZYAsww.js.map} +1 -1
- package/public/assets/{activity-B0LsQHEz.js → activity-CZNwmXg1.js} +2 -2
- package/public/assets/{activity-B0LsQHEz.js.map → activity-CZNwmXg1.js.map} +1 -1
- package/public/assets/{agent-profiles-O--pe5Ic.js → agent-profiles-BhadoGwi.js} +2 -2
- package/public/assets/{agent-profiles-O--pe5Ic.js.map → agent-profiles-BhadoGwi.js.map} +1 -1
- package/public/assets/agents-DWn1xx9x.js +2 -0
- package/public/assets/{agents-Ii6XlVwR.js.map → agents-DWn1xx9x.js.map} +1 -1
- package/public/assets/{analytics-BCzdYWRT.js → analytics-DifGnk7q.js} +2 -2
- package/public/assets/{analytics-BCzdYWRT.js.map → analytics-DifGnk7q.js.map} +1 -1
- package/public/assets/{automation-Crr3Xzui.js → automation-BExrtKkb.js} +2 -2
- package/public/assets/{automation-Crr3Xzui.js.map → automation-BExrtKkb.js.map} +1 -1
- package/public/assets/{branch-state-badge-Bhw2rloy.js → branch-state-badge-CyC5Qrhc.js} +2 -2
- package/public/assets/{branch-state-badge-Bhw2rloy.js.map → branch-state-badge-CyC5Qrhc.js.map} +1 -1
- package/public/assets/{channels-CKs9rvsn.js → channels-BLkdt2XR.js} +2 -2
- package/public/assets/{channels-CKs9rvsn.js.map → channels-BLkdt2XR.js.map} +1 -1
- package/public/assets/chat-C7OhKtr6.js +2 -0
- package/public/assets/{chat-BCwEUj5R.js.map → chat-C7OhKtr6.js.map} +1 -1
- package/public/assets/{connectors-D_kQR_vw.js → connectors-C0n12Je8.js} +2 -2
- package/public/assets/{connectors-D_kQR_vw.js.map → connectors-C0n12Je8.js.map} +1 -1
- package/public/assets/{formatted-body-q_qzt1L-.js → formatted-body-BCQT2qSb.js} +3 -3
- package/public/assets/{formatted-body-q_qzt1L-.js.map → formatted-body-BCQT2qSb.js.map} +1 -1
- package/public/assets/{formatted-body-impl-CSRGuMcM.js → formatted-body-impl-Bk7wnESl.js} +2 -2
- package/public/assets/{formatted-body-impl-CSRGuMcM.js.map → formatted-body-impl-Bk7wnESl.js.map} +1 -1
- package/public/assets/index-CRraCeo_.js +25 -0
- package/public/assets/index-CRraCeo_.js.map +1 -0
- package/public/assets/{insights-DUhXqZTZ.js → insights-DEgES6jz.js} +2 -2
- package/public/assets/{insights-DUhXqZTZ.js.map → insights-DEgES6jz.js.map} +1 -1
- package/public/assets/{integrations-lbg5CjtE.js → integrations-v14ehskT.js} +2 -2
- package/public/assets/{integrations-lbg5CjtE.js.map → integrations-v14ehskT.js.map} +1 -1
- package/public/assets/{maintenance-CQgneSVf.js → maintenance-BgVB6_gO.js} +2 -2
- package/public/assets/{maintenance-CQgneSVf.js.map → maintenance-BgVB6_gO.js.map} +1 -1
- package/public/assets/{managed-agents-DsnYf6fm.js → managed-agents-8_NKSkhI.js} +3 -3
- package/public/assets/{managed-agents-DsnYf6fm.js.map → managed-agents-8_NKSkhI.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-C1ZtGJp6.js → markdown-preview-impl-yxQkrd2M.js} +2 -2
- package/public/assets/{markdown-preview-impl-C1ZtGJp6.js.map → markdown-preview-impl-yxQkrd2M.js.map} +1 -1
- package/public/assets/{memory-SSLUTOI2.js → memory-CJD8eekg.js} +2 -2
- package/public/assets/{memory-SSLUTOI2.js.map → memory-CJD8eekg.js.map} +1 -1
- package/public/assets/{messages-BY26OP4K.js → messages-DLHMZbHZ.js} +2 -2
- package/public/assets/{messages-BY26OP4K.js.map → messages-DLHMZbHZ.js.map} +1 -1
- package/public/assets/{orchestrators-OY7yPzVI.js → orchestrators-DEOep7FX.js} +2 -2
- package/public/assets/{orchestrators-OY7yPzVI.js.map → orchestrators-DEOep7FX.js.map} +1 -1
- package/public/assets/{overview-Dj0u1tPG.js → overview-BgpfO6mt.js} +2 -2
- package/public/assets/{overview-Dj0u1tPG.js.map → overview-BgpfO6mt.js.map} +1 -1
- package/public/assets/{pairs-CrUR50ou.js → pairs-8NVKHh96.js} +2 -2
- package/public/assets/{pairs-CrUR50ou.js.map → pairs-8NVKHh96.js.map} +1 -1
- package/public/assets/{projects-B_AtxHtH.js → projects-CZJW35C7.js} +2 -2
- package/public/assets/{projects-B_AtxHtH.js.map → projects-CZJW35C7.js.map} +1 -1
- package/public/assets/{prompt-settings-CPtYSban.js → prompt-settings-fqyLwUa-.js} +2 -2
- package/public/assets/{prompt-settings-CPtYSban.js.map → prompt-settings-fqyLwUa-.js.map} +1 -1
- package/public/assets/{registry-BgUALWn5.js → registry-BKbLL4mO.js} +2 -2
- package/public/assets/{registry-BgUALWn5.js.map → registry-BKbLL4mO.js.map} +1 -1
- package/public/assets/{security-5lHfGk7g.js → security-BlTFTbq0.js} +2 -2
- package/public/assets/{security-5lHfGk7g.js.map → security-BlTFTbq0.js.map} +1 -1
- package/public/assets/{select-Cgx4QSLj.js → select-Zd_qh0uU.js} +2 -2
- package/public/assets/{select-Cgx4QSLj.js.map → select-Zd_qh0uU.js.map} +1 -1
- package/public/assets/{settings-DFGgxyun.js → settings-nHuSVRkL.js} +2 -2
- package/public/assets/{settings-DFGgxyun.js.map → settings-nHuSVRkL.js.map} +1 -1
- package/public/assets/store-nXGxgz2Q.js +9 -0
- package/public/assets/store-nXGxgz2Q.js.map +1 -0
- package/public/assets/{tasks-BvH5rluu.js → tasks-C3JhQ8vB.js} +2 -2
- package/public/assets/{tasks-BvH5rluu.js.map → tasks-C3JhQ8vB.js.map} +1 -1
- package/public/assets/{teams-VZOIOdoE.js → teams-C9E9Yt39.js} +2 -2
- package/public/assets/{teams-VZOIOdoE.js.map → teams-C9E9Yt39.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DYY3Wmoe.js → terminal-viewer-impl-FP0JlsvS.js} +22 -22
- package/public/assets/{terminal-viewer-impl-DYY3Wmoe.js.map → terminal-viewer-impl-FP0JlsvS.js.map} +1 -1
- package/public/assets/types-uVOXgvMT.js.map +1 -1
- package/public/assets/{work-queue-B6Xk7QNe.js → work-queue-Ema7Hy3s.js} +2 -2
- package/public/assets/{work-queue-B6Xk7QNe.js.map → work-queue-Ema7Hy3s.js.map} +1 -1
- package/public/assets/{workspaces-CPIzAHAe.js → workspaces-DI317fMk.js} +2 -2
- package/public/assets/{workspaces-CPIzAHAe.js.map → workspaces-DI317fMk.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/auto-spawn-dispatch.ts +2 -2
- package/src/commands-db.ts +14 -0
- package/src/db/delivery.ts +85 -3
- package/src/db/index.ts +2 -0
- package/src/db/migrations.ts +2 -1
- package/src/db/provisioning-callmux.ts +180 -0
- package/src/db/provisioning-registry.ts +45 -374
- package/src/db/provisioning-resolve.ts +245 -0
- package/src/db/stats.ts +10 -8
- package/src/db/task-entity.ts +7 -8
- package/src/lifecycle-manager.ts +11 -12
- package/src/maintenance/jobs.ts +3 -1
- package/src/maintenance/workspaces.ts +28 -47
- package/src/managed-policy-escalation.ts +47 -0
- package/src/mcp-task-tools.ts +4 -5
- package/src/routes/commands.ts +8 -245
- package/src/routes/tasks.ts +3 -5
- package/src/routes/workspaces.ts +58 -22
- package/src/services/issue-lifecycle.ts +23 -8
- package/src/services/task-entity.ts +54 -2
- package/src/task-dispatcher.ts +12 -3
- package/src/workspace-auto-merge.ts +94 -34
- package/src/workspace-command-settle.ts +227 -0
- package/src/workspace-escalation.ts +45 -0
- package/src/workspace-merge-progress.ts +16 -0
- package/src/workspace-orphans.ts +96 -4
- package/public/assets/agents-Ii6XlVwR.js +0 -2
- package/public/assets/chat-BCwEUj5R.js +0 -2
- package/public/assets/index-Ceiwd02X.js +0 -25
- package/public/assets/index-Ceiwd02X.js.map +0 -1
- package/public/assets/store-BM4zM89B.js +0 -9
- 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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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))
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
569
|
-
|
|
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
|
-
}
|