@opengeni/core 0.4.6 → 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({
@@ -441,10 +534,15 @@ export async function validateMcpCapabilityConnection(
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);
@@ -465,8 +569,9 @@ function mcpProbeErrorMessage(error: unknown, endpointUrl: string): string {
465
569
  const message = error instanceof Error ? error.message : String(error);
466
570
  const normalized = message.replace(/\s+/g, " ").trim();
467
571
  if (
468
- /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
469
- .test(normalized)
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
+ )
470
575
  ) {
471
576
  return `OpenGeni could not reach a valid Streamable HTTP MCP server at ${endpointUrl}. Check the endpoint URL or choose a different catalog entry.`;
472
577
  }
@@ -480,13 +585,26 @@ export async function disableCapability(input: {
480
585
  settings: Settings;
481
586
  capabilityId: string;
482
587
  }): Promise<CapabilityInstallation> {
483
- 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
+ );
484
594
  if ((item.source === "built_in" || item.source === "configured") && item.kind !== "pack") {
485
- 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
+ });
486
599
  }
487
600
  if (item.kind === "pack") {
488
- await updatePackInstallationStatus(input.db, input.workspaceId, packIdFromCapabilityId(item.id), "disabled").catch(() => undefined);
489
- 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))) {
490
608
  await enableCapabilityInstallation(input.db, {
491
609
  accountId: input.accountId,
492
610
  workspaceId: input.workspaceId,
@@ -496,18 +614,25 @@ export async function disableCapability(input: {
496
614
  config: {},
497
615
  });
498
616
  }
499
- } else if (!await getCapabilityInstallation(input.db, input.workspaceId, item.id)) {
617
+ } else if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
500
618
  throw new HTTPException(409, { message: "capability is not currently enabled" });
501
619
  }
502
620
  return await disableCapabilityInstallation(input.db, input.workspaceId, item.id);
503
621
  }
504
622
 
505
- 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> {
506
628
  const enabled = await listEnabledMcpCapabilityServers(db, workspaceId);
507
629
  return settingsWithMcpCapabilityServers(settings, enabled);
508
630
  }
509
631
 
510
- export function settingsWithMcpCapabilityServers(settings: Settings, enabled: EnabledMcpCapabilityServer[]): Settings {
632
+ export function settingsWithMcpCapabilityServers(
633
+ settings: Settings,
634
+ enabled: EnabledMcpCapabilityServer[],
635
+ ): Settings {
511
636
  if (enabled.length === 0) {
512
637
  return settings;
513
638
  }
@@ -522,18 +647,22 @@ export function settingsWithMcpCapabilityServers(settings: Settings, enabled: En
522
647
  // connect time and break agent turns; leave it out of the run.
523
648
  return [];
524
649
  }
525
- return [{
526
- id: server.id,
527
- name: server.name,
528
- url: server.url,
529
- ...(server.allowedTools ? { allowedTools: server.allowedTools } : {}),
530
- ...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
531
- cacheToolsList: server.cacheToolsList ?? false,
532
- ...(headers && headers !== "unavailable" ? { headers } : {}),
533
- ...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
534
- }];
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
+ ];
535
662
  });
536
- return dynamicServers.length ? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] } : settings;
663
+ return dynamicServers.length
664
+ ? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] }
665
+ : settings;
537
666
  }
538
667
 
