@opengeni/core 0.24.1 → 0.28.1

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.
@@ -5,10 +5,13 @@ import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config"
5
5
  import {
6
6
  CapabilityCatalogItem,
7
7
  capabilityCatalogItemIsTrustedForExposure,
8
+ FIKEN_PROVIDER_DOMAIN,
9
+ FIRST_PARTY_MCP_TOOL_NAMES,
8
10
  type AccessGrant,
9
11
  type CapabilityAction,
10
12
  type CapabilityCatalogResponse,
11
13
  type CapabilityInstallation,
14
+ type ConnectionMetadata,
12
15
  type CreateCapabilityCatalogItemRequest,
13
16
  type EnableCapabilityRequest,
14
17
  type McpServerConnectionRef,
@@ -26,45 +29,33 @@ import {
26
29
  decryptedCapabilityHeaders,
27
30
  disableCapabilityInstallation,
28
31
  enableCapabilityInstallation,
29
- enablePackInstallation,
30
32
  encryptVariableSetValue,
31
33
  getCapabilityCatalogItem,
32
34
  getCapabilityInstallation,
33
35
  getConnectionMetadata,
34
36
  getCodexAppsCredentialAuthorizationForRun,
35
37
  getWorkspaceGrant,
36
- getPackInstallation,
37
38
  getStoredCapabilityHeaderCiphertext,
38
- getVariableSet,
39
39
  listCapabilityCatalogItems,
40
40
  listCapabilityInstallations,
41
41
  listConnectionsMetadata,
42
42
  listEnabledMcpCapabilityServers,
43
43
  listInstalledApiIntegrations,
44
+ listInstalledSkills,
44
45
  listPackInstallations,
45
46
  listSocialConnections,
46
47
  mcpServerIdForCapability,
47
- updatePackInstallationStatus,
48
48
  upsertCapabilityCatalogItem,
49
49
  type Database,
50
50
  type ApiIntegrationRuntime,
51
51
  type EnabledMcpCapabilityServer,
52
+ type InstalledSkillSummary,
52
53
  } from "@opengeni/db";
53
54
  import { HTTPException } from "hono/http-exception";
54
55
  import { hasPermission } from "../access";
55
- import {
56
- getSkillLibraryEntry,
57
- listSkillLibraryEntries,
58
- type SkillLibraryEntry,
59
- } from "@opengeni/runtime/skill-library";
60
- import { validateVariableSetAttachment } from "./environments";
61
- import {
62
- assertPackSandboxImageCompatible,
63
- capabilityPackRequiresInstallationPlan,
64
- listCapabilityPacks,
65
- listWorkspaceCapabilityPacks,
66
- resolveCapabilityPack,
67
- } from "./packs";
56
+ import { isFikenConnection, preferredFikenConnection } from "./fiken";
57
+ import { listSkillLibraryEntries, type SkillLibraryEntry } from "@opengeni/runtime/skill-library";
58
+ import { listCapabilityPacks, listWorkspaceCapabilityPacks } from "./packs";
68
59
 
69
60
  const officialMcpRegistryUrl = "https://registry.modelcontextprotocol.io";
70
61
  const firstPartyMcpServerIds = new Set(["opengeni", "files", "docs"]);
@@ -89,7 +80,9 @@ export async function buildCapabilityCatalog(input: {
89
80
  packInstallations,
90
81
  workspacePacks,
91
82
  socialConnections,
83
+ workspaceConnections,
92
84
  curatedLibrarySkills,
85
+ installedSkills,
93
86
  codexAppsCredentialId,
94
87
  ] = await Promise.all([
95
88
  listCapabilityCatalogItems(input.db, input.workspaceId),
@@ -97,13 +90,23 @@ export async function buildCapabilityCatalog(input: {
97
90
  listPackInstallations(input.db, input.workspaceId),
98
91
  listWorkspaceCapabilityPacks(input.db, input.workspaceId),
99
92
  listSocialConnections(input.db, input.workspaceId, 500, input.subjectId),
93
+ listConnectionsMetadata(input.db, input.workspaceId, null),
100
94
  discoverCuratedSkillLibraryItems(),
95
+ listInstalledSkills(input.db, input.workspaceId),
101
96
  input.settings.codexConnectedAppsEnabled
102
97
  ? resolveCodexAppsCredentialIdForRun(input.db, input.workspaceId)
103
98
  : Promise.resolve(null),
104
99
  ]);
100
+ const catalogInstallations = capabilityInstallations.filter(
101
+ (installation) => installation.kind === "mcp",
102
+ );
105
103
  const capabilityInstallationById = new Map(
106
- capabilityInstallations.map((installation) => [installation.capabilityId, installation]),
104
+ catalogInstallations.map((installation) => [installation.capabilityId, installation]),
105
+ );
106
+ const installedSkillById = new Map(
107
+ installedSkills
108
+ .filter((skill) => skill.owners.some((owner) => owner.kind === "direct"))
109
+ .map((skill) => [skill.capabilityId, skill]),
107
110
  );
108
111
  const activePackIds = new Set(
109
112
  packInstallations
@@ -117,69 +120,42 @@ export async function buildCapabilityCatalog(input: {
117
120
  ),
118
121
  ...configuredMcpCatalogItems(input.settings),
119
122
  ...providerIntegrationCatalogItems(socialConnections),
123
+ fikenCatalogItem(workspaceConnections.filter(isFikenConnection)),
120
124
  ...curatedLibrarySkills,
125
+ ...installedSkills
126
+ .filter(
127
+ (skill) =>
128
+ skill.source !== "library" && skill.owners.some((owner) => owner.kind === "direct"),
129
+ )
130
+ .map(installedSkillCatalogItem),
121
131
  ];
122
132
  const codexApps = input.settings.codexConnectedAppsEnabled
123
133
  ? codexAppsCatalogItem(codexAppsCredentialId !== null)
124
134
  : null;
125
135
  const items = dedupeCatalogItems([
126
136
  ...builtIns,
127
- ...persistedItems.filter((item) => !isReservedCodexAppsCatalogItem(item)),
137
+ ...persistedItems.filter(
138
+ (item) =>
139
+ item.kind !== "skill" &&
140
+ item.kind !== "api" &&
141
+ item.kind !== "plugin" &&
142
+ !isReservedCodexAppsCatalogItem(item),
143
+ ),
128
144
  // Keep the reserved, server-derived item authoritative over any stale
129
145
  // legacy catalog row with the same id.
130
146
  ...(codexApps ? [codexApps] : []),
131
147
  ])
132
148
  .map((item) => {
133
- const runtimeItem = applyInstalledArtifactRuntime(item);
134
- const projected = applyCapabilityEnablement(
135
- runtimeItem,
136
- capabilityInstallationById.get(runtimeItem.id),
137
- activePackIds,
138
- );
149
+ const projected =
150
+ item.kind === "skill"
151
+ ? applyInstalledSkillEnablement(item, installedSkillById.get(item.id))
152
+ : applyCapabilityEnablement(item, capabilityInstallationById.get(item.id), activePackIds);
139
153
  return applyCapabilityLifecycle(projected);
140
154
  })
141
155
  .sort(compareCatalogItems);
142
156
  return {
143
157
  items,
144
- installations: capabilityInstallations,
145
- };
146
- }
147
-
148
- function applyInstalledArtifactRuntime(item: CapabilityCatalogItem): CapabilityCatalogItem {
149
- if (
150
- item.kind === "api" &&
151
- item.metadata.platformVersion === 2 &&
152
- typeof item.metadata.pluginVersionId === "string" &&
153
- typeof item.metadata.apiFacetId === "string" &&
154
- typeof item.metadata.revisionId === "string" &&
155
- typeof item.metadata.serverId === "string"
156
- ) {
157
- return {
158
- ...item,
159
- runtime: {
160
- available: true,
161
- mcpServerId: item.metadata.serverId,
162
- transport: "local-adapter",
163
- notes: "Available through an immutable workspace API Integration revision.",
164
- },
165
- };
166
- }
167
- if (
168
- item.kind !== "skill" ||
169
- item.metadata.platformVersion !== 2 ||
170
- typeof item.metadata.pluginVersionId !== "string" ||
171
- typeof item.metadata.facetId !== "string" ||
172
- typeof item.metadata.sourceCommit !== "string" ||
173
- typeof item.metadata.contentSha256 !== "string"
174
- ) {
175
- return item;
176
- }
177
- return {
178
- ...item,
179
- runtime: {
180
- available: true,
181
- notes: "Installed from an immutable workspace Skill artifact.",
182
- },
158
+ installations: catalogInstallations,
183
159
  };
184
160
  }
185
161
 
@@ -197,7 +173,17 @@ export async function createCatalogItem(input: {
197
173
  }
198
174
  if (id.startsWith("skill:")) {
199
175
  throw new HTTPException(422, {
200
- message: "skill ids are managed by the OpenGeni skill library or runtime adapters",
176
+ message: "Skills are installed through the Skill library or source import flow",
177
+ });
178
+ }
179
+ if (id.startsWith("api:")) {
180
+ throw new HTTPException(422, {
181
+ message: "API Integrations are installed from typed Integration Definitions",
182
+ });
183
+ }
184
+ if (id.startsWith("plugin:")) {
185
+ throw new HTTPException(422, {
186
+ message: "Plugins are installed through the Plugin Package flow",
201
187
  });
202
188
  }
203
189
  if (
@@ -259,6 +245,26 @@ export async function enableCapability(input: {
259
245
  input.settings,
260
246
  input.capabilityId,
261
247
  );
248
+ if (item.kind === "skill") {
249
+ throw new HTTPException(409, {
250
+ message: "Install Skills through the Skill library or source import flow",
251
+ });
252
+ }
253
+ if (item.kind === "api") {
254
+ throw new HTTPException(409, {
255
+ message: "Install API Integrations through the Integration Definitions flow",
256
+ });
257
+ }
258
+ if (item.kind === "plugin") {
259
+ throw new HTTPException(409, {
260
+ message: "Install Plugins through the Plugin Package flow",
261
+ });
262
+ }
263
+ if (item.kind === "pack") {
264
+ throw new HTTPException(409, {
265
+ message: "Install Packs through the Pack installation preview flow",
266
+ });
267
+ }
262
268
  if (item.kind === "mcp" && !item.runtime.available) {
263
269
  throw new HTTPException(422, {
264
270
  message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled",
@@ -273,44 +279,6 @@ export async function enableCapability(input: {
273
279
  delete installationConfig.headersEncrypted;
274
280
  delete installationConfig.headerNames;
275
281
  delete installationConfig.connectionRef;
276
- if (item.kind === "skill" && item.source === "library") {
277
- const libraryId = stringMetadata(item.metadata.libraryId);
278
- const catalogVersion = stringMetadata(item.metadata.version);
279
- if (!libraryId || !catalogVersion) {
280
- throw new HTTPException(422, {
281
- message: `skill library metadata is incomplete for ${item.id}`,
282
- });
283
- }
284
- const requestedVersion = input.payload.config.version;
285
- if (requestedVersion !== undefined && typeof requestedVersion !== "string") {
286
- throw new HTTPException(422, {
287
- message: "skill activation config.version must be a string",
288
- });
289
- }
290
- const normalizedVersion = requestedVersion?.trim() || catalogVersion;
291
- if (normalizedVersion !== catalogVersion) {
292
- throw new HTTPException(422, {
293
- message: `skill ${libraryId} only supports immutable version ${catalogVersion}`,
294
- });
295
- }
296
- const entry = getSkillLibraryEntry(libraryId, normalizedVersion);
297
- if (!entry) {
298
- throw new HTTPException(422, {
299
- message: `skill library entry is unavailable: ${libraryId}@${normalizedVersion}`,
300
- });
301
- }
302
- // Skill activation has a deliberately narrow config surface. In
303
- // particular, no variable-set, connection, credential-header, MCP, or
304
- // provider-routing input is persisted or consulted for a library skill.
305
- installationConfig = { version: entry.version };
306
- installationMetadata = {
307
- libraryId: entry.id,
308
- libraryVersion: entry.version,
309
- contentSha256: entry.contentSha256,
310
- sourceCommit: entry.sourceCommit,
311
- provenance: entry.provenance,
312
- };
313
- }
314
282
  if (item.kind === "mcp") {
315
283
  const headers = await resolveMcpCredentialHeaders(input, item);
316
284
  const connectionRef = input.payload.connectionRef
@@ -333,87 +301,6 @@ export async function enableCapability(input: {
333
301
  );
334
302
  }
335
303
  }
336
- if (item.kind === "pack") {
337
- const packId = packIdFromCapabilityId(item.id);
338
- const pack = await resolveCapabilityPack(input.db, input.workspaceId, packId);
339
- if (!pack) {
340
- throw new HTTPException(404, { message: "pack not found" });
341
- }
342
- if (capabilityPackRequiresInstallationPlan(pack)) {
343
- throw new HTTPException(409, {
344
- message:
345
- "This Pack uses component or Rig requirements. Preview and install it through the Pack installation flow.",
346
- });
347
- }
348
- await assertPackSandboxImageCompatible(input.db, input.workspaceId, pack);
349
- // The unified capability-enable path accepts an initial variableSet
350
- // attachment (`payload.variableSetId`), mirroring POST /packs/:id/enable:
351
- // a request-supplied id is validated as a fresh attachment, otherwise the
352
- // attachment stored by a previous enable is preserved and re-validated.
353
- const existing = await getPackInstallation(input.db, input.workspaceId, packId);
354
- const storedVariableSetId =
355
- typeof existing?.metadata.variableSetId === "string"
356
- ? existing.metadata.variableSetId
357
- : typeof existing?.metadata.environmentId === "string"
358
- ? existing.metadata.environmentId
359
- : undefined;
360
- const requestedVariableSetId = input.payload.variableSetId;
361
- const variableSetId = requestedVariableSetId ?? storedVariableSetId;
362
- if (pack.variableSet?.required && !variableSetId) {
363
- throw new HTTPException(422, {
364
- message: `pack ${packId} requires an variableSet attachment; pass variableSetId`,
365
- });
366
- }
367
- if (variableSetId) {
368
- if (requestedVariableSetId) {
369
- // A fresh attachment: validate it like the packs enable endpoint does.
370
- // The grant holds workspace:admin here, which implies variable-sets:use,
371
- // so the attachment authorization succeeds for this caller.
372
- const variableSet = await validateVariableSetAttachment(
373
- { settings: input.settings, db: input.db },
374
- input.grant,
375
- input.workspaceId,
376
- requestedVariableSetId,
377
- );
378
- const missing = (pack.variableSet?.requiredVariables ?? []).filter(
379
- (name) => !variableSet.variables.some((variable) => variable.name === name),
380
- );
381
- if (missing.length > 0) {
382
- throw new HTTPException(422, {
383
- message: `variable set is missing required variable(s): ${missing.join(", ")}`,
384
- });
385
- }
386
- } else {
387
- // The stored attachment was authorized at pack-enable time, but the
388
- // variableSet may have been deleted or its variables changed since;
389
- // re-validate it like the packs enable endpoint does.
390
- const variableSet = await getVariableSet(input.db, input.workspaceId, variableSetId);
391
- if (!variableSet) {
392
- throw new HTTPException(422, {
393
- message: `the stored variableSet attachment for pack ${packId} no longer exists; re-enable it with variableSetId`,
394
- });
395
- }
396
- const missing = (pack.variableSet?.requiredVariables ?? []).filter(
397
- (name) => !variableSet.variables.some((variable) => variable.name === name),
398
- );
399
- if (missing.length > 0) {
400
- throw new HTTPException(422, {
401
- message: `variable set is missing required variable(s): ${missing.join(", ")}`,
402
- });
403
- }
404
- }
405
- }
406
- await enablePackInstallation(input.db, {
407
- accountId: input.accountId,
408
- workspaceId: input.workspaceId,
409
- packId,
410
- metadata: {
411
- ...input.payload.metadata,
412
- packVersion: pack.version,
413
- ...(variableSetId ? { variableSetId } : {}),
414
- },
415
- });
416
- }
417
304
  return await enableCapabilityInstallation(input.db, {
418
305
  accountId: input.accountId,
419
306
  workspaceId: input.workspaceId,
@@ -785,44 +672,33 @@ export async function disableCapability(input: {
785
672
  input.settings,
786
673
  input.capabilityId,
787
674
  );
788
- if ((item.source === "built_in" || item.source === "configured") && item.kind !== "pack") {
675
+ if (item.kind === "skill") {
676
+ throw new HTTPException(409, {
677
+ message: "Uninstall Skills through the Skill uninstall preview flow",
678
+ });
679
+ }
680
+ if (item.kind === "api") {
681
+ throw new HTTPException(409, {
682
+ message: "Remove API Integrations through the Integration instance flow",
683
+ });
684
+ }
685
+ if (item.kind === "plugin") {
686
+ throw new HTTPException(409, {
687
+ message: "Remove Plugins through the Plugin Package flow",
688
+ });
689
+ }
690
+ if (item.kind === "pack") {
691
+ throw new HTTPException(409, {
692
+ message: "Uninstall Packs through the Pack uninstall preview flow",
693
+ });
694
+ }
695
+ if (item.source === "built_in" || item.source === "configured") {
789
696
  throw new HTTPException(409, {
790
697
  message:
791
698
  "built-in and configured capabilities are always available; remove them from configuration to disable them",
792
699
  });
793
700
  }
794
- if (item.kind === "pack") {
795
- const packInstallation = await getPackInstallation(
796
- input.db,
797
- input.workspaceId,
798
- packIdFromCapabilityId(item.id),
799
- );
800
- if (
801
- packInstallation &&
802
- (packInstallation.manifestDigest !== null || packInstallation.manifestSnapshot !== null)
803
- ) {
804
- throw new HTTPException(409, {
805
- message:
806
- "This Pack owns explicit components. Uninstall it through the Pack uninstall preview flow.",
807
- });
808
- }
809
- await updatePackInstallationStatus(
810
- input.db,
811
- input.workspaceId,
812
- packIdFromCapabilityId(item.id),
813
- "disabled",
814
- ).catch(() => undefined);
815
- if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
816
- await enableCapabilityInstallation(input.db, {
817
- accountId: input.accountId,
818
- workspaceId: input.workspaceId,
819
- capabilityId: item.id,
820
- kind: "pack",
821
- metadata: {},
822
- config: {},
823
- });
824
- }
825
- } else if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
701
+ if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
826
702
  throw new HTTPException(409, {
827
703
  message: "capability is not currently enabled",
828
704
  });
@@ -1332,6 +1208,51 @@ const SOCIAL_PROVIDER_TOOL_NAMES = {
1332
1208
  ],
1333
1209
  } as const;
1334
1210
 
1211
+ /**
1212
+ * The first-party Fiken accounting connector tile. Enablement is derived from
1213
+ * the workspace-shared verified Fiken connection (either lane), mirroring how
1214
+ * the social provider tiles derive theirs from social connections.
1215
+ */
1216
+ function fikenCatalogItem(fikenConnections: ConnectionMetadata[]): CapabilityCatalogItem {
1217
+ const fikenConnection = preferredFikenConnection(fikenConnections);
1218
+ const fikenEnabled =
1219
+ fikenConnection?.status === "active" || fikenConnection?.status === "needs_reauth";
1220
+ return CapabilityCatalogItem.parse({
1221
+ id: "api:fiken",
1222
+ kind: "api",
1223
+ source: "built_in",
1224
+ name: "Fiken",
1225
+ description:
1226
+ "Connect Fiken accounting for contacts, products, invoices, invoice drafts, purchases, sales, and bank accounts.",
1227
+ category: "finance",
1228
+ tags: ["api", "fiken", "accounting", "invoicing", "norway"],
1229
+ homepageUrl: "https://fiken.no",
1230
+ authModel: "personal_api_token",
1231
+ providerDomain: FIKEN_PROVIDER_DOMAIN,
1232
+ surfaceType: "first_party_fiken",
1233
+ authKind: "api_key",
1234
+ tools: [{ kind: "mcp", id: "opengeni" }],
1235
+ runtime: {
1236
+ available: true,
1237
+ mcpServerId: "opengeni",
1238
+ notes: "Fiken access is provided through OpenGeni's first-party fiken tools.",
1239
+ },
1240
+ enabled: fikenEnabled,
1241
+ enabledReason: fikenEnabled
1242
+ ? fikenConnection.status === "active"
1243
+ ? "workspace Fiken connection active"
1244
+ : "workspace Fiken connection needs reconnection"
1245
+ : null,
1246
+ metadata: {
1247
+ connectorMode: "first_party_fiken",
1248
+ ownership: "workspace",
1249
+ // Derived from the contracts catalog so a new fiken_* tool cannot be
1250
+ // registered without also appearing on the capability tile.
1251
+ firstPartyMcpTools: FIRST_PARTY_MCP_TOOL_NAMES.filter((name) => name.startsWith("fiken_")),
1252
+ },
1253
+ });
1254
+ }
1255
+
1335
1256
  function providerIntegrationCatalogItems(
1336
1257
  socialConnections: SocialConnection[],
1337
1258
  ): CapabilityCatalogItem[] {
@@ -1488,6 +1409,53 @@ function curatedSkillCatalogItem(entry: SkillLibraryEntry): CapabilityCatalogIte
1488
1409
  });
1489
1410
  }
1490
1411
 
1412
+ function installedSkillCatalogItem(skill: InstalledSkillSummary): CapabilityCatalogItem {
1413
+ return CapabilityCatalogItem.parse({
1414
+ id: skill.capabilityId,
1415
+ kind: "skill",
1416
+ source: skill.source === "library" ? "library" : "manual",
1417
+ name: skill.name,
1418
+ description: skill.description,
1419
+ category: skill.category,
1420
+ tags: skill.tags,
1421
+ homepageUrl: skill.repositoryUrl,
1422
+ installUrl: skill.sourceUrl,
1423
+ provenance: skill.provenance,
1424
+ tier: skill.source === "library" ? "verified" : "community",
1425
+ runtime: {
1426
+ available: true,
1427
+ notes: "Available from an immutable Skill installation.",
1428
+ },
1429
+ metadata: {
1430
+ version: skill.version,
1431
+ contentSha256: skill.contentSha256,
1432
+ sourceCommit: skill.sourceCommit,
1433
+ sourcePath: skill.sourcePath,
1434
+ sourceUrl: skill.sourceUrl,
1435
+ repositoryUrl: skill.repositoryUrl,
1436
+ provenance: skill.provenance,
1437
+ license: skill.license,
1438
+ installedSkill: installedSkillMetadata(skill),
1439
+ },
1440
+ });
1441
+ }
1442
+
1443
+ function installedSkillMetadata(skill: InstalledSkillSummary): Record<string, unknown> {
1444
+ return {
1445
+ pluginKey: skill.pluginKey,
1446
+ installationVersion: skill.installationVersion,
1447
+ source: skill.source,
1448
+ version: skill.version,
1449
+ sourceCommit: skill.sourceCommit,
1450
+ contentSha256: skill.contentSha256,
1451
+ fileCount: skill.fileCount,
1452
+ totalBytes: skill.totalBytes,
1453
+ installedAt: skill.installedAt,
1454
+ updatedAt: skill.updatedAt,
1455
+ owners: skill.owners.map((owner) => ({ ...owner })),
1456
+ };
1457
+ }
1458
+
1491
1459
  function stringMetadata(value: unknown): string | null {
1492
1460
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1493
1461
  }
@@ -1528,11 +1496,11 @@ export function applyCapabilityEnablement(
1528
1496
  installation: CapabilityInstallation | undefined,
1529
1497
  activePackIds: Set<string>,
1530
1498
  ): CapabilityCatalogItem {
1499
+ if (item.kind === "skill" || item.kind === "api" || item.kind === "plugin") {
1500
+ return { ...item, enabled: false, enabledReason: null, connectionRef: null };
1501
+ }
1531
1502
  if (item.kind === "pack") {
1532
- // Pack enablement lives in pack_installations regardless of whether the
1533
- // pack is built in or registered from a workspace manifest.
1534
- const enabled =
1535
- activePackIds.has(packIdFromCapabilityId(item.id)) || installation?.status === "active";
1503
+ const enabled = activePackIds.has(packIdFromCapabilityId(item.id));
1536
1504
  return {
1537
1505
  ...item,
1538
1506
  enabled,
@@ -1545,6 +1513,12 @@ export function applyCapabilityEnablement(
1545
1513
  // publishes a personal connection UUID or collapses many accounts to one.
1546
1514
  return { ...item, connectionRef: null };
1547
1515
  }
1516
+ if (item.surfaceType === "first_party_fiken") {
1517
+ // Fiken connector state is derived from the authoritative workspace
1518
+ // connection row while the catalog is built; browseable never means an
1519
+ // account is already connected.
1520
+ return { ...item, connectionRef: null };
1521
+ }
1548
1522
  if (item.surfaceType === "codex_apps") {
1549
1523
  // Unlike ordinary built-ins, Apps availability is derived from the exact
1550
1524
  // active workspace designation above. Do not overwrite it with the
@@ -1565,37 +1539,8 @@ export function applyCapabilityEnablement(
1565
1539
  // state explicitly above rather than becoming enabled by taxonomy.
1566
1540
  return { ...item, connectionRef: null };
1567
1541
  }
1568
- if (item.source === "library") {
1569
- const enabled =
1570
- installation?.status === "active" && skillLibraryInstallationRuntimeReady(item, installation);
1571
- return {
1572
- ...item,
1573
- enabled,
1574
- enabledReason: enabled ? "explicitly selected" : null,
1575
- connectionRef: null,
1576
- };
1577
- }
1578
1542
  const activeInstallation = installation?.status === "active";
1579
1543
  const enabled = !!activeInstallation && capabilityInstallationRuntimeReady(item, installation);
1580
- if (item.kind === "api" && item.metadata.platformVersion === 2) {
1581
- const connectionBound = installation?.metadata.connectionBound === true;
1582
- const providerDomain = stringMetadata(installation?.metadata.providerDomain);
1583
- const connectionKind = stringMetadata(installation?.metadata.connectionKind);
1584
- const connectionOwnership = stringMetadata(installation?.metadata.connectionOwnership);
1585
- return {
1586
- ...item,
1587
- enabled,
1588
- enabledReason: enabled ? "installed immutable Integration revision" : null,
1589
- connectionRef:
1590
- enabled && connectionBound && providerDomain && connectionKind
1591
- ? {
1592
- providerDomain,
1593
- kind: connectionKind,
1594
- ...(connectionOwnership === "subject" ? { subjectScope: "subject" as const } : {}),
1595
- }
1596
- : null,
1597
- };
1598
- }
1599
1544
  return {
1600
1545
  ...item,
1601
1546
  enabled,
@@ -1688,40 +1633,32 @@ function applyCapabilityLifecycle(item: CapabilityCatalogItem): CapabilityCatalo
1688
1633
  };
1689
1634
  }
1690
1635
 
1691
- function skillLibraryInstallationRuntimeReady(
1636
+ function applyInstalledSkillEnablement(
1692
1637
  item: CapabilityCatalogItem,
1693
- installation: CapabilityInstallation,
1694
- ): boolean {
1695
- if (item.kind !== "skill" || item.source !== "library" || installation.kind !== "skill") {
1696
- return false;
1697
- }
1698
- const libraryId = stringMetadata(item.metadata.libraryId);
1699
- const version = stringMetadata(item.metadata.version);
1700
- const contentSha256 = stringMetadata(item.metadata.contentSha256);
1701
- const sourceCommit = stringMetadata(item.metadata.sourceCommit);
1702
- const provenance = stringMetadata(item.metadata.provenance);
1703
- if (!libraryId || !version || !contentSha256 || !sourceCommit || !provenance) {
1704
- return false;
1705
- }
1706
- const entry = getSkillLibraryEntry(libraryId, version);
1707
- if (
1708
- !entry ||
1709
- item.id !== `skill:${entry.id}` ||
1710
- installation.capabilityId !== `skill:${entry.id}` ||
1711
- contentSha256 !== entry.contentSha256 ||
1712
- sourceCommit !== entry.sourceCommit ||
1713
- provenance !== entry.provenance
1714
- ) {
1715
- return false;
1716
- }
1717
- return (
1718
- installation.config.version === entry.version &&
1719
- stringMetadata(installation.metadata.libraryId) === entry.id &&
1720
- stringMetadata(installation.metadata.libraryVersion) === entry.version &&
1721
- stringMetadata(installation.metadata.contentSha256) === entry.contentSha256 &&
1722
- stringMetadata(installation.metadata.sourceCommit) === entry.sourceCommit &&
1723
- stringMetadata(installation.metadata.provenance) === entry.provenance
1724
- );
1638
+ installation: InstalledSkillSummary | undefined,
1639
+ ): CapabilityCatalogItem {
1640
+ if (!installation) {
1641
+ return { ...item, enabled: false, enabledReason: null, connectionRef: null };
1642
+ }
1643
+ const catalogVersion = stringMetadata(item.metadata.version);
1644
+ const current =
1645
+ catalogVersion === null ||
1646
+ (catalogVersion === installation.version &&
1647
+ stringMetadata(item.metadata.contentSha256) === installation.contentSha256 &&
1648
+ stringMetadata(item.metadata.sourceCommit) === installation.sourceCommit);
1649
+ return {
1650
+ ...item,
1651
+ enabled: true,
1652
+ enabledReason: current
1653
+ ? "explicitly installed"
1654
+ : `version ${installation.version} installed; update available`,
1655
+ connectionRef: null,
1656
+ metadata: {
1657
+ ...item.metadata,
1658
+ installedSkill: installedSkillMetadata(installation),
1659
+ updateAvailable: !current,
1660
+ },
1661
+ };
1725
1662
  }
1726
1663
 
1727
1664
  /**