@opengeni/core 0.4.5 → 0.4.7

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.
@@ -8,24 +8,23 @@ import {
8
8
  type AccessGrant,
9
9
  type CapabilityCatalogResponse,
10
10
  type CapabilityInstallation,
11
- type CapabilityKind,
12
11
  type CreateCapabilityCatalogItemRequest,
13
12
  type EnableCapabilityRequest,
14
13
  type McpServerConnectionRef,
15
14
  } from "@opengeni/contracts";
16
15
  import {
17
- decryptEnvironmentValue,
16
+ decryptVariableSetValue,
18
17
  decryptedCapabilityHeaders,
19
18
  disableCapabilityInstallation,
20
19
  enableCapabilityInstallation,
21
20
  enablePackInstallation,
22
- encryptEnvironmentValue,
21
+ encryptVariableSetValue,
23
22
  getCapabilityCatalogItem,
24
23
  getCapabilityInstallation,
25
24
  getConnectionMetadata,
26
25
  getPackInstallation,
27
26
  getStoredCapabilityHeaderCiphertext,
28
- getWorkspaceEnvironment,
27
+ getVariableSet,
29
28
  listCapabilityCatalogItems,
30
29
  listCapabilityInstallations,
31
30
  listEnabledMcpCapabilityServers,
@@ -37,8 +36,13 @@ import {
37
36
  type EnabledMcpCapabilityServer,
38
37
  } from "@opengeni/db";
39
38
  import { HTTPException } from "hono/http-exception";
40
- import { validateEnvironmentAttachment } from "./environments";
41
- import { assertPackSandboxImageCompatible, listCapabilityPacks, listWorkspaceCapabilityPacks, resolveCapabilityPack } from "./packs";
39
+ import { validateVariableSetAttachment } from "./environments";
40
+ import {
41
+ assertPackSandboxImageCompatible,
42
+ listCapabilityPacks,
43
+ listWorkspaceCapabilityPacks,
44
+ resolveCapabilityPack,
45
+ } from "./packs";
42
46
 
43
47
  const officialMcpRegistryUrl = "https://registry.modelcontextprotocol.io";
44
48
  const firstPartyMcpServerIds = new Set(["opengeni", "files", "docs"]);
@@ -68,17 +72,27 @@ export async function buildCapabilityCatalog(input: {
68
72
  listWorkspaceCapabilityPacks(input.db, input.workspaceId),
69
73
  discoverBundledSkills(),
70
74
  ]);
71
- const capabilityInstallationById = new Map(capabilityInstallations.map((installation) => [installation.capabilityId, installation]));
72
- const activePackIds = new Set(packInstallations.filter((installation) => installation.status === "active").map((installation) => installation.packId));
75
+ const capabilityInstallationById = new Map(
76
+ capabilityInstallations.map((installation) => [installation.capabilityId, installation]),
77
+ );
78
+ const activePackIds = new Set(
79
+ packInstallations
80
+ .filter((installation) => installation.status === "active")
81
+ .map((installation) => installation.packId),
82
+ );
73
83
  const builtInPackIds = new Set(listCapabilityPacks().map((pack) => pack.id));
74
84
  const builtIns = [
75
- ...workspacePacks.map((pack) => packCatalogItem(pack, builtInPackIds.has(pack.id) ? "built_in" : "manual")),
85
+ ...workspacePacks.map((pack) =>
86
+ packCatalogItem(pack, builtInPackIds.has(pack.id) ? "built_in" : "manual"),
87
+ ),
76
88
  ...configuredMcpCatalogItems(input.settings),
77
89
  ...platformApiCatalogItems(),
78
90
  ...bundledSkills,
79
91
  ];
80
92
  const items = dedupeCatalogItems([...builtIns, ...persistedItems])
81
- .map((item) => applyCapabilityEnablement(item, capabilityInstallationById.get(item.id), activePackIds))
93
+ .map((item) =>
94
+ applyCapabilityEnablement(item, capabilityInstallationById.get(item.id), activePackIds),
95
+ )
82
96
  .sort(compareCatalogItems);
83
97
  return {
84
98
  items,
@@ -94,12 +108,21 @@ export async function createCatalogItem(input: {
94
108
  }): Promise<CapabilityCatalogItem> {
95
109
  const id = input.payload.id?.trim() || generatedCapabilityId(input.payload);
96
110
  if (id.startsWith("pack:")) {
97
- throw new HTTPException(422, { message: "packs are managed by OpenGeni and cannot be manually created" });
111
+ throw new HTTPException(422, {
112
+ message: "packs are managed by OpenGeni and cannot be manually created",
113
+ });
98
114
  }
99
- const source = input.payload.source === "built_in" || input.payload.source === "configured" || input.payload.source === "registry" ? "manual" : input.payload.source;
115
+ const source =
116
+ input.payload.source === "built_in" ||
117
+ input.payload.source === "configured" ||
118
+ input.payload.source === "registry"
119
+ ? "manual"
120
+ : input.payload.source;
100
121
  const metadata = {
101
122
  ...input.payload.metadata,
102
- ...(input.payload.kind === "mcp" && input.payload.endpointUrl && !input.payload.metadata.mcpServerId
123
+ ...(input.payload.kind === "mcp" &&
124
+ input.payload.endpointUrl &&
125
+ !input.payload.metadata.mcpServerId
103
126
  ? { mcpServerId: mcpServerIdForCapability(id, input.payload.metadata) }
104
127
  : {}),
105
128
  };
@@ -131,9 +154,16 @@ export async function enableCapability(input: {
131
154
  payload: EnableCapabilityRequest;
132
155
  probeMcpServer?: McpCapabilityProbe;
133
156
  }): Promise<CapabilityInstallation> {
134
- const item = await requireCatalogItem(input.db, input.workspaceId, input.settings, input.capabilityId);
157
+ const item = await requireCatalogItem(
158
+ input.db,
159
+ input.workspaceId,
160
+ input.settings,
161
+ input.capabilityId,
162
+ );
135
163
  if (item.kind === "mcp" && !item.runtime.available) {
136
- throw new HTTPException(422, { message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled" });
164
+ throw new HTTPException(422, {
165
+ message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled",
166
+ });
137
167
  }
138
168
  let installationMetadata = input.payload.metadata;
139
169
  // Credential-header storage is written exclusively by this flow; strip the
@@ -162,7 +192,7 @@ export async function enableCapability(input: {
162
192
  if (headers) {
163
193
  const key = requireCapabilityHeaderEncryption(input.settings);
164
194
  installationConfig.headersEncrypted = Object.fromEntries(
165
- Object.entries(headers).map(([name, value]) => [name, encryptEnvironmentValue(key, value)]),
195
+ Object.entries(headers).map(([name, value]) => [name, encryptVariableSetValue(key, value)]),
166
196
  );
167
197
  }
168
198
  }
@@ -173,49 +203,60 @@ export async function enableCapability(input: {
173
203
  throw new HTTPException(404, { message: "pack not found" });
174
204
  }
175
205
  await assertPackSandboxImageCompatible(input.db, input.workspaceId, pack);
176
- // The unified capability-enable path accepts an initial environment
177
- // attachment (`payload.environmentId`), mirroring POST /packs/:id/enable:
206
+ // The unified capability-enable path accepts an initial variableSet
207
+ // attachment (`payload.variableSetId`), mirroring POST /packs/:id/enable:
178
208
  // a request-supplied id is validated as a fresh attachment, otherwise the
179
209
  // attachment stored by a previous enable is preserved and re-validated.
180
210
  const existing = await getPackInstallation(input.db, input.workspaceId, packId);
181
- const storedEnvironmentId = typeof existing?.metadata.environmentId === "string" ? existing.metadata.environmentId : undefined;
182
- const requestedEnvironmentId = input.payload.environmentId;
183
- const environmentId = requestedEnvironmentId ?? storedEnvironmentId;
184
- if (pack.environment?.required && !environmentId) {
211
+ const storedVariableSetId =
212
+ typeof existing?.metadata.variableSetId === "string"
213
+ ? existing.metadata.variableSetId
214
+ : typeof existing?.metadata.environmentId === "string"
215
+ ? existing.metadata.environmentId
216
+ : undefined;
217
+ const requestedVariableSetId = input.payload.variableSetId;
218
+ const variableSetId = requestedVariableSetId ?? storedVariableSetId;
219
+ if (pack.variableSet?.required && !variableSetId) {
185
220
  throw new HTTPException(422, {
186
- message: `pack ${packId} requires an environment attachment; pass environmentId`,
221
+ message: `pack ${packId} requires an variableSet attachment; pass variableSetId`,
187
222
  });
188
223
  }
189
- if (environmentId) {
190
- if (requestedEnvironmentId) {
224
+ if (variableSetId) {
225
+ if (requestedVariableSetId) {
191
226
  // A fresh attachment: validate it like the packs enable endpoint does.
192
- // The grant holds workspace:admin here, which implies environments:use,
227
+ // The grant holds workspace:admin here, which implies variable-sets:use,
193
228
  // so the attachment authorization succeeds for this caller.
194
- const environment = await validateEnvironmentAttachment(
229
+ const variableSet = await validateVariableSetAttachment(
195
230
  { settings: input.settings, db: input.db },
196
231
  input.grant,
197
232
  input.workspaceId,
198
- requestedEnvironmentId,
233
+ requestedVariableSetId,
234
+ );
235
+ const missing = (pack.variableSet?.requiredVariables ?? []).filter(
236
+ (name) => !variableSet.variables.some((variable) => variable.name === name),
199
237
  );
200
- const missing = (pack.environment?.requiredVariables ?? [])
201
- .filter((name) => !environment.variables.some((variable) => variable.name === name));
202
238
  if (missing.length > 0) {
203
- throw new HTTPException(422, { message: `environment is missing required variable(s): ${missing.join(", ")}` });
239
+ throw new HTTPException(422, {
240
+ message: `variable set is missing required variable(s): ${missing.join(", ")}`,
241
+ });
204
242
  }
205
243
  } else {
206
244
  // The stored attachment was authorized at pack-enable time, but the
207
- // environment may have been deleted or its variables changed since;
245
+ // variableSet may have been deleted or its variables changed since;
208
246
  // re-validate it like the packs enable endpoint does.
209
- const environment = await getWorkspaceEnvironment(input.db, input.workspaceId, environmentId);
210
- if (!environment) {
247
+ const variableSet = await getVariableSet(input.db, input.workspaceId, variableSetId);
248
+ if (!variableSet) {
211
249
  throw new HTTPException(422, {
212
- message: `the stored environment attachment for pack ${packId} no longer exists; re-enable it with environmentId`,
250
+ message: `the stored variableSet attachment for pack ${packId} no longer exists; re-enable it with variableSetId`,
213
251
  });
214
252
  }
215
- const missing = (pack.environment?.requiredVariables ?? [])
216
- .filter((name) => !environment.variables.some((variable) => variable.name === name));
253
+ const missing = (pack.variableSet?.requiredVariables ?? []).filter(
254
+ (name) => !variableSet.variables.some((variable) => variable.name === name),
255
+ );
217
256
  if (missing.length > 0) {
218
- throw new HTTPException(422, { message: `environment is missing required variable(s): ${missing.join(", ")}` });
257
+ throw new HTTPException(422, {
258
+ message: `variable set is missing required variable(s): ${missing.join(", ")}`,
259
+ });
219
260
  }
220
261
  }
221
262
  }
@@ -226,7 +267,7 @@ export async function enableCapability(input: {
226
267
  metadata: {
227
268
  ...input.payload.metadata,
228
269
  packVersion: pack.version,
229
- ...(environmentId ? { environmentId } : {}),
270
+ ...(variableSetId ? { variableSetId } : {}),
230
271
  },
231
272
  });
232
273
  }
@@ -247,7 +288,12 @@ export async function enableCapability(input: {
247
288
  * credentials). Returns null when neither exists.
248
289
  */
249
290
  async function resolveMcpCredentialHeaders(
250
- input: { db: Database; workspaceId: string; settings: Settings; payload: EnableCapabilityRequest },
291
+ input: {
292
+ db: Database;
293
+ workspaceId: string;
294
+ settings: Settings;
295
+ payload: EnableCapabilityRequest;
296
+ },
251
297
  item: CapabilityCatalogItem,
252
298
  ): Promise<Record<string, string> | null> {
253
299
  const provided = normalizedMcpCredentialHeaders(input.payload.headers);
@@ -257,13 +303,22 @@ async function resolveMcpCredentialHeaders(
257
303
  requireCapabilityHeaderEncryption(input.settings);
258
304
  return provided;
259
305
  }
260
- const storedCiphertext = await getStoredCapabilityHeaderCiphertext(input.db, input.workspaceId, item.id);
306
+ const storedCiphertext = await getStoredCapabilityHeaderCiphertext(
307
+ input.db,
308
+ input.workspaceId,
309
+ item.id,
310
+ );
261
311
  if (!storedCiphertext) {
262
312
  return null;
263
313
  }
264
314
  const key = requireCapabilityHeaderEncryption(input.settings);
265
315
  try {
266
- return Object.fromEntries(Object.entries(storedCiphertext).map(([name, value]) => [name, decryptEnvironmentValue(key, value)]));
316
+ return Object.fromEntries(
317
+ Object.entries(storedCiphertext).map(([name, value]) => [
318
+ name,
319
+ decryptVariableSetValue(key, value),
320
+ ]),
321
+ );
267
322
  } catch {
268
323
  throw new HTTPException(422, {
269
324
  message: `stored credential headers for "${item.name}" could not be decrypted; supply them again in the enable request "headers" field`,
@@ -271,13 +326,19 @@ async function resolveMcpCredentialHeaders(
271
326
  }
272
327
  }
273
328
 
274
- function normalizedMcpCredentialHeaders(headers: Record<string, string>): Record<string, string> | null {
275
- const entries = Object.entries(headers).map(([name, value]) => [name.trim(), value] as const).filter(([name]) => name.length > 0);
329
+ function normalizedMcpCredentialHeaders(
330
+ headers: Record<string, string>,
331
+ ): Record<string, string> | null {
332
+ const entries = Object.entries(headers)
333
+ .map(([name, value]) => [name.trim(), value] as const)
334
+ .filter(([name]) => name.length > 0);
276
335
  if (entries.length === 0) {
277
336
  return null;
278
337
  }
279
338
  if (entries.length > maxMcpCredentialHeaders) {
280
- throw new HTTPException(422, { message: `an MCP capability supports at most ${maxMcpCredentialHeaders} credential headers` });
339
+ throw new HTTPException(422, {
340
+ message: `an MCP capability supports at most ${maxMcpCredentialHeaders} credential headers`,
341
+ });
281
342
  }
282
343
  const seen = new Set<string>();
283
344
  for (const [name, value] of entries) {
@@ -290,13 +351,17 @@ function normalizedMcpCredentialHeaders(headers: Record<string, string>): Record
290
351
  }
291
352
  seen.add(lower);
292
353
  if (value.length === 0 || value.length > maxMcpCredentialHeaderValueLength) {
293
- throw new HTTPException(422, { message: `credential header ${name} must be 1-${maxMcpCredentialHeaderValueLength} characters` });
354
+ throw new HTTPException(422, {
355
+ message: `credential header ${name} must be 1-${maxMcpCredentialHeaderValueLength} characters`,
356
+ });
294
357
  }
295
358
  // RFC 9110 §5.5: field values are HTAB / printable characters — reject
296
359
  // all other control characters (they would also fail at the HTTP client).
297
360
  // eslint-disable-next-line no-control-regex
298
361
  if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
299
- throw new HTTPException(422, { message: `credential header ${name} contains forbidden control characters` });
362
+ throw new HTTPException(422, {
363
+ message: `credential header ${name} contains forbidden control characters`,
364
+ });
300
365
  }
301
366
  }
302
367
  return Object.fromEntries(entries);
@@ -308,7 +373,9 @@ async function validateMcpCapabilityConnectionRef(
308
373
  ref: McpServerConnectionRef,
309
374
  ): Promise<McpServerConnectionRef> {
310
375
  if (ref.subjectScope === "subject") {
311
- throw new HTTPException(422, { message: "subject-owned connection refs are not supported for agent runtime use yet" });
376
+ throw new HTTPException(422, {
377
+ message: "subject-owned connection refs are not supported for agent runtime use yet",
378
+ });
312
379
  }
313
380
  const normalized: McpServerConnectionRef = {
314
381
  providerDomain: ref.providerDomain.trim(),
@@ -322,26 +389,44 @@ async function validateMcpCapabilityConnectionRef(
322
389
  throw new HTTPException(422, { message: "connectionRef.providerDomain is required" });
323
390
  }
324
391
  if (!item.endpointUrl || !item.runtime.mcpServerId) {
325
- throw new HTTPException(422, { message: "MCP capabilities need a remote streamable HTTP endpoint before they can use a connectionRef" });
392
+ throw new HTTPException(422, {
393
+ message:
394
+ "MCP capabilities need a remote streamable HTTP endpoint before they can use a connectionRef",
395
+ });
326
396
  }
327
397
  if (!normalized.connectionId) {
328
398
  return normalized;
329
399
  }
330
- const connection = await getConnectionMetadata(input.db, input.workspaceId, normalized.connectionId, input.grant.subjectId);
400
+ const connection = await getConnectionMetadata(
401
+ input.db,
402
+ input.workspaceId,
403
+ normalized.connectionId,
404
+ input.grant.subjectId,
405
+ );
331
406
  if (!connection) {
332
- throw new HTTPException(422, { message: "connectionRef.connectionId does not reference a visible connection" });
407
+ throw new HTTPException(422, {
408
+ message: "connectionRef.connectionId does not reference a visible connection",
409
+ });
333
410
  }
334
411
  if (connection.subjectId !== null) {
335
- throw new HTTPException(422, { message: "agent runtime connection refs must reference workspace-shared connections in I1" });
412
+ throw new HTTPException(422, {
413
+ message: "agent runtime connection refs must reference workspace-shared connections in I1",
414
+ });
336
415
  }
337
416
  if (connection.status !== "active") {
338
- throw new HTTPException(422, { message: `connectionRef.connectionId is not active (${connection.status})` });
417
+ throw new HTTPException(422, {
418
+ message: `connectionRef.connectionId is not active (${connection.status})`,
419
+ });
339
420
  }
340
421
  if (connection.providerDomain !== normalized.providerDomain) {
341
- throw new HTTPException(422, { message: "connectionRef.providerDomain does not match the referenced connection" });
422
+ throw new HTTPException(422, {
423
+ message: "connectionRef.providerDomain does not match the referenced connection",
424
+ });
342
425
  }
343
426
  if (normalized.kind && connection.kind !== normalized.kind) {
344
- throw new HTTPException(422, { message: "connectionRef.kind does not match the referenced connection" });
427
+ throw new HTTPException(422, {
428
+ message: "connectionRef.kind does not match the referenced connection",
429
+ });
345
430
  }
346
431
  return normalized;
347
432
  }
@@ -383,13 +468,17 @@ function requiredCapabilityHeaders(metadata: Record<string, unknown>): string[]
383
468
  if (!Array.isArray(value)) {
384
469
  return [];
385
470
  }
386
- return value.filter((name): name is string => typeof name === "string" && name.trim().length > 0).map((name) => name.trim());
471
+ return value
472
+ .filter((name): name is string => typeof name === "string" && name.trim().length > 0)
473
+ .map((name) => name.trim());
387
474
  }
388
475
 
389
476
  function requireCapabilityHeaderEncryption(settings: Settings): Uint8Array {
390
477
  const key = environmentsEncryptionKeyBytes(settings);
391
478
  if (!key) {
392
- throw new HTTPException(503, { message: "MCP credential headers require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY" });
479
+ throw new HTTPException(503, {
480
+ message: "MCP credential headers require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY",
481
+ });
393
482
  }
394
483
  return key;
395
484
  }
@@ -406,7 +495,9 @@ export type McpCapabilityProbeResult = {
406
495
  toolCount: number;
407
496
  };
408
497
 
409
- export type McpCapabilityProbe = (input: McpCapabilityProbeInput) => Promise<McpCapabilityProbeResult>;
498
+ export type McpCapabilityProbe = (
499
+ input: McpCapabilityProbeInput,
500
+ ) => Promise<McpCapabilityProbeResult>;
410
501
 
411
502
  export async function validateMcpCapabilityConnection(
412
503
  item: CapabilityCatalogItem,
@@ -417,7 +508,9 @@ export async function validateMcpCapabilityConnection(
417
508
  return {};
418
509
  }
419
510
  if (!item.endpointUrl || !item.runtime.mcpServerId) {
420
- throw new HTTPException(422, { message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled" });
511
+ throw new HTTPException(422, {
512
+ message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled",
513
+ });
421
514
  }
422
515
  try {
423
516
  const result = await probe({
@@ -436,15 +529,20 @@ export async function validateMcpCapabilityConnection(
436
529
  };
437
530
  } catch (error) {
438
531
  throw new HTTPException(422, {
439
- message: `MCP capability "${item.name}" could not be enabled because OpenGeni could not initialize ${item.endpointUrl}: ${mcpProbeErrorMessage(error)}`,
532
+ message: `MCP capability "${item.name}" could not be enabled because ${mcpProbeErrorMessage(error, item.endpointUrl)}`,
440
533
  });
441
534
  }
442
535
  }
443
536
 
444
- async function probeStreamableHttpMcpServer(input: McpCapabilityProbeInput): Promise<McpCapabilityProbeResult> {
537
+ async function probeStreamableHttpMcpServer(
538
+ input: McpCapabilityProbeInput,
539
+ ): Promise<McpCapabilityProbeResult> {
445
540
  const controller = new AbortController();
446
541
  const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
447
- const client = new Client({ name: "opengeni-capability-probe", version: "0.1.0" }, { capabilities: {} });
542
+ const client = new Client(
543
+ { name: "opengeni-capability-probe", version: "0.1.0" },
544
+ { capabilities: {} },
545
+ );
448
546
  try {
449
547
  const transport = new StreamableHTTPClientTransport(new URL(input.url), {
450
548
  requestInit: {
@@ -452,8 +550,14 @@ async function probeStreamableHttpMcpServer(input: McpCapabilityProbeInput): Pro
452
550
  ...(input.headers ? { headers: input.headers } : {}),
453
551
  },
454
552
  });
455
- await client.connect(transport as unknown as Transport, { timeout: input.timeoutMs, maxTotalTimeout: input.timeoutMs });
456
- const tools = await client.listTools(undefined, { timeout: input.timeoutMs, maxTotalTimeout: input.timeoutMs });
553
+ await client.connect(transport as unknown as Transport, {
554
+ timeout: input.timeoutMs,
555
+ maxTotalTimeout: input.timeoutMs,
556
+ });
557
+ const tools = await client.listTools(undefined, {
558
+ timeout: input.timeoutMs,
559
+ maxTotalTimeout: input.timeoutMs,
560
+ });
457
561
  return { toolCount: tools.tools.length };
458
562
  } finally {
459
563
  clearTimeout(timeout);
@@ -461,9 +565,17 @@ async function probeStreamableHttpMcpServer(input: McpCapabilityProbeInput): Pro
461
565
  }
462
566
  }
463
567
 
464
- function mcpProbeErrorMessage(error: unknown): string {
568
+ function mcpProbeErrorMessage(error: unknown, endpointUrl: string): string {
465
569
  const message = error instanceof Error ? error.message : String(error);
466
- return message.replace(/\s+/g, " ").trim().slice(0, 500) || "unknown error";
570
+ const normalized = message.replace(/\s+/g, " ").trim();
571
+ if (
572
+ /404|405|not found|unexpected token|not valid json|invalid json|failed to parse|streamable http error|unable to connect|fetch failed|econnrefused|enotfound|timeout|aborted/i.test(
573
+ normalized,
574
+ )
575
+ ) {
576
+ return `OpenGeni could not reach a valid Streamable HTTP MCP server at ${endpointUrl}. Check the endpoint URL or choose a different catalog entry.`;
577
+ }
578
+ return `OpenGeni could not initialize ${endpointUrl}: ${normalized.slice(0, 500) || "unknown error"}`;
467
579
  }
468
580
 
469
581
  export async function disableCapability(input: {
@@ -473,13 +585,26 @@ export async function disableCapability(input: {
473
585
  settings: Settings;
474
586
  capabilityId: string;
475
587
  }): Promise<CapabilityInstallation> {
476
- const item = await requireCatalogItem(input.db, input.workspaceId, input.settings, input.capabilityId);
588
+ const item = await requireCatalogItem(
589
+ input.db,
590
+ input.workspaceId,
591
+ input.settings,
592
+ input.capabilityId,
593
+ );
477
594
  if ((item.source === "built_in" || item.source === "configured") && item.kind !== "pack") {
478
- throw new HTTPException(409, { message: "built-in and configured capabilities are always available; remove them from configuration to disable them" });
595
+ throw new HTTPException(409, {
596
+ message:
597
+ "built-in and configured capabilities are always available; remove them from configuration to disable them",
598
+ });
479
599
  }
480
600
  if (item.kind === "pack") {
481
- await updatePackInstallationStatus(input.db, input.workspaceId, packIdFromCapabilityId(item.id), "disabled").catch(() => undefined);
482
- if (!await getCapabilityInstallation(input.db, input.workspaceId, item.id)) {
601
+ await updatePackInstallationStatus(
602
+ input.db,
603
+ input.workspaceId,
604
+ packIdFromCapabilityId(item.id),
605
+ "disabled",
606
+ ).catch(() => undefined);
607
+ if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
483
608
  await enableCapabilityInstallation(input.db, {
484
609
  accountId: input.accountId,
485
610
  workspaceId: input.workspaceId,
@@ -489,18 +614,25 @@ export async function disableCapability(input: {
489
614
  config: {},
490
615
  });
491
616
  }
492
- } else if (!await getCapabilityInstallation(input.db, input.workspaceId, item.id)) {
617
+ } else if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
493
618
  throw new HTTPException(409, { message: "capability is not currently enabled" });
494
619
  }
495
620
  return await disableCapabilityInstallation(input.db, input.workspaceId, item.id);
496
621
  }
497
622
 
498
- export async function settingsWithEnabledCapabilityMcpServers(db: Database, workspaceId: string, settings: Settings): Promise<Settings> {
623
+ export async function settingsWithEnabledCapabilityMcpServers(
624
+ db: Database,
625
+ workspaceId: string,
626
+ settings: Settings,
627
+ ): Promise<Settings> {
499
628
  const enabled = await listEnabledMcpCapabilityServers(db, workspaceId);
500
629
  return settingsWithMcpCapabilityServers(settings, enabled);
501
630
  }
502
631
 
503
- export function settingsWithMcpCapabilityServers(settings: Settings, enabled: EnabledMcpCapabilityServer[]): Settings {
632
+ export function settingsWithMcpCapabilityServers(
633
+ settings: Settings,
634
+ enabled: EnabledMcpCapabilityServer[],
635
+ ): Settings {
504
636
  if (enabled.length === 0) {
505
637
  return settings;
506
638
  }
@@ -515,18 +647,22 @@ export function settingsWithMcpCapabilityServers(settings: Settings, enabled: En
515
647
  // connect time and break agent turns; leave it out of the run.
516
648
  return [];
517
649
  }
518
- return [{
519
- id: server.id,
520
- name: server.name,
521
- url: server.url,
522
- ...(server.allowedTools ? { allowedTools: server.allowedTools } : {}),
523
- ...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
524
- cacheToolsList: server.cacheToolsList ?? false,
525
- ...(headers && headers !== "unavailable" ? { headers } : {}),
526
- ...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
527
- }];
650
+ return [
651
+ {
652
+ id: server.id,
653
+ name: server.name,
654
+ url: server.url,
655
+ ...(server.allowedTools ? { allowedTools: server.allowedTools } : {}),
656
+ ...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
657
+ cacheToolsList: server.cacheToolsList ?? false,
658
+ ...(headers && headers !== "unavailable" ? { headers } : {}),
659
+ ...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
660
+ },
661
+ ];
528
662
  });
529
- return dynamicServers.length ? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] } : settings;
663
+ return dynamicServers.length
664
+ ? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] }
665
+ : settings;
530
666
  }
531
667
 
532
668
  export async function discoverMcpRegistryCapabilities(input: {
@@ -588,19 +724,25 @@ export { officialMcpRegistryUrl };
588
724
 
589
725
  type McpRegistryFetch = (input: URL, init?: RequestInit) => Promise<Response>;
590
726
 
591
- async function fetchMcpRegistryPage(url: URL, options: {
592
- fetchImpl?: McpRegistryFetch;
593
- timeoutMs?: number;
594
- } = {}): Promise<McpRegistryPage> {
727
+ async function fetchMcpRegistryPage(
728
+ url: URL,
729
+ options: {
730
+ fetchImpl?: McpRegistryFetch;
731
+ timeoutMs?: number;
732
+ } = {},
733
+ ): Promise<McpRegistryPage> {
595
734
  const fetchImpl = options.fetchImpl ?? fetch;
596
735
  const controller = new AbortController();
597
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? mcpRegistryFetchTimeoutMs);
736
+ const timeout = setTimeout(
737
+ () => controller.abort(),
738
+ options.timeoutMs ?? mcpRegistryFetchTimeoutMs,
739
+ );
598
740
  try {
599
741
  const response = await fetchImpl(url, { signal: controller.signal });
600
742
  if (!response.ok) {
601
743
  throw new HTTPException(502, { message: `MCP registry returned ${response.status}` });
602
744
  }
603
- return await response.json() as McpRegistryPage;
745
+ return (await response.json()) as McpRegistryPage;
604
746
  } catch (error) {
605
747
  if (error instanceof HTTPException) {
606
748
  throw error;
@@ -616,16 +758,26 @@ async function fetchMcpRegistryPage(url: URL, options: {
616
758
  }
617
759
  }
618
760
 
619
- async function requireCatalogItem(db: Database, workspaceId: string, settings: Settings, capabilityId: string): Promise<CapabilityCatalogItem> {
761
+ async function requireCatalogItem(
762
+ db: Database,
763
+ workspaceId: string,
764
+ settings: Settings,
765
+ capabilityId: string,
766
+ ): Promise<CapabilityCatalogItem> {
620
767
  const catalog = await buildCapabilityCatalog({ db, workspaceId, settings });
621
- const item = catalog.items.find((candidate) => candidate.id === capabilityId) ?? await getCapabilityCatalogItem(db, workspaceId, capabilityId);
768
+ const item =
769
+ catalog.items.find((candidate) => candidate.id === capabilityId) ??
770
+ (await getCapabilityCatalogItem(db, workspaceId, capabilityId));
622
771
  if (!item) {
623
772
  throw new HTTPException(404, { message: "capability not found" });
624
773
  }
625
774
  return item;
626
775
  }
627
776
 
628
- function packCatalogItem(pack: ReturnType<typeof listCapabilityPacks>[number], source: "built_in" | "manual"): CapabilityCatalogItem {
777
+ function packCatalogItem(
778
+ pack: ReturnType<typeof listCapabilityPacks>[number],
779
+ source: "built_in" | "manual",
780
+ ): CapabilityCatalogItem {
629
781
  return CapabilityCatalogItem.parse({
630
782
  id: `pack:${pack.id}`,
631
783
  kind: "pack",
@@ -654,28 +806,32 @@ function packCatalogItem(pack: ReturnType<typeof listCapabilityPacks>[number], s
654
806
  }
655
807
 
656
808
  function configuredMcpCatalogItems(settings: Settings): CapabilityCatalogItem[] {
657
- return settings.mcpServers.map((server) => CapabilityCatalogItem.parse({
658
- id: `mcp:${server.id}`,
659
- kind: "mcp",
660
- source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
661
- name: server.name ?? server.id,
662
- description: firstPartyMcpDescription(server.id),
663
- category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
664
- tags: ["mcp", ...(server.allowedTools?.length ? ["limited-tools"] : [])],
665
- endpointUrl: server.url,
666
- tools: [{ kind: "mcp", id: server.id }],
667
- runtime: {
668
- available: true,
669
- mcpServerId: server.id,
670
- transport: "streamable-http",
671
- notes: firstPartyMcpServerIds.has(server.id) ? "Available from OpenGeni runtime configuration." : "Configured through OPENGENI_MCP_SERVERS.",
672
- },
673
- metadata: {
674
- mcpServerId: server.id,
675
- allowedTools: server.allowedTools ?? [],
676
- cacheToolsList: server.cacheToolsList,
677
- },
678
- }));
809
+ return settings.mcpServers.map((server) =>
810
+ CapabilityCatalogItem.parse({
811
+ id: `mcp:${server.id}`,
812
+ kind: "mcp",
813
+ source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
814
+ name: server.name ?? server.id,
815
+ description: firstPartyMcpDescription(server.id),
816
+ category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
817
+ tags: ["mcp", ...(server.allowedTools?.length ? ["limited-tools"] : [])],
818
+ endpointUrl: server.url,
819
+ tools: [{ kind: "mcp", id: server.id }],
820
+ runtime: {
821
+ available: true,
822
+ mcpServerId: server.id,
823
+ transport: "streamable-http",
824
+ notes: firstPartyMcpServerIds.has(server.id)
825
+ ? "Available from OpenGeni runtime configuration."
826
+ : "Configured through OPENGENI_MCP_SERVERS.",
827
+ },
828
+ metadata: {
829
+ mcpServerId: server.id,
830
+ allowedTools: server.allowedTools ?? [],
831
+ cacheToolsList: server.cacheToolsList,
832
+ },
833
+ }),
834
+ );
679
835
  }
680
836
 
681
837
  function platformApiCatalogItems(): CapabilityCatalogItem[] {
@@ -712,76 +868,89 @@ function platformApiCatalogItems(): CapabilityCatalogItem[] {
712
868
  tags: ["api", "schedules", "agents"],
713
869
  endpointPath: "/v1/workspaces/{workspaceId}/scheduled-tasks",
714
870
  },
715
- ].map((item) => CapabilityCatalogItem.parse({
716
- id: item.id,
717
- name: item.name,
718
- description: item.description,
719
- category: item.category,
720
- tags: item.tags,
721
- kind: "api",
722
- source: "built_in",
723
- runtime: {
724
- available: true,
725
- notes: "Available through the OpenGeni API.",
726
- },
727
- metadata: {
728
- endpointPath: item.endpointPath,
729
- },
730
- }));
871
+ ].map((item) =>
872
+ CapabilityCatalogItem.parse({
873
+ id: item.id,
874
+ name: item.name,
875
+ description: item.description,
876
+ category: item.category,
877
+ tags: item.tags,
878
+ kind: "api",
879
+ source: "built_in",
880
+ runtime: {
881
+ available: true,
882
+ notes: "Available through the OpenGeni API.",
883
+ },
884
+ metadata: {
885
+ endpointPath: item.endpointPath,
886
+ },
887
+ }),
888
+ );
731
889
  }
732
890
 
733
891
  async function discoverBundledSkills(): Promise<CapabilityCatalogItem[]> {
734
- const skillsDir = new URL("../../../../packages/runtime/src/bundled_hashicorp_terraform_skills/", import.meta.url);
892
+ const skillsDir = new URL(
893
+ "../../../../packages/runtime/src/bundled_hashicorp_terraform_skills/",
894
+ import.meta.url,
895
+ );
735
896
  try {
736
897
  const entries = await readdir(skillsDir, { withFileTypes: true });
737
- const skills = await Promise.all(entries
738
- .filter((entry) => entry.isDirectory())
739
- .map(async (entry) => {
740
- const skill = await readSkillMetadata(new URL(`${entry.name}/SKILL.md`, skillsDir), entry.name);
741
- return CapabilityCatalogItem.parse({
742
- id: `skill:${entry.name}`,
743
- kind: "skill",
744
- source: "built_in",
745
- name: skill.name,
746
- description: skill.description,
747
- category: skill.category,
748
- tags: ["skill", skill.category],
749
- runtime: {
750
- available: true,
751
- notes: "Bundled into the sandbox skill library.",
752
- },
753
- metadata: {
754
- path: `packages/runtime/src/bundled_hashicorp_terraform_skills/${entry.name}/SKILL.md`,
755
- },
756
- });
757
- }));
898
+ const skills = await Promise.all(
899
+ entries
900
+ .filter((entry) => entry.isDirectory())
901
+ .map(async (entry) => {
902
+ const skill = await readSkillMetadata(
903
+ new URL(`${entry.name}/SKILL.md`, skillsDir),
904
+ entry.name,
905
+ );
906
+ return CapabilityCatalogItem.parse({
907
+ id: `skill:${entry.name}`,
908
+ kind: "skill",
909
+ source: "built_in",
910
+ name: skill.name,
911
+ description: skill.description,
912
+ category: skill.category,
913
+ tags: ["skill", skill.category],
914
+ runtime: {
915
+ available: true,
916
+ notes: "Bundled into the sandbox skill library.",
917
+ },
918
+ metadata: {
919
+ path: `packages/runtime/src/bundled_hashicorp_terraform_skills/${entry.name}/SKILL.md`,
920
+ },
921
+ });
922
+ }),
923
+ );
758
924
  return skills;
759
925
  } catch {
760
926
  return [];
761
927
  }
762
928
  }
763
929
 
764
- async function readSkillMetadata(url: URL, fallbackName: string): Promise<{ name: string; description: string | null; category: string }> {
930
+ async function readSkillMetadata(
931
+ url: URL,
932
+ fallbackName: string,
933
+ ): Promise<{ name: string; description: string | null; category: string }> {
765
934
  const content = await readFile(url, "utf8");
766
935
  const frontMatter = content.match(/^---\n([\s\S]*?)\n---/);
767
936
  const frontMatterBody = frontMatter?.[1] ?? "";
768
937
  const name = frontMatterBody.match(/^name:\s*(.+)$/m)?.[1]?.trim() || fallbackName;
769
- const blockDescription = frontMatterBody.match(/^description:\s*>-\s*\n([\s\S]*?)(?:\n[a-zA-Z_-]+:|\n?$)/m)?.[1]
938
+ const blockDescription = frontMatterBody
939
+ .match(/^description:\s*>-\s*\n([\s\S]*?)(?:\n[a-zA-Z_-]+:|\n?$)/m)?.[1]
770
940
  ?.split("\n")
771
941
  .map((line) => line.trim())
772
942
  .filter(Boolean)
773
943
  .join(" ");
774
944
  const inlineDescription = frontMatterBody.match(/^description:\s*(?!>-\s*$)(.+)$/m)?.[1]?.trim();
775
- const description = blockDescription
776
- || inlineDescription
777
- || content.match(/^#\s+(.+)$/m)?.[1]?.trim()
778
- || null;
945
+ const description =
946
+ blockDescription || inlineDescription || content.match(/^#\s+(.+)$/m)?.[1]?.trim() || null;
779
947
  const lower = `${fallbackName} ${name} ${description ?? ""}`.toLowerCase();
780
- const category = lower.includes("social") || lower.includes("marketing")
781
- ? "marketing"
782
- : lower.includes("checkov") || lower.includes("terraform") || lower.includes("azure")
783
- ? "infrastructure"
784
- : "general";
948
+ const category =
949
+ lower.includes("social") || lower.includes("marketing")
950
+ ? "marketing"
951
+ : lower.includes("checkov") || lower.includes("terraform") || lower.includes("azure")
952
+ ? "infrastructure"
953
+ : "general";
785
954
  return { name, description, category };
786
955
  }
787
956
 
@@ -793,7 +962,8 @@ export function applyCapabilityEnablement(
793
962
  if (item.kind === "pack") {
794
963
  // Pack enablement lives in pack_installations regardless of whether the
795
964
  // pack is built in or registered from a workspace manifest.
796
- const enabled = activePackIds.has(packIdFromCapabilityId(item.id)) || installation?.status === "active";
965
+ const enabled =
966
+ activePackIds.has(packIdFromCapabilityId(item.id)) || installation?.status === "active";
797
967
  return {
798
968
  ...item,
799
969
  enabled,
@@ -823,13 +993,19 @@ export function applyCapabilityEnablement(
823
993
  * the enable path — see enableCapability). Headers-enabled and credential-
824
994
  * free installations never set it, so this returns null for them.
825
995
  */
826
- function installationConnectionRef(config: Record<string, unknown>): CapabilityCatalogItem["connectionRef"] {
996
+ function installationConnectionRef(
997
+ config: Record<string, unknown>,
998
+ ): CapabilityCatalogItem["connectionRef"] {
827
999
  const ref = config.connectionRef;
828
1000
  if (!ref || typeof ref !== "object") {
829
1001
  return null;
830
1002
  }
831
1003
  const { connectionId, providerDomain, kind } = ref as Record<string, unknown>;
832
- if (typeof connectionId !== "string" || typeof providerDomain !== "string" || typeof kind !== "string") {
1004
+ if (
1005
+ typeof connectionId !== "string" ||
1006
+ typeof providerDomain !== "string" ||
1007
+ typeof kind !== "string"
1008
+ ) {
833
1009
  return null;
834
1010
  }
835
1011
  return { connectionId, providerDomain, kind };
@@ -861,7 +1037,11 @@ function firstPartyMcpDescription(id: string): string | null {
861
1037
  }
862
1038
 
863
1039
  function generatedCapabilityId(payload: CreateCapabilityCatalogItemRequest): string {
864
- const source = [payload.kind, payload.name, payload.endpointUrl ?? payload.installUrl ?? payload.homepageUrl ?? ""].join(":");
1040
+ const source = [
1041
+ payload.kind,
1042
+ payload.name,
1043
+ payload.endpointUrl ?? payload.installUrl ?? payload.homepageUrl ?? "",
1044
+ ].join(":");
865
1045
  return `${payload.kind}:${slugify(payload.name)}-${shortHash(source)}`;
866
1046
  }
867
1047
 
@@ -882,7 +1062,13 @@ function uniqueStrings(values: string[]): string[] {
882
1062
  }
883
1063
 
884
1064
  function slugify(value: string): string {
885
- return value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "capability";
1065
+ return (
1066
+ value
1067
+ .toLowerCase()
1068
+ .replace(/[^a-z0-9_-]+/g, "-")
1069
+ .replace(/^-+|-+$/g, "")
1070
+ .slice(0, 60) || "capability"
1071
+ );
886
1072
  }
887
1073
 
888
1074
  function shortHash(value: string): string {
@@ -946,7 +1132,9 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
946
1132
  if (official?.isLatest === false) {
947
1133
  return null;
948
1134
  }
949
- const remote = server.remotes?.find((candidate) => candidate.type === "streamable-http" && candidate.url);
1135
+ const remote = server.remotes?.find(
1136
+ (candidate) => candidate.type === "streamable-http" && candidate.url,
1137
+ );
950
1138
  const endpointUrl = validUrl(remote?.url);
951
1139
  if (!remote || !endpointUrl) {
952
1140
  return null;
@@ -963,7 +1151,12 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
963
1151
  name: server.title || server.name,
964
1152
  description: server.description ?? null,
965
1153
  category: "public-mcp",
966
- tags: ["mcp", "public", "registry", ...(requiredHeaders.length ? ["requires-credentials"] : [])],
1154
+ tags: [
1155
+ "mcp",
1156
+ "public",
1157
+ "registry",
1158
+ ...(requiredHeaders.length ? ["requires-credentials"] : []),
1159
+ ],
967
1160
  homepageUrl,
968
1161
  endpointUrl,
969
1162
  installUrl: homepageUrl,
@@ -973,9 +1166,10 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
973
1166
  available: true,
974
1167
  mcpServerId,
975
1168
  transport: "streamable-http",
976
- notes: requiredHeaders.length === 0
977
- ? "Remote MCP server from the official MCP Registry."
978
- : `This MCP requires credential header(s) ${requiredHeaders.join(", ")} supplied in the enable request.`,
1169
+ notes:
1170
+ requiredHeaders.length === 0
1171
+ ? "Remote MCP server from the official MCP Registry."
1172
+ : `This MCP requires credential header(s) ${requiredHeaders.join(", ")} supplied in the enable request.`,
979
1173
  },
980
1174
  metadata: {
981
1175
  registry: "official_mcp_registry",
@@ -1016,7 +1210,10 @@ function catalogSearchText(item: CapabilityCatalogItem): string {
1016
1210
  item.homepageUrl,
1017
1211
  item.installUrl,
1018
1212
  JSON.stringify(item.metadata),
1019
- ].filter(Boolean).join(" ").toLowerCase();
1213
+ ]
1214
+ .filter(Boolean)
1215
+ .join(" ")
1216
+ .toLowerCase();
1020
1217
  }
1021
1218
 
1022
1219
  function capabilityInstallationRuntimeReady(
@@ -1033,17 +1230,22 @@ function capabilityInstallationRuntimeReady(
1033
1230
  return false;
1034
1231
  }
1035
1232
  const connectivity = installation.metadata.mcpConnectivity;
1036
- return !!connectivity
1037
- && typeof connectivity === "object"
1038
- && "status" in connectivity
1039
- && (connectivity.status === "ok" || connectivity.status === "auth_deferred");
1233
+ return (
1234
+ !!connectivity &&
1235
+ typeof connectivity === "object" &&
1236
+ "status" in connectivity &&
1237
+ (connectivity.status === "ok" || connectivity.status === "auth_deferred")
1238
+ );
1040
1239
  }
1041
1240
 
1042
1241
  /**
1043
1242
  * Checks the redacted installation config (header names only) against the
1044
1243
  * capability's declared credential requirements.
1045
1244
  */
1046
- function storedCredentialHeadersSatisfy(item: CapabilityCatalogItem, installation: CapabilityInstallation): boolean {
1245
+ function storedCredentialHeadersSatisfy(
1246
+ item: CapabilityCatalogItem,
1247
+ installation: CapabilityInstallation,
1248
+ ): boolean {
1047
1249
  if (storedConnectionRef(installation.config)) {
1048
1250
  return true;
1049
1251
  }
@@ -1061,6 +1263,10 @@ function storedCredentialHeadersSatisfy(item: CapabilityCatalogItem, installatio
1061
1263
 
1062
1264
  function storedConnectionRef(config: Record<string, unknown>): boolean {
1063
1265
  const ref = config.connectionRef;
1064
- return !!ref && typeof ref === "object" && !Array.isArray(ref)
1065
- && typeof (ref as { providerDomain?: unknown }).providerDomain === "string";
1266
+ return (
1267
+ !!ref &&
1268
+ typeof ref === "object" &&
1269
+ !Array.isArray(ref) &&
1270
+ typeof (ref as { providerDomain?: unknown }).providerDomain === "string"
1271
+ );
1066
1272
  }