539
668
  export async function discoverMcpRegistryCapabilities(input: {
@@ -595,19 +724,25 @@ export { officialMcpRegistryUrl };
595
724
 
596
725
  type McpRegistryFetch = (input: URL, init?: RequestInit) => Promise<Response>;
597
726
 
598
- async function fetchMcpRegistryPage(url: URL, options: {
599
- fetchImpl?: McpRegistryFetch;
600
- timeoutMs?: number;
601
- } = {}): Promise<McpRegistryPage> {
727
+ async function fetchMcpRegistryPage(
728
+ url: URL,
729
+ options: {
730
+ fetchImpl?: McpRegistryFetch;
731
+ timeoutMs?: number;
732
+ } = {},
733
+ ): Promise<McpRegistryPage> {
602
734
  const fetchImpl = options.fetchImpl ?? fetch;
603
735
  const controller = new AbortController();
604
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? mcpRegistryFetchTimeoutMs);
736
+ const timeout = setTimeout(
737
+ () => controller.abort(),
738
+ options.timeoutMs ?? mcpRegistryFetchTimeoutMs,
739
+ );
605
740
  try {
606
741
  const response = await fetchImpl(url, { signal: controller.signal });
607
742
  if (!response.ok) {
608
743
  throw new HTTPException(502, { message: `MCP registry returned ${response.status}` });
609
744
  }
610
- return await response.json() as McpRegistryPage;
745
+ return (await response.json()) as McpRegistryPage;
611
746
  } catch (error) {
612
747
  if (error instanceof HTTPException) {
613
748
  throw error;
@@ -623,16 +758,26 @@ async function fetchMcpRegistryPage(url: URL, options: {
623
758
  }
624
759
  }
625
760
 
626
- 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> {
627
767
  const catalog = await buildCapabilityCatalog({ db, workspaceId, settings });
628
- 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));
629
771
  if (!item) {
630
772
  throw new HTTPException(404, { message: "capability not found" });
631
773
  }
632
774
  return item;
633
775
  }
634
776
 
635
- 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 {
636
781
  return CapabilityCatalogItem.parse({
637
782
  id: `pack:${pack.id}`,
638
783
  kind: "pack",
@@ -661,28 +806,32 @@ function packCatalogItem(pack: ReturnType<typeof listCapabilityPacks>[number], s
661
806
  }
662
807
 
663
808
  function configuredMcpCatalogItems(settings: Settings): CapabilityCatalogItem[] {
664
- return settings.mcpServers.map((server) => CapabilityCatalogItem.parse({
665
- id: `mcp:${server.id}`,
666
- kind: "mcp",
667
- source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
668
- name: server.name ?? server.id,
669
- description: firstPartyMcpDescription(server.id),
670
- category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
671
- tags: ["mcp", ...(server.allowedTools?.length ? ["limited-tools"] : [])],
672
- endpointUrl: server.url,
673
- tools: [{ kind: "mcp", id: server.id }],
674
- runtime: {
675
- available: true,
676
- mcpServerId: server.id,
677
- transport: "streamable-http",
678
- notes: firstPartyMcpServerIds.has(server.id) ? "Available from OpenGeni runtime configuration." : "Configured through OPENGENI_MCP_SERVERS.",
679
- },
680
- metadata: {
681
- mcpServerId: server.id,
682
- allowedTools: server.allowedTools ?? [],
683
- cacheToolsList: server.cacheToolsList,
684
- },
685
- }));
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
+ );
686
835
  }
687
836
 
688
837
  function platformApiCatalogItems(): CapabilityCatalogItem[] {
@@ -719,76 +868,89 @@ function platformApiCatalogItems(): CapabilityCatalogItem[] {
719
868
  tags: ["api", "schedules", "agents"],
720
869
  endpointPath: "/v1/workspaces/{workspaceId}/scheduled-tasks",
721
870
  },
722
- ].map((item) => CapabilityCatalogItem.parse({
723
- id: item.id,
724
- name: item.name,
725
- description: item.description,
726
- category: item.category,
727
- tags: item.tags,
728
- kind: "api",
729
- source: "built_in",
730
- runtime: {
731
- available: true,
732
- notes: "Available through the OpenGeni API.",
733
- },
734
- metadata: {
735
- endpointPath: item.endpointPath,
736
- },
737
- }));
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
+ );
738
889
  }
739
890
 
740
891
  async function discoverBundledSkills(): Promise<CapabilityCatalogItem[]> {
741
- 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
+ );
742
896
  try {
743
897
  const entries = await readdir(skillsDir, { withFileTypes: true });
744
- const skills = await Promise.all(entries
745
- .filter((entry) => entry.isDirectory())
746
- .map(async (entry) => {
747
- const skill = await readSkillMetadata(new URL(`${entry.name}/SKILL.md`, skillsDir), entry.name);
748
- return CapabilityCatalogItem.parse({
749
- id: `skill:${entry.name}`,
750
- kind: "skill",
751
- source: "built_in",
752
- name: skill.name,
753
- description: skill.description,
754
- category: skill.category,
755
- tags: ["skill", skill.category],
756
- runtime: {
757
- available: true,
758
- notes: "Bundled into the sandbox skill library.",
759
- },
760
- metadata: {
761
- path: `packages/runtime/src/bundled_hashicorp_terraform_skills/${entry.name}/SKILL.md`,
762
- },
763
- });
764
- }));
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
+ );
765
924
  return skills;
766
925
  } catch {
767
926
  return [];
768
927
  }
769
928
  }
770
929
 
771
- 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 }> {
772
934
  const content = await readFile(url, "utf8");
773
935
  const frontMatter = content.match(/^---\n([\s\S]*?)\n---/);
774
936
  const frontMatterBody = frontMatter?.[1] ?? "";
775
937
  const name = frontMatterBody.match(/^name:\s*(.+)$/m)?.[1]?.trim() || fallbackName;
776
- 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]
777
940
  ?.split("\n")
