@opengeni/core 0.21.10 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/index.d.ts +1 -1
- package/dist/application/new-session-drafts.d.ts +1 -1
- package/dist/application/session-commands.d.ts +2 -2
- package/dist/billing/limits.d.ts +1 -1
- package/dist/canonical-human-identities.d.ts +12 -0
- package/dist/canonical-human-identities.js +23 -0
- package/dist/canonical-human-identities.js.map +1 -0
- package/dist/chunk-6WKPWE5S.js +2380 -0
- package/dist/chunk-6WKPWE5S.js.map +1 -0
- package/dist/chunk-IBOEYG6N.js +34 -0
- package/dist/chunk-IBOEYG6N.js.map +1 -0
- package/dist/chunk-JJU6I5XD.js +2713 -0
- package/dist/chunk-JJU6I5XD.js.map +1 -0
- package/dist/chunk-NYPMQ7NO.js +2270 -0
- package/dist/chunk-NYPMQ7NO.js.map +1 -0
- package/dist/dependencies.d.ts +30 -6
- package/dist/domain/capabilities.d.ts +28 -3
- package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
- package/dist/domain/conversation-integrations.d.ts +225 -0
- package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
- package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
- package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
- package/dist/domain/editable-artifacts/errors.d.ts +47 -0
- package/dist/domain/editable-artifacts/hash.d.ts +11 -0
- package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
- package/dist/domain/editable-artifacts/index.d.ts +14 -0
- package/dist/domain/editable-artifacts/ports.d.ts +341 -0
- package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
- package/dist/domain/editable-artifacts/service.d.ts +105 -0
- package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
- package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
- package/dist/domain/editable-artifacts/types.d.ts +410 -0
- package/dist/domain/fiken.d.ts +35 -0
- package/dist/domain/memory-slack-delivery.d.ts +19 -0
- package/dist/domain/packs.d.ts +29 -1
- package/dist/domain/personal-connection-delegations.d.ts +9 -1
- package/dist/domain/scheduled-tasks.d.ts +2 -2
- package/dist/domain/session-tool-policy.d.ts +2 -2
- package/dist/domain/sessions.d.ts +24 -8
- package/dist/domain/skill-imports.d.ts +32 -0
- package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
- package/dist/domain/timeline-annotations.d.ts +5 -0
- package/dist/domain/video-generation-capabilities.d.ts +10 -0
- package/dist/domain/video-generation.d.ts +47 -0
- package/dist/editable-artifact-live/application.d.ts +69 -0
- package/dist/editable-artifact-live/errors.d.ts +1 -0
- package/dist/editable-artifact-live/index.d.ts +8 -0
- package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
- package/dist/editable-artifact-live/ports.d.ts +100 -0
- package/dist/editable-artifact-live/server.d.ts +71 -0
- package/dist/editable-artifact-live/ticket.d.ts +40 -0
- package/dist/editable-artifact-live/types.d.ts +213 -0
- package/dist/editable-artifact-live/wire.d.ts +34 -0
- package/dist/editable-artifact-live.d.ts +2 -0
- package/dist/editable-artifact-live.js +58 -0
- package/dist/editable-artifact-live.js.map +1 -0
- package/dist/editable-artifacts.d.ts +2 -0
- package/dist/editable-artifacts.js +179 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/index.d.ts +37 -26
- package/dist/index.js +4145 -959
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +6 -2
- package/dist/rigs/index.d.ts +1 -1
- package/dist/rigs/provider-images.d.ts +31 -0
- package/dist/sandbox/fleet.d.ts +8 -5
- package/dist/sandbox/routing.d.ts +1 -0
- package/dist/session-authorization.d.ts +4 -2
- package/dist/transcription.d.ts +5 -0
- package/dist/workflow-wake-contract.d.ts +7 -0
- package/package.json +22 -10
- package/src/access/index.ts +14 -2
- package/src/application/new-session-drafts.ts +2 -0
- package/src/application/session-commands.ts +188 -107
- package/src/canonical-human-identities.ts +27 -0
- package/src/dependencies.ts +34 -2
- package/src/domain/capabilities.ts +729 -408
- package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
- package/src/domain/conversation-integrations.ts +1238 -0
- package/src/domain/durable-learning-slack-publication.ts +202 -0
- package/src/domain/editable-artifacts/agent-application.ts +687 -0
- package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
- package/src/domain/editable-artifacts/durable-export.ts +734 -0
- package/src/domain/editable-artifacts/errors.ts +115 -0
- package/src/domain/editable-artifacts/hash.ts +131 -0
- package/src/domain/editable-artifacts/in-memory.ts +1662 -0
- package/src/domain/editable-artifacts/index.ts +14 -0
- package/src/domain/editable-artifacts/ports.ts +462 -0
- package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
- package/src/domain/editable-artifacts/service.ts +2402 -0
- package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
- package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
- package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
- package/src/domain/editable-artifacts/types.ts +835 -0
- package/src/domain/fiken.ts +88 -0
- package/src/domain/insights.ts +163 -50
- package/src/domain/memory-slack-delivery.ts +204 -0
- package/src/domain/memory-slack-publication.ts +5 -2
- package/src/domain/packs.ts +266 -5
- package/src/domain/personal-connection-delegations.ts +97 -6
- package/src/domain/scheduled-tasks.ts +171 -29
- package/src/domain/session-tool-policy.ts +8 -2
- package/src/domain/sessions.ts +395 -148
- package/src/domain/skill-imports.ts +350 -0
- package/src/domain/slack-publication-secret-safety.ts +42 -0
- package/src/domain/timeline-annotations.ts +204 -0
- package/src/domain/video-generation-capabilities.ts +104 -0
- package/src/domain/video-generation.ts +249 -0
- package/src/editable-artifact-live/application.ts +182 -0
- package/src/editable-artifact-live/errors.ts +4 -0
- package/src/editable-artifact-live/index.ts +8 -0
- package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
- package/src/editable-artifact-live/ports.ts +139 -0
- package/src/editable-artifact-live/server.ts +1707 -0
- package/src/editable-artifact-live/ticket.ts +217 -0
- package/src/editable-artifact-live/types.ts +286 -0
- package/src/editable-artifact-live/wire.ts +95 -0
- package/src/editable-artifact-live.ts +2 -0
- package/src/editable-artifacts.ts +2 -0
- package/src/index.ts +15 -0
- package/src/managed-session.ts +19 -2
- package/src/rigs/index.ts +26 -19
- package/src/rigs/provider-images.ts +110 -0
- package/src/sandbox/fleet.ts +72 -66
- package/src/sandbox/routing.ts +6 -3
- package/src/session-authorization.ts +187 -52
- package/src/transcription.ts +5 -0
- package/src/workflow-wake-contract.ts +8 -0
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
2
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
2
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
3
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
5
4
|
import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
|
|
6
5
|
import {
|
|
7
6
|
CapabilityCatalogItem,
|
|
7
|
+
capabilityCatalogItemIsTrustedForExposure,
|
|
8
|
+
FIKEN_PROVIDER_DOMAIN,
|
|
9
|
+
FIRST_PARTY_MCP_TOOL_NAMES,
|
|
8
10
|
type AccessGrant,
|
|
11
|
+
type CapabilityAction,
|
|
9
12
|
type CapabilityCatalogResponse,
|
|
10
13
|
type CapabilityInstallation,
|
|
14
|
+
type ConnectionMetadata,
|
|
11
15
|
type CreateCapabilityCatalogItemRequest,
|
|
12
16
|
type EnableCapabilityRequest,
|
|
13
17
|
type McpServerConnectionRef,
|
|
18
|
+
type McpPersonalConnectionDelegation,
|
|
14
19
|
type SocialConnection,
|
|
15
20
|
} from "@opengeni/contracts";
|
|
16
21
|
import {
|
|
@@ -24,42 +29,33 @@ import {
|
|
|
24
29
|
decryptedCapabilityHeaders,
|
|
25
30
|
disableCapabilityInstallation,
|
|
26
31
|
enableCapabilityInstallation,
|
|
27
|
-
enablePackInstallation,
|
|
28
32
|
encryptVariableSetValue,
|
|
29
33
|
getCapabilityCatalogItem,
|
|
30
34
|
getCapabilityInstallation,
|
|
31
35
|
getConnectionMetadata,
|
|
32
36
|
getCodexAppsCredentialAuthorizationForRun,
|
|
33
37
|
getWorkspaceGrant,
|
|
34
|
-
getPackInstallation,
|
|
35
38
|
getStoredCapabilityHeaderCiphertext,
|
|
36
|
-
getVariableSet,
|
|
37
39
|
listCapabilityCatalogItems,
|
|
38
40
|
listCapabilityInstallations,
|
|
39
41
|
listConnectionsMetadata,
|
|
40
42
|
listEnabledMcpCapabilityServers,
|
|
43
|
+
listInstalledApiIntegrations,
|
|
44
|
+
listInstalledSkills,
|
|
41
45
|
listPackInstallations,
|
|
42
46
|
listSocialConnections,
|
|
43
47
|
mcpServerIdForCapability,
|
|
44
|
-
updatePackInstallationStatus,
|
|
45
48
|
upsertCapabilityCatalogItem,
|
|
46
49
|
type Database,
|
|
50
|
+
type ApiIntegrationRuntime,
|
|
47
51
|
type EnabledMcpCapabilityServer,
|
|
52
|
+
type InstalledSkillSummary,
|
|
48
53
|
} from "@opengeni/db";
|
|
49
54
|
import { HTTPException } from "hono/http-exception";
|
|
50
55
|
import { hasPermission } from "../access";
|
|
51
|
-
import {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
type SkillLibraryEntry,
|
|
55
|
-
} from "@opengeni/runtime/skill-library";
|
|
56
|
-
import { validateVariableSetAttachment } from "./environments";
|
|
57
|
-
import {
|
|
58
|
-
assertPackSandboxImageCompatible,
|
|
59
|
-
listCapabilityPacks,
|
|
60
|
-
listWorkspaceCapabilityPacks,
|
|
61
|
-
resolveCapabilityPack,
|
|
62
|
-
} from "./packs";
|
|
56
|
+
import { isFikenConnection, preferredFikenConnection } from "./fiken";
|
|
57
|
+
import { listSkillLibraryEntries, type SkillLibraryEntry } from "@opengeni/runtime/skill-library";
|
|
58
|
+
import { listCapabilityPacks, listWorkspaceCapabilityPacks } from "./packs";
|
|
63
59
|
|
|
64
60
|
const officialMcpRegistryUrl = "https://registry.modelcontextprotocol.io";
|
|
65
61
|
const firstPartyMcpServerIds = new Set(["opengeni", "files", "docs"]);
|
|
@@ -68,6 +64,7 @@ const mcpRegistryMaxPages = 3;
|
|
|
68
64
|
const mcpCapabilityProbeTimeoutMs = 15000;
|
|
69
65
|
const maxMcpCredentialHeaders = 16;
|
|
70
66
|
const maxMcpCredentialHeaderValueLength = 4096;
|
|
67
|
+
const officialGmailMcpUrl = "https://gmailmcp.googleapis.com/mcp/v1";
|
|
71
68
|
// RFC 9110 field-name token characters.
|
|
72
69
|
const mcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
73
70
|
|
|
@@ -83,8 +80,9 @@ export async function buildCapabilityCatalog(input: {
|
|
|
83
80
|
packInstallations,
|
|
84
81
|
workspacePacks,
|
|
85
82
|
socialConnections,
|
|
86
|
-
|
|
83
|
+
workspaceConnections,
|
|
87
84
|
curatedLibrarySkills,
|
|
85
|
+
installedSkills,
|
|
88
86
|
codexAppsCredentialId,
|
|
89
87
|
] = await Promise.all([
|
|
90
88
|
listCapabilityCatalogItems(input.db, input.workspaceId),
|
|
@@ -92,14 +90,23 @@ export async function buildCapabilityCatalog(input: {
|
|
|
92
90
|
listPackInstallations(input.db, input.workspaceId),
|
|
93
91
|
listWorkspaceCapabilityPacks(input.db, input.workspaceId),
|
|
94
92
|
listSocialConnections(input.db, input.workspaceId, 500, input.subjectId),
|
|
95
|
-
|
|
93
|
+
listConnectionsMetadata(input.db, input.workspaceId, null),
|
|
96
94
|
discoverCuratedSkillLibraryItems(),
|
|
95
|
+
listInstalledSkills(input.db, input.workspaceId),
|
|
97
96
|
input.settings.codexConnectedAppsEnabled
|
|
98
97
|
? resolveCodexAppsCredentialIdForRun(input.db, input.workspaceId)
|
|
99
98
|
: Promise.resolve(null),
|
|
100
99
|
]);
|
|
100
|
+
const catalogInstallations = capabilityInstallations.filter(
|
|
101
|
+
(installation) => installation.kind === "mcp",
|
|
102
|
+
);
|
|
101
103
|
const capabilityInstallationById = new Map(
|
|
102
|
-
|
|
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]),
|
|
103
110
|
);
|
|
104
111
|
const activePackIds = new Set(
|
|
105
112
|
packInstallations
|
|
@@ -112,27 +119,43 @@ export async function buildCapabilityCatalog(input: {
|
|
|
112
119
|
packCatalogItem(pack, builtInPackIds.has(pack.id) ? "built_in" : "manual"),
|
|
113
120
|
),
|
|
114
121
|
...configuredMcpCatalogItems(input.settings),
|
|
115
|
-
...
|
|
116
|
-
|
|
122
|
+
...providerIntegrationCatalogItems(socialConnections),
|
|
123
|
+
fikenCatalogItem(workspaceConnections.filter(isFikenConnection)),
|
|
117
124
|
...curatedLibrarySkills,
|
|
125
|
+
...installedSkills
|
|
126
|
+
.filter(
|
|
127
|
+
(skill) =>
|
|
128
|
+
skill.source !== "library" && skill.owners.some((owner) => owner.kind === "direct"),
|
|
129
|
+
)
|
|
130
|
+
.map(installedSkillCatalogItem),
|
|
118
131
|
];
|
|
119
132
|
const codexApps = input.settings.codexConnectedAppsEnabled
|
|
120
133
|
? codexAppsCatalogItem(codexAppsCredentialId !== null)
|
|
121
134
|
: null;
|
|
122
135
|
const items = dedupeCatalogItems([
|
|
123
136
|
...builtIns,
|
|
124
|
-
...persistedItems.filter(
|
|
137
|
+
...persistedItems.filter(
|
|
138
|
+
(item) =>
|
|
139
|
+
item.kind !== "skill" &&
|
|
140
|
+
item.kind !== "api" &&
|
|
141
|
+
item.kind !== "plugin" &&
|
|
142
|
+
!isReservedCodexAppsCatalogItem(item),
|
|
143
|
+
),
|
|
125
144
|
// Keep the reserved, server-derived item authoritative over any stale
|
|
126
145
|
// legacy catalog row with the same id.
|
|
127
146
|
...(codexApps ? [codexApps] : []),
|
|
128
147
|
])
|
|
129
|
-
.map((item) =>
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
.map((item) => {
|
|
149
|
+
const projected =
|
|
150
|
+
item.kind === "skill"
|
|
151
|
+
? applyInstalledSkillEnablement(item, installedSkillById.get(item.id))
|
|
152
|
+
: applyCapabilityEnablement(item, capabilityInstallationById.get(item.id), activePackIds);
|
|
153
|
+
return applyCapabilityLifecycle(projected);
|
|
154
|
+
})
|
|
132
155
|
.sort(compareCatalogItems);
|
|
133
156
|
return {
|
|
134
157
|
items,
|
|
135
|
-
installations:
|
|
158
|
+
installations: catalogInstallations,
|
|
136
159
|
};
|
|
137
160
|
}
|
|
138
161
|
|
|
@@ -150,7 +173,17 @@ export async function createCatalogItem(input: {
|
|
|
150
173
|
}
|
|
151
174
|
if (id.startsWith("skill:")) {
|
|
152
175
|
throw new HTTPException(422, {
|
|
153
|
-
message: "
|
|
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",
|
|
154
187
|
});
|
|
155
188
|
}
|
|
156
189
|
if (
|
|
@@ -212,6 +245,26 @@ export async function enableCapability(input: {
|
|
|
212
245
|
input.settings,
|
|
213
246
|
input.capabilityId,
|
|
214
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
|
+
}
|
|
215
268
|
if (item.kind === "mcp" && !item.runtime.available) {
|
|
216
269
|
throw new HTTPException(422, {
|
|
217
270
|
message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled",
|
|
@@ -226,44 +279,6 @@ export async function enableCapability(input: {
|
|
|
226
279
|
delete installationConfig.headersEncrypted;
|
|
227
280
|
delete installationConfig.headerNames;
|
|
228
281
|
delete installationConfig.connectionRef;
|
|
229
|
-
if (item.kind === "skill" && item.source === "library") {
|
|
230
|
-
const libraryId = stringMetadata(item.metadata.libraryId);
|
|
231
|
-
const catalogVersion = stringMetadata(item.metadata.version);
|
|
232
|
-
if (!libraryId || !catalogVersion) {
|
|
233
|
-
throw new HTTPException(422, {
|
|
234
|
-
message: `skill library metadata is incomplete for ${item.id}`,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
const requestedVersion = input.payload.config.version;
|
|
238
|
-
if (requestedVersion !== undefined && typeof requestedVersion !== "string") {
|
|
239
|
-
throw new HTTPException(422, {
|
|
240
|
-
message: "skill activation config.version must be a string",
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
const normalizedVersion = requestedVersion?.trim() || catalogVersion;
|
|
244
|
-
if (normalizedVersion !== catalogVersion) {
|
|
245
|
-
throw new HTTPException(422, {
|
|
246
|
-
message: `skill ${libraryId} only supports immutable version ${catalogVersion}`,
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
const entry = getSkillLibraryEntry(libraryId, normalizedVersion);
|
|
250
|
-
if (!entry) {
|
|
251
|
-
throw new HTTPException(422, {
|
|
252
|
-
message: `skill library entry is unavailable: ${libraryId}@${normalizedVersion}`,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
// Skill activation has a deliberately narrow config surface. In
|
|
256
|
-
// particular, no variable-set, connection, credential-header, MCP, or
|
|
257
|
-
// provider-routing input is persisted or consulted for a library skill.
|
|
258
|
-
installationConfig = { version: entry.version };
|
|
259
|
-
installationMetadata = {
|
|
260
|
-
libraryId: entry.id,
|
|
261
|
-
libraryVersion: entry.version,
|
|
262
|
-
contentSha256: entry.contentSha256,
|
|
263
|
-
sourceCommit: entry.sourceCommit,
|
|
264
|
-
provenance: entry.provenance,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
282
|
if (item.kind === "mcp") {
|
|
268
283
|
const headers = await resolveMcpCredentialHeaders(input, item);
|
|
269
284
|
const connectionRef = input.payload.connectionRef
|
|
@@ -286,81 +301,6 @@ export async function enableCapability(input: {
|
|
|
286
301
|
);
|
|
287
302
|
}
|
|
288
303
|
}
|
|
289
|
-
if (item.kind === "pack") {
|
|
290
|
-
const packId = packIdFromCapabilityId(item.id);
|
|
291
|
-
const pack = await resolveCapabilityPack(input.db, input.workspaceId, packId);
|
|
292
|
-
if (!pack) {
|
|
293
|
-
throw new HTTPException(404, { message: "pack not found" });
|
|
294
|
-
}
|
|
295
|
-
await assertPackSandboxImageCompatible(input.db, input.workspaceId, pack);
|
|
296
|
-
// The unified capability-enable path accepts an initial variableSet
|
|
297
|
-
// attachment (`payload.variableSetId`), mirroring POST /packs/:id/enable:
|
|
298
|
-
// a request-supplied id is validated as a fresh attachment, otherwise the
|
|
299
|
-
// attachment stored by a previous enable is preserved and re-validated.
|
|
300
|
-
const existing = await getPackInstallation(input.db, input.workspaceId, packId);
|
|
301
|
-
const storedVariableSetId =
|
|
302
|
-
typeof existing?.metadata.variableSetId === "string"
|
|
303
|
-
? existing.metadata.variableSetId
|
|
304
|
-
: typeof existing?.metadata.environmentId === "string"
|
|
305
|
-
? existing.metadata.environmentId
|
|
306
|
-
: undefined;
|
|
307
|
-
const requestedVariableSetId = input.payload.variableSetId;
|
|
308
|
-
const variableSetId = requestedVariableSetId ?? storedVariableSetId;
|
|
309
|
-
if (pack.variableSet?.required && !variableSetId) {
|
|
310
|
-
throw new HTTPException(422, {
|
|
311
|
-
message: `pack ${packId} requires an variableSet attachment; pass variableSetId`,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
if (variableSetId) {
|
|
315
|
-
if (requestedVariableSetId) {
|
|
316
|
-
// A fresh attachment: validate it like the packs enable endpoint does.
|
|
317
|
-
// The grant holds workspace:admin here, which implies variable-sets:use,
|
|
318
|
-
// so the attachment authorization succeeds for this caller.
|
|
319
|
-
const variableSet = await validateVariableSetAttachment(
|
|
320
|
-
{ settings: input.settings, db: input.db },
|
|
321
|
-
input.grant,
|
|
322
|
-
input.workspaceId,
|
|
323
|
-
requestedVariableSetId,
|
|
324
|
-
);
|
|
325
|
-
const missing = (pack.variableSet?.requiredVariables ?? []).filter(
|
|
326
|
-
(name) => !variableSet.variables.some((variable) => variable.name === name),
|
|
327
|
-
);
|
|
328
|
-
if (missing.length > 0) {
|
|
329
|
-
throw new HTTPException(422, {
|
|
330
|
-
message: `variable set is missing required variable(s): ${missing.join(", ")}`,
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
} else {
|
|
334
|
-
// The stored attachment was authorized at pack-enable time, but the
|
|
335
|
-
// variableSet may have been deleted or its variables changed since;
|
|
336
|
-
// re-validate it like the packs enable endpoint does.
|
|
337
|
-
const variableSet = await getVariableSet(input.db, input.workspaceId, variableSetId);
|
|
338
|
-
if (!variableSet) {
|
|
339
|
-
throw new HTTPException(422, {
|
|
340
|
-
message: `the stored variableSet attachment for pack ${packId} no longer exists; re-enable it with variableSetId`,
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
const missing = (pack.variableSet?.requiredVariables ?? []).filter(
|
|
344
|
-
(name) => !variableSet.variables.some((variable) => variable.name === name),
|
|
345
|
-
);
|
|
346
|
-
if (missing.length > 0) {
|
|
347
|
-
throw new HTTPException(422, {
|
|
348
|
-
message: `variable set is missing required variable(s): ${missing.join(", ")}`,
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
await enablePackInstallation(input.db, {
|
|
354
|
-
accountId: input.accountId,
|
|
355
|
-
workspaceId: input.workspaceId,
|
|
356
|
-
packId,
|
|
357
|
-
metadata: {
|
|
358
|
-
...input.payload.metadata,
|
|
359
|
-
packVersion: pack.version,
|
|
360
|
-
...(variableSetId ? { variableSetId } : {}),
|
|
361
|
-
},
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
304
|
return await enableCapabilityInstallation(input.db, {
|
|
365
305
|
accountId: input.accountId,
|
|
366
306
|
workspaceId: input.workspaceId,
|
|
@@ -433,11 +373,15 @@ function normalizedMcpCredentialHeaders(
|
|
|
433
373
|
const seen = new Set<string>();
|
|
434
374
|
for (const [name, value] of entries) {
|
|
435
375
|
if (!mcpCredentialHeaderName.test(name)) {
|
|
436
|
-
throw new HTTPException(422, {
|
|
376
|
+
throw new HTTPException(422, {
|
|
377
|
+
message: `invalid credential header name: ${name}`,
|
|
378
|
+
});
|
|
437
379
|
}
|
|
438
380
|
const lower = name.toLowerCase();
|
|
439
381
|
if (seen.has(lower)) {
|
|
440
|
-
throw new HTTPException(422, {
|
|
382
|
+
throw new HTTPException(422, {
|
|
383
|
+
message: `duplicate credential header name: ${name}`,
|
|
384
|
+
});
|
|
441
385
|
}
|
|
442
386
|
seen.add(lower);
|
|
443
387
|
if (value.length === 0 || value.length > maxMcpCredentialHeaderValueLength) {
|
|
@@ -463,6 +407,15 @@ async function validateMcpCapabilityConnectionRef(
|
|
|
463
407
|
ref: McpServerConnectionRef,
|
|
464
408
|
): Promise<McpServerConnectionRef> {
|
|
465
409
|
const subjectScope = ref.subjectScope ?? "workspace";
|
|
410
|
+
const personalOnly =
|
|
411
|
+
item.metadata.connectionOwnership === "personal_only" ||
|
|
412
|
+
item.endpointUrl?.replace(/\/+$/, "") === officialGmailMcpUrl;
|
|
413
|
+
if (personalOnly && subjectScope !== "subject") {
|
|
414
|
+
throw new HTTPException(422, {
|
|
415
|
+
message:
|
|
416
|
+
"this capability requires a personal connection; each workspace member must connect their own account",
|
|
417
|
+
});
|
|
418
|
+
}
|
|
466
419
|
const normalized: McpServerConnectionRef = {
|
|
467
420
|
providerDomain: ref.providerDomain.trim(),
|
|
468
421
|
subjectScope,
|
|
@@ -472,11 +425,17 @@ async function validateMcpCapabilityConnectionRef(
|
|
|
472
425
|
...(ref.scopes ? { scopes: uniqueStrings(ref.scopes) } : {}),
|
|
473
426
|
...(ref.resource ? { resource: ref.resource } : {}),
|
|
474
427
|
...(ref.selectedResources
|
|
475
|
-
? {
|
|
428
|
+
? {
|
|
429
|
+
selectedResources: ref.selectedResources.map((resource) => ({
|
|
430
|
+
...resource,
|
|
431
|
+
})),
|
|
432
|
+
}
|
|
476
433
|
: {}),
|
|
477
434
|
};
|
|
478
435
|
if (!normalized.providerDomain) {
|
|
479
|
-
throw new HTTPException(422, {
|
|
436
|
+
throw new HTTPException(422, {
|
|
437
|
+
message: "connectionRef.providerDomain is required",
|
|
438
|
+
});
|
|
480
439
|
}
|
|
481
440
|
if (!item.endpointUrl || !item.runtime.mcpServerId) {
|
|
482
441
|
throw new HTTPException(422, {
|
|
@@ -713,31 +672,36 @@ export async function disableCapability(input: {
|
|
|
713
672
|
input.settings,
|
|
714
673
|
input.capabilityId,
|
|
715
674
|
);
|
|
716
|
-
if (
|
|
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") {
|
|
717
696
|
throw new HTTPException(409, {
|
|
718
697
|
message:
|
|
719
698
|
"built-in and configured capabilities are always available; remove them from configuration to disable them",
|
|
720
699
|
});
|
|
721
700
|
}
|
|
722
|
-
if (
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
packIdFromCapabilityId(item.id),
|
|
727
|
-
"disabled",
|
|
728
|
-
).catch(() => undefined);
|
|
729
|
-
if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
|
|
730
|
-
await enableCapabilityInstallation(input.db, {
|
|
731
|
-
accountId: input.accountId,
|
|
732
|
-
workspaceId: input.workspaceId,
|
|
733
|
-
capabilityId: item.id,
|
|
734
|
-
kind: "pack",
|
|
735
|
-
metadata: {},
|
|
736
|
-
config: {},
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
} else if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
|
|
740
|
-
throw new HTTPException(409, { message: "capability is not currently enabled" });
|
|
701
|
+
if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
|
|
702
|
+
throw new HTTPException(409, {
|
|
703
|
+
message: "capability is not currently enabled",
|
|
704
|
+
});
|
|
741
705
|
}
|
|
742
706
|
return await disableCapabilityInstallation(input.db, input.workspaceId, item.id);
|
|
743
707
|
}
|
|
@@ -746,17 +710,109 @@ export async function settingsWithEnabledCapabilityMcpServers(
|
|
|
746
710
|
db: Database,
|
|
747
711
|
workspaceId: string,
|
|
748
712
|
settings: Settings,
|
|
713
|
+
options?: {
|
|
714
|
+
subjectId?: string;
|
|
715
|
+
personalConnectionDelegations?: readonly McpPersonalConnectionDelegation[];
|
|
716
|
+
onResolvedApiIntegrations?: (integrations: readonly ApiIntegrationRuntime[]) => void;
|
|
717
|
+
},
|
|
749
718
|
): Promise<Settings> {
|
|
750
|
-
const
|
|
719
|
+
const apiIntegrationsPromise = options?.subjectId
|
|
720
|
+
? listInstalledApiIntegrations(db, workspaceId, options.subjectId)
|
|
721
|
+
: listInstalledApiIntegrationsForDelegations(
|
|
722
|
+
db,
|
|
723
|
+
workspaceId,
|
|
724
|
+
options?.personalConnectionDelegations ?? [],
|
|
725
|
+
);
|
|
726
|
+
const [enabled, apiIntegrations, codexAppsCredentialId] = await Promise.all([
|
|
751
727
|
listEnabledMcpCapabilityServers(db, workspaceId),
|
|
728
|
+
apiIntegrationsPromise,
|
|
752
729
|
resolveCodexAppsCredentialIdForRun(db, workspaceId),
|
|
753
730
|
]);
|
|
731
|
+
options?.onResolvedApiIntegrations?.(apiIntegrations);
|
|
754
732
|
return settingsWithCodexAppsMcpServer(
|
|
755
|
-
|
|
733
|
+
settingsWithApiIntegrationServers(
|
|
734
|
+
settingsWithMcpCapabilityServers(settings, enabled),
|
|
735
|
+
apiIntegrations,
|
|
736
|
+
),
|
|
756
737
|
codexAppsCredentialId !== null,
|
|
757
738
|
);
|
|
758
739
|
}
|
|
759
740
|
|
|
741
|
+
export function apiIntegrationsMatchingDelegations(
|
|
742
|
+
integrations: readonly ApiIntegrationRuntime[],
|
|
743
|
+
delegations: readonly McpPersonalConnectionDelegation[],
|
|
744
|
+
): ApiIntegrationRuntime[] {
|
|
745
|
+
const exact = new Set(
|
|
746
|
+
delegations.map((delegation) =>
|
|
747
|
+
[
|
|
748
|
+
delegation.serverId,
|
|
749
|
+
delegation.connectionId,
|
|
750
|
+
delegation.providerDomain.toLowerCase(),
|
|
751
|
+
delegation.kind ?? "",
|
|
752
|
+
].join("\u0000"),
|
|
753
|
+
),
|
|
754
|
+
);
|
|
755
|
+
return integrations.filter((integration) => {
|
|
756
|
+
const ref = integration.connectionRef;
|
|
757
|
+
if (!ref || ref.subjectScope !== "subject" || !ref.connectionId) return false;
|
|
758
|
+
return exact.has(
|
|
759
|
+
[
|
|
760
|
+
integration.serverId,
|
|
761
|
+
ref.connectionId,
|
|
762
|
+
ref.providerDomain.toLowerCase(),
|
|
763
|
+
ref.kind ?? "",
|
|
764
|
+
].join("\u0000"),
|
|
765
|
+
);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function listInstalledApiIntegrationsForDelegations(
|
|
770
|
+
db: Database,
|
|
771
|
+
workspaceId: string,
|
|
772
|
+
delegations: readonly McpPersonalConnectionDelegation[],
|
|
773
|
+
): Promise<ApiIntegrationRuntime[]> {
|
|
774
|
+
const workspace = await listInstalledApiIntegrations(db, workspaceId);
|
|
775
|
+
if (delegations.length === 0) return workspace;
|
|
776
|
+
const owners = [...new Set(delegations.map((delegation) => delegation.ownerSubjectId))];
|
|
777
|
+
const delegatedByOwner = await Promise.all(
|
|
778
|
+
owners.map(async (subjectId) =>
|
|
779
|
+
apiIntegrationsMatchingDelegations(
|
|
780
|
+
await listInstalledApiIntegrations(db, workspaceId, subjectId),
|
|
781
|
+
delegations.filter((delegation) => delegation.ownerSubjectId === subjectId),
|
|
782
|
+
),
|
|
783
|
+
),
|
|
784
|
+
);
|
|
785
|
+
const byServerId = new Map(workspace.map((integration) => [integration.serverId, integration]));
|
|
786
|
+
for (const integration of delegatedByOwner.flat())
|
|
787
|
+
byServerId.set(integration.serverId, integration);
|
|
788
|
+
return [...byServerId.values()];
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function settingsWithApiIntegrationServers(
|
|
792
|
+
settings: Settings,
|
|
793
|
+
integrations: readonly ApiIntegrationRuntime[],
|
|
794
|
+
): Settings {
|
|
795
|
+
if (integrations.length === 0) return settings;
|
|
796
|
+
const existingIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
797
|
+
const dynamicServers = integrations
|
|
798
|
+
.filter((integration) => !existingIds.has(integration.serverId))
|
|
799
|
+
.map((integration) => ({
|
|
800
|
+
id: integration.serverId,
|
|
801
|
+
name: integration.name,
|
|
802
|
+
// Local adapters never send MCP traffic to this URL. Keeping the exact
|
|
803
|
+
// provider base URL in Settings preserves destination identity for policy,
|
|
804
|
+
// diagnostics, and the stable session-MCP approval fallback.
|
|
805
|
+
url: integration.baseUrl,
|
|
806
|
+
allowedTools: [...integration.allowedTools],
|
|
807
|
+
cacheToolsList: true,
|
|
808
|
+
requireApproval: integration.requireApproval,
|
|
809
|
+
...(integration.connectionRef ? { connectionRef: integration.connectionRef } : {}),
|
|
810
|
+
}));
|
|
811
|
+
return dynamicServers.length
|
|
812
|
+
? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] }
|
|
813
|
+
: settings;
|
|
814
|
+
}
|
|
815
|
+
|
|
760
816
|
/**
|
|
761
817
|
* Resolve executable Apps authority. The connector must remain active and its
|
|
762
818
|
* exact owner must still hold workspace connection-management permission.
|
|
@@ -846,6 +902,9 @@ export function settingsWithMcpCapabilityServers(
|
|
|
846
902
|
...(server.allowedTools ? { allowedTools: server.allowedTools } : {}),
|
|
847
903
|
...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
|
|
848
904
|
cacheToolsList: server.cacheToolsList ?? false,
|
|
905
|
+
...(server.requireApproval !== undefined
|
|
906
|
+
? { requireApproval: server.requireApproval }
|
|
907
|
+
: {}),
|
|
849
908
|
...(headers && headers !== "unavailable" ? { headers } : {}),
|
|
850
909
|
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
851
910
|
},
|
|
@@ -931,7 +990,9 @@ async function fetchMcpRegistryPage(
|
|
|
931
990
|
try {
|
|
932
991
|
const response = await fetchImpl(url, { signal: controller.signal });
|
|
933
992
|
if (!response.ok) {
|
|
934
|
-
throw new HTTPException(502, {
|
|
993
|
+
throw new HTTPException(502, {
|
|
994
|
+
message: `MCP registry returned ${response.status}`,
|
|
995
|
+
});
|
|
935
996
|
}
|
|
936
997
|
return (await response.json()) as McpRegistryPage;
|
|
937
998
|
} catch (error) {
|
|
@@ -939,7 +1000,9 @@ async function fetchMcpRegistryPage(
|
|
|
939
1000
|
throw error;
|
|
940
1001
|
}
|
|
941
1002
|
if (error instanceof Error && error.name === "AbortError") {
|
|
942
|
-
throw new HTTPException(504, {
|
|
1003
|
+
throw new HTTPException(504, {
|
|
1004
|
+
message: "MCP registry request timed out",
|
|
1005
|
+
});
|
|
943
1006
|
}
|
|
944
1007
|
throw new HTTPException(502, {
|
|
945
1008
|
message: `MCP registry request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -1002,6 +1065,7 @@ function packCatalogItem(
|
|
|
1002
1065
|
connectors: pack.connectors,
|
|
1003
1066
|
knowledge: pack.knowledge,
|
|
1004
1067
|
scheduledTaskTemplates: pack.scheduledTaskTemplates,
|
|
1068
|
+
...(pack.variableSet ? { variableSet: pack.variableSet } : {}),
|
|
1005
1069
|
// Runtime composition surface only: skill names, never file content.
|
|
1006
1070
|
...(pack.sandboxImage ? { sandboxImage: pack.sandboxImage } : {}),
|
|
1007
1071
|
...(pack.sandboxProviderImages ? { sandboxProviderImages: pack.sandboxProviderImages } : {}),
|
|
@@ -1011,34 +1075,40 @@ function packCatalogItem(
|
|
|
1011
1075
|
}
|
|
1012
1076
|
|
|
1013
1077
|
function configuredMcpCatalogItems(settings: Settings): CapabilityCatalogItem[] {
|
|
1014
|
-
return
|
|
1015
|
-
.
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1078
|
+
return (
|
|
1079
|
+
settings.mcpServers
|
|
1080
|
+
// OpenGeni, Files, and Document Search are native runtime surfaces. They
|
|
1081
|
+
// remain available to sessions through configuration, but are not things a
|
|
1082
|
+
// user installs, connects, or enables in the Capabilities control center.
|
|
1083
|
+
.filter(
|
|
1084
|
+
(server) =>
|
|
1085
|
+
server.id !== CODEX_APPS_MCP_SERVER_ID && !firstPartyMcpServerIds.has(server.id),
|
|
1086
|
+
)
|
|
1087
|
+
.map((server) =>
|
|
1088
|
+
CapabilityCatalogItem.parse({
|
|
1089
|
+
id: `mcp:${server.id}`,
|
|
1090
|
+
kind: "mcp",
|
|
1091
|
+
source: "configured",
|
|
1092
|
+
name: server.name ?? server.id,
|
|
1093
|
+
description: null,
|
|
1094
|
+
category: "configured",
|
|
1095
|
+
tags: ["mcp", ...(server.allowedTools?.length ? ["limited-tools"] : [])],
|
|
1096
|
+
endpointUrl: server.url,
|
|
1097
|
+
tools: [{ kind: "mcp", id: server.id }],
|
|
1098
|
+
runtime: {
|
|
1099
|
+
available: true,
|
|
1100
|
+
mcpServerId: server.id,
|
|
1101
|
+
transport: "streamable-http",
|
|
1102
|
+
notes: "Managed by this OpenGeni deployment through OPENGENI_MCP_SERVERS.",
|
|
1103
|
+
},
|
|
1104
|
+
metadata: {
|
|
1105
|
+
mcpServerId: server.id,
|
|
1106
|
+
allowedTools: server.allowedTools ?? [],
|
|
1107
|
+
cacheToolsList: server.cacheToolsList,
|
|
1108
|
+
},
|
|
1109
|
+
}),
|
|
1110
|
+
)
|
|
1111
|
+
);
|
|
1042
1112
|
}
|
|
1043
1113
|
|
|
1044
1114
|
function isReservedCodexAppsCatalogItem(item: CapabilityCatalogItem): boolean {
|
|
@@ -1089,160 +1159,212 @@ export function codexAppsCatalogItem(available: boolean): CapabilityCatalogItem
|
|
|
1089
1159
|
});
|
|
1090
1160
|
}
|
|
1091
1161
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1162
|
+
type SocialProviderIntegrationDefinition = {
|
|
1163
|
+
provider: "x" | "reddit";
|
|
1164
|
+
name: string;
|
|
1165
|
+
description: string;
|
|
1166
|
+
providerDomain: string;
|
|
1167
|
+
homepageUrl: string;
|
|
1168
|
+
tags: string[];
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
const SOCIAL_PROVIDER_INTEGRATIONS: readonly SocialProviderIntegrationDefinition[] = [
|
|
1172
|
+
{
|
|
1173
|
+
provider: "x",
|
|
1099
1174
|
name: "X",
|
|
1100
1175
|
description:
|
|
1101
|
-
"Connect
|
|
1102
|
-
category: "social-media",
|
|
1103
|
-
tags: ["api", "x", "twitter", "social", "marketing"],
|
|
1104
|
-
homepageUrl: "https://x.com",
|
|
1105
|
-
authModel: "oauth2_authorization_code_pkce",
|
|
1176
|
+
"Connect one or more X accounts for live search, mentions, thread context, post sync, and permission-controlled replies.",
|
|
1106
1177
|
providerDomain: "x.com",
|
|
1107
|
-
|
|
1108
|
-
|
|
1178
|
+
homepageUrl: "https://x.com",
|
|
1179
|
+
tags: ["api", "x", "twitter", "social", "marketing"],
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
provider: "reddit",
|
|
1183
|
+
name: "Reddit",
|
|
1184
|
+
description:
|
|
1185
|
+
"Connect one or more Reddit accounts for search, mentions, thread context, account sync, and permission-controlled replies.",
|
|
1186
|
+
providerDomain: "reddit.com",
|
|
1187
|
+
homepageUrl: "https://www.reddit.com",
|
|
1188
|
+
tags: ["api", "reddit", "social", "community", "marketing"],
|
|
1189
|
+
},
|
|
1190
|
+
];
|
|
1191
|
+
|
|
1192
|
+
const SOCIAL_PROVIDER_TOOL_NAMES = {
|
|
1193
|
+
x: [
|
|
1194
|
+
"x_accounts_list",
|
|
1195
|
+
"x_search_live",
|
|
1196
|
+
"x_mentions_live",
|
|
1197
|
+
"x_thread_fetch",
|
|
1198
|
+
"x_posts_sync",
|
|
1199
|
+
"x_post_reply",
|
|
1200
|
+
],
|
|
1201
|
+
reddit: [
|
|
1202
|
+
"reddit_accounts_list",
|
|
1203
|
+
"reddit_search_live",
|
|
1204
|
+
"reddit_mentions_live",
|
|
1205
|
+
"reddit_thread_fetch",
|
|
1206
|
+
"reddit_posts_sync",
|
|
1207
|
+
"reddit_post_reply",
|
|
1208
|
+
],
|
|
1209
|
+
} as const;
|
|
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",
|
|
1109
1234
|
tools: [{ kind: "mcp", id: "opengeni" }],
|
|
1110
1235
|
runtime: {
|
|
1111
1236
|
available: true,
|
|
1112
1237
|
mcpServerId: "opengeni",
|
|
1113
|
-
notes: "
|
|
1238
|
+
notes: "Fiken access is provided through OpenGeni's first-party fiken tools.",
|
|
1114
1239
|
},
|
|
1115
|
-
enabled:
|
|
1116
|
-
enabledReason:
|
|
1117
|
-
?
|
|
1118
|
-
?
|
|
1119
|
-
:
|
|
1240
|
+
enabled: fikenEnabled,
|
|
1241
|
+
enabledReason: fikenEnabled
|
|
1242
|
+
? fikenConnection.status === "active"
|
|
1243
|
+
? "workspace Fiken connection active"
|
|
1244
|
+
: "workspace Fiken connection needs reconnection"
|
|
1120
1245
|
: null,
|
|
1121
1246
|
metadata: {
|
|
1122
|
-
connectorMode: "
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
"social_posts_recent",
|
|
1128
|
-
"social_daily_analysis_context",
|
|
1129
|
-
"social_search_live",
|
|
1130
|
-
"social_mentions_live",
|
|
1131
|
-
"social_thread_fetch",
|
|
1132
|
-
"social_posts_sync",
|
|
1133
|
-
"social_post_reply",
|
|
1134
|
-
],
|
|
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_")),
|
|
1135
1252
|
},
|
|
1136
1253
|
});
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function providerIntegrationCatalogItems(
|
|
1257
|
+
socialConnections: SocialConnection[],
|
|
1258
|
+
): CapabilityCatalogItem[] {
|
|
1259
|
+
return SOCIAL_PROVIDER_INTEGRATIONS.map((definition) => {
|
|
1260
|
+
const counts = socialConnectionCounts(socialConnections, definition.provider);
|
|
1261
|
+
const enabled = counts.connected + counts.needsReauth > 0;
|
|
1262
|
+
return CapabilityCatalogItem.parse({
|
|
1263
|
+
id: `api:${definition.provider}`,
|
|
1264
|
+
kind: "api",
|
|
1265
|
+
source: "built_in",
|
|
1266
|
+
name: definition.name,
|
|
1267
|
+
description: definition.description,
|
|
1268
|
+
category: "social-media",
|
|
1269
|
+
tags: definition.tags,
|
|
1270
|
+
homepageUrl: definition.homepageUrl,
|
|
1271
|
+
authModel: "oauth2_authorization_code_pkce",
|
|
1272
|
+
providerDomain: definition.providerDomain,
|
|
1273
|
+
surfaceType: "provider_integration",
|
|
1274
|
+
authKind: "oauth2",
|
|
1275
|
+
tools: [{ kind: "mcp", id: "opengeni" }],
|
|
1276
|
+
runtime: {
|
|
1277
|
+
available: true,
|
|
1278
|
+
mcpServerId: "opengeni",
|
|
1279
|
+
notes:
|
|
1280
|
+
"OpenGeni's social provider adapter routes every call through an exact visible account Connection.",
|
|
1281
|
+
},
|
|
1282
|
+
enabled,
|
|
1283
|
+
enabledReason: socialConnectionSummary(counts),
|
|
1284
|
+
provenance: "OpenGeni provider adapter",
|
|
1285
|
+
metadata: {
|
|
1286
|
+
providerAdapter: "social",
|
|
1287
|
+
provider: definition.provider,
|
|
1288
|
+
connectionCounts: counts,
|
|
1289
|
+
runtimeNamespace: "social",
|
|
1290
|
+
firstPartyMcpTools: SOCIAL_PROVIDER_TOOL_NAMES[definition.provider],
|
|
1291
|
+
},
|
|
1292
|
+
});
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Native product connection recommendations are intentionally separate from
|
|
1298
|
+
* the installable catalog returned to the Capabilities UI. Agents may still
|
|
1299
|
+
* recommend the owning product flow without manufacturing an enabled catalog
|
|
1300
|
+
* row for GitHub resources, Documents, schedules, or other platform features.
|
|
1301
|
+
*/
|
|
1302
|
+
export function nativeConnectionCapabilityRecommendations(): CapabilityCatalogItem[] {
|
|
1303
|
+
return [
|
|
1171
1304
|
CapabilityCatalogItem.parse({
|
|
1172
|
-
id:
|
|
1173
|
-
name: item.name,
|
|
1174
|
-
description: item.description,
|
|
1175
|
-
category: item.category,
|
|
1176
|
-
tags: item.tags,
|
|
1305
|
+
id: "api:github-app",
|
|
1177
1306
|
kind: "api",
|
|
1178
1307
|
source: "built_in",
|
|
1308
|
+
name: "GitHub App",
|
|
1309
|
+
description: "Connect repositories through OpenGeni's GitHub resource picker.",
|
|
1310
|
+
category: "source-control",
|
|
1311
|
+
tags: ["github", "repositories", "source-control"],
|
|
1312
|
+
homepageUrl: "https://github.com",
|
|
1313
|
+
providerDomain: "github.com",
|
|
1314
|
+
authModel: "github_app_owner_consent",
|
|
1315
|
+
authKind: "oauth2",
|
|
1316
|
+
surfaceType: "first_party_github",
|
|
1317
|
+
tools: [{ kind: "mcp", id: "opengeni" }],
|
|
1179
1318
|
runtime: {
|
|
1180
1319
|
available: true,
|
|
1181
|
-
notes:
|
|
1320
|
+
notes:
|
|
1321
|
+
"GitHub credentials stay host-owned; a workspace owner must approve repository access.",
|
|
1322
|
+
},
|
|
1323
|
+
lifecycle: {
|
|
1324
|
+
status: "available",
|
|
1325
|
+
readiness: "setup_required",
|
|
1326
|
+
detail: "Connect GitHub from the repository resource flow.",
|
|
1327
|
+
managedBy: "platform",
|
|
1182
1328
|
},
|
|
1329
|
+
actions: ["connect", "inspect"],
|
|
1183
1330
|
metadata: {
|
|
1184
|
-
endpointPath:
|
|
1331
|
+
endpointPath: "/v1/workspaces/{workspaceId}/github/app",
|
|
1332
|
+
firstPartyMcpTools: ["github_connect_link", "github_repositories_list"],
|
|
1333
|
+
recommendationOnly: true,
|
|
1185
1334
|
},
|
|
1186
1335
|
}),
|
|
1187
|
-
|
|
1188
|
-
return [x, ...platformApis];
|
|
1336
|
+
];
|
|
1189
1337
|
}
|
|
1190
1338
|
|
|
1191
|
-
function
|
|
1339
|
+
function socialConnectionCounts(
|
|
1192
1340
|
connections: SocialConnection[],
|
|
1193
1341
|
provider: "x" | "reddit",
|
|
1194
|
-
):
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
statusRank(left.status) - statusRank(right.status) ||
|
|
1203
|
-
right.updatedAt.localeCompare(left.updatedAt) ||
|
|
1204
|
-
left.id.localeCompare(right.id),
|
|
1205
|
-
)[0] ?? null
|
|
1206
|
-
);
|
|
1342
|
+
): { connected: number; needsReauth: number; disabled: number; total: number } {
|
|
1343
|
+
const matching = connections.filter((connection) => connection.provider === provider);
|
|
1344
|
+
return {
|
|
1345
|
+
connected: matching.filter((connection) => connection.status === "connected").length,
|
|
1346
|
+
needsReauth: matching.filter((connection) => connection.status === "needs_reauth").length,
|
|
1347
|
+
disabled: matching.filter((connection) => connection.status === "disabled").length,
|
|
1348
|
+
total: matching.length,
|
|
1349
|
+
};
|
|
1207
1350
|
}
|
|
1208
1351
|
|
|
1209
|
-
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
.map(async (entry) => {
|
|
1220
|
-
const skill = await readSkillMetadata(
|
|
1221
|
-
new URL(`${entry.name}/SKILL.md`, skillsDir),
|
|
1222
|
-
entry.name,
|
|
1223
|
-
);
|
|
1224
|
-
return CapabilityCatalogItem.parse({
|
|
1225
|
-
id: `skill:${entry.name}`,
|
|
1226
|
-
kind: "skill",
|
|
1227
|
-
source: "built_in",
|
|
1228
|
-
name: skill.name,
|
|
1229
|
-
description: skill.description,
|
|
1230
|
-
category: skill.category,
|
|
1231
|
-
tags: ["skill", skill.category],
|
|
1232
|
-
runtime: {
|
|
1233
|
-
available: true,
|
|
1234
|
-
notes: "Bundled into the sandbox skill library.",
|
|
1235
|
-
},
|
|
1236
|
-
metadata: {
|
|
1237
|
-
path: `packages/runtime/src/bundled_hashicorp_terraform_skills/${entry.name}/SKILL.md`,
|
|
1238
|
-
},
|
|
1239
|
-
});
|
|
1240
|
-
}),
|
|
1352
|
+
function socialConnectionSummary(counts: ReturnType<typeof socialConnectionCounts>): string | null {
|
|
1353
|
+
const parts: string[] = [];
|
|
1354
|
+
if (counts.connected > 0) {
|
|
1355
|
+
parts.push(`${counts.connected} connected account${counts.connected === 1 ? "" : "s"}`);
|
|
1356
|
+
}
|
|
1357
|
+
if (counts.needsReauth > 0) {
|
|
1358
|
+
parts.push(
|
|
1359
|
+
`${counts.needsReauth} account${counts.needsReauth === 1 ? "" : "s"} need${
|
|
1360
|
+
counts.needsReauth === 1 ? "s" : ""
|
|
1361
|
+
} reconnection`,
|
|
1241
1362
|
);
|
|
1242
|
-
return skills;
|
|
1243
|
-
} catch {
|
|
1244
|
-
return [];
|
|
1245
1363
|
}
|
|
1364
|
+
if (counts.disabled > 0) {
|
|
1365
|
+
parts.push(`${counts.disabled} disconnected account${counts.disabled === 1 ? "" : "s"}`);
|
|
1366
|
+
}
|
|
1367
|
+
return parts.length > 0 ? parts.join("; ") : null;
|
|
1246
1368
|
}
|
|
1247
1369
|
|
|
1248
1370
|
/**
|
|
@@ -1287,35 +1409,86 @@ function curatedSkillCatalogItem(entry: SkillLibraryEntry): CapabilityCatalogIte
|
|
|
1287
1409
|
});
|
|
1288
1410
|
}
|
|
1289
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
|
+
|
|
1290
1459
|
function stringMetadata(value: unknown): string | null {
|
|
1291
1460
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1292
1461
|
}
|
|
1293
1462
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
?
|
|
1317
|
-
:
|
|
1318
|
-
|
|
1463
|
+
function isSocialProviderIntegration(item: CapabilityCatalogItem): boolean {
|
|
1464
|
+
return (
|
|
1465
|
+
item.surfaceType === "first_party_social" ||
|
|
1466
|
+
(item.surfaceType === "provider_integration" && item.metadata.providerAdapter === "social")
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function socialProviderConnectionCounts(item: CapabilityCatalogItem): {
|
|
1471
|
+
connected: number;
|
|
1472
|
+
needsReauth: number;
|
|
1473
|
+
} {
|
|
1474
|
+
const value = item.metadata.connectionCounts;
|
|
1475
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1476
|
+
return {
|
|
1477
|
+
connected: item.enabled && !item.enabledReason?.includes("reconnection") ? 1 : 0,
|
|
1478
|
+
needsReauth: item.enabledReason?.includes("reconnection") ? 1 : 0,
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
const record = value as Record<string, unknown>;
|
|
1482
|
+
return {
|
|
1483
|
+
connected:
|
|
1484
|
+
typeof record.connected === "number" && Number.isInteger(record.connected)
|
|
1485
|
+
? Math.max(0, record.connected)
|
|
1486
|
+
: 0,
|
|
1487
|
+
needsReauth:
|
|
1488
|
+
typeof record.needsReauth === "number" && Number.isInteger(record.needsReauth)
|
|
1489
|
+
? Math.max(0, record.needsReauth)
|
|
1490
|
+
: 0,
|
|
1491
|
+
};
|
|
1319
1492
|
}
|
|
1320
1493
|
|
|
1321
1494
|
export function applyCapabilityEnablement(
|
|
@@ -1323,21 +1496,27 @@ export function applyCapabilityEnablement(
|
|
|
1323
1496
|
installation: CapabilityInstallation | undefined,
|
|
1324
1497
|
activePackIds: Set<string>,
|
|
1325
1498
|
): CapabilityCatalogItem {
|
|
1499
|
+
if (item.kind === "skill" || item.kind === "api" || item.kind === "plugin") {
|
|
1500
|
+
return { ...item, enabled: false, enabledReason: null, connectionRef: null };
|
|
1501
|
+
}
|
|
1326
1502
|
if (item.kind === "pack") {
|
|
1327
|
-
|
|
1328
|
-
// pack is built in or registered from a workspace manifest.
|
|
1329
|
-
const enabled =
|
|
1330
|
-
activePackIds.has(packIdFromCapabilityId(item.id)) || installation?.status === "active";
|
|
1503
|
+
const enabled = activePackIds.has(packIdFromCapabilityId(item.id));
|
|
1331
1504
|
return {
|
|
1332
1505
|
...item,
|
|
1333
1506
|
enabled,
|
|
1334
1507
|
enabledReason: enabled ? "enabled" : null,
|
|
1335
1508
|
};
|
|
1336
1509
|
}
|
|
1337
|
-
if (item
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1510
|
+
if (isSocialProviderIntegration(item)) {
|
|
1511
|
+
// Provider-integration state is derived from every authoritative visible
|
|
1512
|
+
// social Connection while the catalog is built. The catalog summary never
|
|
1513
|
+
// publishes a personal connection UUID or collapses many accounts to one.
|
|
1514
|
+
return { ...item, connectionRef: null };
|
|
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.
|
|
1341
1520
|
return { ...item, connectionRef: null };
|
|
1342
1521
|
}
|
|
1343
1522
|
if (item.surfaceType === "codex_apps") {
|
|
@@ -1346,23 +1525,20 @@ export function applyCapabilityEnablement(
|
|
|
1346
1525
|
// generic source-based "built in" enablement rule.
|
|
1347
1526
|
return { ...item, connectionRef: null };
|
|
1348
1527
|
}
|
|
1349
|
-
if (item.source === "
|
|
1528
|
+
if (item.source === "configured") {
|
|
1350
1529
|
return {
|
|
1351
1530
|
...item,
|
|
1352
1531
|
enabled: true,
|
|
1353
|
-
enabledReason:
|
|
1354
|
-
};
|
|
1355
|
-
}
|
|
1356
|
-
if (item.source === "library") {
|
|
1357
|
-
const enabled =
|
|
1358
|
-
installation?.status === "active" && skillLibraryInstallationRuntimeReady(item, installation);
|
|
1359
|
-
return {
|
|
1360
|
-
...item,
|
|
1361
|
-
enabled,
|
|
1362
|
-
enabledReason: enabled ? "explicitly selected" : null,
|
|
1532
|
+
enabledReason: "managed by deployment",
|
|
1363
1533
|
connectionRef: null,
|
|
1364
1534
|
};
|
|
1365
1535
|
}
|
|
1536
|
+
if (item.source === "built_in") {
|
|
1537
|
+
// "Built in" describes provenance, not lifecycle. Native product
|
|
1538
|
+
// surfaces and first-party connectors must project their authoritative
|
|
1539
|
+
// state explicitly above rather than becoming enabled by taxonomy.
|
|
1540
|
+
return { ...item, connectionRef: null };
|
|
1541
|
+
}
|
|
1366
1542
|
const activeInstallation = installation?.status === "active";
|
|
1367
1543
|
const enabled = !!activeInstallation && capabilityInstallationRuntimeReady(item, installation);
|
|
1368
1544
|
return {
|
|
@@ -1373,40 +1549,116 @@ export function applyCapabilityEnablement(
|
|
|
1373
1549
|
};
|
|
1374
1550
|
}
|
|
1375
1551
|
|
|
1376
|
-
function
|
|
1377
|
-
item
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1552
|
+
function applyCapabilityLifecycle(item: CapabilityCatalogItem): CapabilityCatalogItem {
|
|
1553
|
+
if (item.source === "configured") {
|
|
1554
|
+
return {
|
|
1555
|
+
...item,
|
|
1556
|
+
lifecycle: {
|
|
1557
|
+
status: "managed",
|
|
1558
|
+
readiness: item.runtime.available ? "ready" : "unavailable",
|
|
1559
|
+
detail: item.runtime.notes,
|
|
1560
|
+
managedBy: "deployment",
|
|
1561
|
+
},
|
|
1562
|
+
actions: ["inspect"],
|
|
1563
|
+
};
|
|
1382
1564
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1565
|
+
|
|
1566
|
+
if (!item.runtime.available) {
|
|
1567
|
+
return {
|
|
1568
|
+
...item,
|
|
1569
|
+
lifecycle: {
|
|
1570
|
+
status: "unavailable",
|
|
1571
|
+
readiness: "unavailable",
|
|
1572
|
+
detail: item.runtime.notes,
|
|
1573
|
+
managedBy: item.source === "built_in" ? "platform" : null,
|
|
1574
|
+
},
|
|
1575
|
+
actions: ["inspect"],
|
|
1576
|
+
};
|
|
1390
1577
|
}
|
|
1391
|
-
|
|
1392
|
-
if (
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1578
|
+
|
|
1579
|
+
if (isSocialProviderIntegration(item)) {
|
|
1580
|
+
const counts = socialProviderConnectionCounts(item);
|
|
1581
|
+
const needsAttention = counts.needsReauth > 0;
|
|
1582
|
+
const connected = counts.connected > 0;
|
|
1583
|
+
return {
|
|
1584
|
+
...item,
|
|
1585
|
+
lifecycle: {
|
|
1586
|
+
status: needsAttention ? "needs_attention" : connected ? "connected" : "available",
|
|
1587
|
+
readiness: needsAttention ? "attention" : connected ? "ready" : "setup_required",
|
|
1588
|
+
detail: item.enabledReason,
|
|
1589
|
+
managedBy: null,
|
|
1590
|
+
},
|
|
1591
|
+
actions: needsAttention
|
|
1592
|
+
? ["repair", "connect", "disconnect", "inspect"]
|
|
1593
|
+
: connected
|
|
1594
|
+
? ["connect", "configure", "disconnect", "inspect"]
|
|
1595
|
+
: ["connect", "inspect"],
|
|
1596
|
+
};
|
|
1401
1597
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1598
|
+
|
|
1599
|
+
if (item.surfaceType === "codex_apps") {
|
|
1600
|
+
return {
|
|
1601
|
+
...item,
|
|
1602
|
+
lifecycle: {
|
|
1603
|
+
status: item.enabled ? "connected" : "available",
|
|
1604
|
+
readiness: item.enabled ? "ready" : "setup_required",
|
|
1605
|
+
detail: item.enabledReason,
|
|
1606
|
+
managedBy: "platform",
|
|
1607
|
+
},
|
|
1608
|
+
actions: item.enabled ? ["configure", "disconnect", "inspect"] : ["connect", "inspect"],
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
const installed = item.enabled;
|
|
1613
|
+
const actions: CapabilityAction[] = installed
|
|
1614
|
+
? item.kind === "pack" || item.kind === "skill" || item.kind === "plugin"
|
|
1615
|
+
? ["configure", "update", "uninstall", "inspect"]
|
|
1616
|
+
: ["configure", "disconnect", "inspect"]
|
|
1617
|
+
: item.kind === "mcp" || item.kind === "api"
|
|
1618
|
+
? ["connect", "inspect"]
|
|
1619
|
+
: ["install", "inspect"];
|
|
1620
|
+
return {
|
|
1621
|
+
...item,
|
|
1622
|
+
lifecycle: {
|
|
1623
|
+
status: installed
|
|
1624
|
+
? item.kind === "mcp" || item.kind === "api"
|
|
1625
|
+
? "ready"
|
|
1626
|
+
: "installed"
|
|
1627
|
+
: "available",
|
|
1628
|
+
readiness: installed ? "ready" : "setup_required",
|
|
1629
|
+
detail: item.enabledReason,
|
|
1630
|
+
managedBy: item.source === "built_in" ? "platform" : "workspace",
|
|
1631
|
+
},
|
|
1632
|
+
actions,
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function applyInstalledSkillEnablement(
|
|
1637
|
+
item: CapabilityCatalogItem,
|
|
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
|
+
};
|
|
1410
1662
|
}
|
|
1411
1663
|
|
|
1412
1664
|
/**
|
|
@@ -1460,17 +1712,86 @@ function compareCatalogItems(a: CapabilityCatalogItem, b: CapabilityCatalogItem)
|
|
|
1460
1712
|
return `${a.kind}:${a.category}:${a.name}`.localeCompare(`${b.kind}:${b.category}:${b.name}`);
|
|
1461
1713
|
}
|
|
1462
1714
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1715
|
+
export type CapabilityCatalogSearchMatch = {
|
|
1716
|
+
item: CapabilityCatalogItem;
|
|
1717
|
+
score: number;
|
|
1718
|
+
matchedOn: Array<"name" | "provider" | "tag" | "category" | "description" | "id">;
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* Deterministically rank the already-merged workspace catalog for an agent.
|
|
1723
|
+
* The caller still owns live authorization checks (GitHub binding, OAuth row,
|
|
1724
|
+
* and so on); this function searches metadata only and never probes a provider
|
|
1725
|
+
* or exposes credential/setup prose to the model.
|
|
1726
|
+
*/
|
|
1727
|
+
export function searchCapabilityCatalogItems(
|
|
1728
|
+
items: readonly CapabilityCatalogItem[],
|
|
1729
|
+
query: string,
|
|
1730
|
+
limit = 8,
|
|
1731
|
+
): CapabilityCatalogSearchMatch[] {
|
|
1732
|
+
const phrase = normalizeCapabilitySearchText(query);
|
|
1733
|
+
const tokens = [...new Set(phrase.split(" ").filter(Boolean))].slice(0, 24);
|
|
1734
|
+
if (tokens.length === 0) return [];
|
|
1735
|
+
const boundedLimit = Math.min(20, Math.max(1, Math.trunc(limit)));
|
|
1736
|
+
const weightedFields = (item: CapabilityCatalogItem) =>
|
|
1737
|
+
[
|
|
1738
|
+
["name", item.name, 100],
|
|
1739
|
+
["provider", item.providerDomain ?? "", 80],
|
|
1740
|
+
["tag", item.tags.join(" "), 50],
|
|
1741
|
+
["category", item.category, 30],
|
|
1742
|
+
["id", item.id, 25],
|
|
1743
|
+
["description", item.description ?? "", 15],
|
|
1744
|
+
] as const;
|
|
1745
|
+
|
|
1746
|
+
return items
|
|
1747
|
+
.filter((item) => capabilityCatalogItemIsTrustedForExposure(item))
|
|
1748
|
+
.flatMap((item): CapabilityCatalogSearchMatch[] => {
|
|
1749
|
+
let score = 0;
|
|
1750
|
+
const matchedOn = new Set<CapabilityCatalogSearchMatch["matchedOn"][number]>();
|
|
1751
|
+
for (const [field, raw, weight] of weightedFields(item)) {
|
|
1752
|
+
const value = normalizeCapabilitySearchText(raw);
|
|
1753
|
+
if (!value) continue;
|
|
1754
|
+
const words = value.split(" ");
|
|
1755
|
+
if (value.includes(phrase)) {
|
|
1756
|
+
score += Math.round(weight * 1.5);
|
|
1757
|
+
matchedOn.add(field);
|
|
1758
|
+
}
|
|
1759
|
+
for (const token of tokens) {
|
|
1760
|
+
if (words.includes(token)) {
|
|
1761
|
+
score += weight;
|
|
1762
|
+
matchedOn.add(field);
|
|
1763
|
+
} else if (words.some((word) => word.startsWith(token) || token.startsWith(word))) {
|
|
1764
|
+
score += Math.round(weight * 0.7);
|
|
1765
|
+
matchedOn.add(field);
|
|
1766
|
+
} else if (value.includes(token)) {
|
|
1767
|
+
score += Math.round(weight * 0.4);
|
|
1768
|
+
matchedOn.add(field);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
if (score === 0) return [];
|
|
1773
|
+
if (item.enabled) score += 12;
|
|
1774
|
+
if (item.tier === "verified") score += 8;
|
|
1775
|
+
if (item.source === "built_in") score += 6;
|
|
1776
|
+
return [{ item, score, matchedOn: [...matchedOn] }];
|
|
1777
|
+
})
|
|
1778
|
+
.sort(
|
|
1779
|
+
(left, right) =>
|
|
1780
|
+
right.score - left.score ||
|
|
1781
|
+
Number(right.item.enabled) - Number(left.item.enabled) ||
|
|
1782
|
+
left.item.name.localeCompare(right.item.name) ||
|
|
1783
|
+
left.item.id.localeCompare(right.item.id),
|
|
1784
|
+
)
|
|
1785
|
+
.slice(0, boundedLimit);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function normalizeCapabilitySearchText(value: string): string {
|
|
1789
|
+
return value
|
|
1790
|
+
.normalize("NFKD")
|
|
1791
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
1792
|
+
.toLowerCase()
|
|
1793
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
1794
|
+
.trim();
|
|
1474
1795
|
}
|
|
1475
1796
|
|
|
1476
1797
|
function generatedCapabilityId(payload: CreateCapabilityCatalogItemRequest): string {
|