778
941
  .map((line) => line.trim())
779
942
  .filter(Boolean)
780
943
  .join(" ");
781
944
  const inlineDescription = frontMatterBody.match(/^description:\s*(?!>-\s*$)(.+)$/m)?.[1]?.trim();
782
- const description = blockDescription
783
- || inlineDescription
784
- || content.match(/^#\s+(.+)$/m)?.[1]?.trim()
785
- || null;
945
+ const description =
946
+ blockDescription || inlineDescription || content.match(/^#\s+(.+)$/m)?.[1]?.trim() || null;
786
947
  const lower = `${fallbackName} ${name} ${description ?? ""}`.toLowerCase();
787
- const category = lower.includes("social") || lower.includes("marketing")
788
- ? "marketing"
789
- : lower.includes("checkov") || lower.includes("terraform") || lower.includes("azure")
790
- ? "infrastructure"
791
- : "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";
792
954
  return { name, description, category };
793
955
  }
794
956
 
@@ -800,7 +962,8 @@ export function applyCapabilityEnablement(
800
962
  if (item.kind === "pack") {
801
963
  // Pack enablement lives in pack_installations regardless of whether the
802
964
  // pack is built in or registered from a workspace manifest.
803
- const enabled = activePackIds.has(packIdFromCapabilityId(item.id)) || installation?.status === "active";
965
+ const enabled =
966
+ activePackIds.has(packIdFromCapabilityId(item.id)) || installation?.status === "active";
804
967
  return {
805
968
  ...item,
806
969
  enabled,
@@ -830,13 +993,19 @@ export function applyCapabilityEnablement(
830
993
  * the enable path — see enableCapability). Headers-enabled and credential-
831
994
  * free installations never set it, so this returns null for them.
832
995
  */
833
- function installationConnectionRef(config: Record<string, unknown>): CapabilityCatalogItem["connectionRef"] {
996
+ function installationConnectionRef(
997
+ config: Record<string, unknown>,
998
+ ): CapabilityCatalogItem["connectionRef"] {
834
999
  const ref = config.connectionRef;
835
1000
  if (!ref || typeof ref !== "object") {
836
1001
  return null;
837
1002
  }
838
1003
  const { connectionId, providerDomain, kind } = ref as Record<string, unknown>;
839
- 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
+ ) {
840
1009
  return null;
841
1010
  }
842
1011
  return { connectionId, providerDomain, kind };
@@ -868,7 +1037,11 @@ function firstPartyMcpDescription(id: string): string | null {
868
1037
  }
869
1038
 
870
1039
  function generatedCapabilityId(payload: CreateCapabilityCatalogItemRequest): string {
871
- 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(":");
872
1045
  return `${payload.kind}:${slugify(payload.name)}-${shortHash(source)}`;
873
1046
  }
874
1047
 
@@ -889,7 +1062,13 @@ function uniqueStrings(values: string[]): string[] {
889
1062
  }
890
1063
 
891
1064
  function slugify(value: string): string {
892
- 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
+ );
893
1072
  }
894
1073
 
895
1074
  function shortHash(value: string): string {
@@ -953,7 +1132,9 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
953
1132
  if (official?.isLatest === false) {
954
1133
  return null;
955
1134
  }
956
- 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
+ );
957
1138
  const endpointUrl = validUrl(remote?.url);
958
1139
  if (!remote || !endpointUrl) {
959
1140
  return null;
@@ -970,7 +1151,12 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
970
1151
  name: server.title || server.name,
971
1152
  description: server.description ?? null,
972
1153
  category: "public-mcp",
973
- tags: ["mcp", "public", "registry", ...(requiredHeaders.length ? ["requires-credentials"] : [])],
1154
+ tags: [
1155
+ "mcp",
1156
+ "public",
1157
+ "registry",
1158
+ ...(requiredHeaders.length ? ["requires-credentials"] : []),
1159
+ ],
974
1160
  homepageUrl,
975
1161
  endpointUrl,
976
1162
  installUrl: homepageUrl,
@@ -980,9 +1166,10 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
980
1166
  available: true,
981
1167
  mcpServerId,
982
1168
  transport: "streamable-http",
983
- notes: requiredHeaders.length === 0
984
- ? "Remote MCP server from the official MCP Registry."
985
- : `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.`,
986
1173
  },
987
1174
  metadata: {
988
1175
  registry: "official_mcp_registry",
@@ -1023,7 +1210,10 @@ function catalogSearchText(item: CapabilityCatalogItem): string {
1023
1210
  item.homepageUrl,
1024
1211
  item.installUrl,
1025
1212
  JSON.stringify(item.metadata),
1026
- ].filter(Boolean).join(" ").toLowerCase();
1213
+ ]
1214
+ .filter(Boolean)
1215
+ .join(" ")
1216
+ .toLowerCase();
1027
1217
  }
1028
1218
 
1029
1219
  function capabilityInstallationRuntimeReady(
@@ -1040,17 +1230,22 @@ function capabilityInstallationRuntimeReady(
1040
1230
  return false;
1041
1231
  }
1042
1232
  const connectivity = installation.metadata.mcpConnectivity;
1043
- return !!connectivity
1044
- && typeof connectivity === "object"
1045
- && "status" in connectivity
1046
- && (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
+ );
1047
1239
  }
1048
1240
 
1049
1241
  /**
1050
1242
  * Checks the redacted installation config (header names only) against the
1051
1243
  * capability's declared credential requirements.
1052
1244
  */
1053
- function storedCredentialHeadersSatisfy(item: CapabilityCatalogItem, installation: CapabilityInstallation): boolean {
1245
+ function storedCredentialHeadersSatisfy(
1246
+ item: CapabilityCatalogItem,
1247
+ installation: CapabilityInstallation,
1248
+ ): boolean {
1054
1249
  if (storedConnectionRef(installation.config)) {
1055
1250
  return true;
1056
1251
  }
@@ -1068,6 +1263,10 @@ function storedCredentialHeadersSatisfy(item: CapabilityCatalogItem, installatio
1068
1263
 
1069
1264
  function storedConnectionRef(config: Record<string, unknown>): boolean {
1070
1265
  const ref = config.connectionRef;
1071
- return !!ref && typeof ref === "object" && !Array.isArray(ref)
1072
- && 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
+ );
1073
1272
  }