@opengeni/core 3.0.1 → 3.1.2
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/README.md +4 -4
- package/dist/access/index.d.ts +131 -7
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +1 -1
- package/dist/application/connect-operation.d.ts +6 -0
- package/dist/application/external-workspace-members.d.ts +13 -1
- package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
- package/dist/application/modal-native-original-configuration.d.ts +79 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +29 -2
- package/dist/application/organization-privacy.d.ts +57 -0
- package/dist/application/prepared-mcp-connection.d.ts +31 -0
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +3 -0
- package/dist/application/session-tenancy.d.ts +13 -4
- package/dist/billing/agent-run-admission.d.ts +21 -0
- package/dist/billing/limits.d.ts +27 -1
- package/dist/billing/model-call-admission.d.ts +34 -0
- package/dist/billing/model-usage-settlement.d.ts +112 -0
- package/dist/canonical-human-identities.js +1 -1
- package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
- package/dist/chunk-7T2SZ7BI.js.map +1 -0
- package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
- package/dist/chunk-OMJFIIWK.js.map +1 -0
- package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
- package/dist/chunk-QI2M2B2K.js.map +1 -0
- package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
- package/dist/chunk-TTWTXQB2.js.map +1 -0
- package/dist/codex-model-availability.d.ts +55 -0
- package/dist/default-session-model.d.ts +192 -0
- package/dist/dependencies.d.ts +41 -7
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +8 -1
- package/dist/domain/capabilities.d.ts +112 -8
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +62 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +3 -1
- package/dist/domain/editable-artifacts/types.d.ts +1 -1
- package/dist/domain/encryption-key.d.ts +12 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +21 -3
- package/dist/domain/knowledge-files.d.ts +1 -1
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -76
- package/dist/domain/mcp-account-bindings.d.ts +17 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/personal-connection-delegations.d.ts +73 -12
- package/dist/domain/pr-review.d.ts +121 -6
- package/dist/domain/realtime-voice-billing.d.ts +103 -0
- package/dist/domain/resources.d.ts +17 -1
- package/dist/domain/scheduled-task-access.d.ts +265 -0
- package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
- package/dist/domain/scheduled-task-audience.d.ts +70 -0
- package/dist/domain/scheduled-tasks.d.ts +61 -18
- package/dist/domain/session-connection-accounts.d.ts +53 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +102 -4
- package/dist/domain/sessions.d.ts +179 -15
- package/dist/domain/site-audience.d.ts +52 -0
- package/dist/domain/skills.d.ts +29 -1
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
- package/dist/domain/vaults/index.d.ts +7 -0
- package/dist/domain/vaults/mapping.d.ts +25 -0
- package/dist/domain/vaults/provider-registry.d.ts +72 -0
- package/dist/domain/vaults/provider-sync.d.ts +176 -0
- package/dist/domain/vaults/provider-writes.d.ts +57 -0
- package/dist/domain/vaults/secret-resolver.d.ts +34 -0
- package/dist/domain/vaults/types.d.ts +24 -0
- package/dist/domain/vaults/vault-service.d.ts +35 -0
- package/dist/domain/voice-input-billing.d.ts +77 -0
- package/dist/domain/web-search-billing.d.ts +51 -0
- package/dist/domain/workspace-credits.d.ts +7 -0
- package/dist/editable-artifact-live/application.d.ts +2 -1
- package/dist/editable-artifact-live/server.d.ts +5 -1
- package/dist/editable-artifact-live/ticket.d.ts +2 -1
- package/dist/editable-artifact-live/types.d.ts +9 -0
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -2
- package/dist/goal-admission.d.ts +627 -0
- package/dist/host-session-usage-limit.d.ts +15 -0
- package/dist/index.d.ts +43 -8
- package/dist/index.js +22682 -11743
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +110 -7
- package/dist/model-call.d.ts +58 -0
- package/dist/model-catalog.d.ts +29 -3
- package/dist/model-provider-credentials.d.ts +43 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +1 -13
- package/dist/sandbox/fleet.d.ts +58 -4
- package/dist/sandbox/routing.d.ts +42 -2
- package/dist/sandbox/runtime-settings.d.ts +27 -17
- package/dist/session-authorization.d.ts +10 -0
- package/dist/session-usage-limit-metrics.d.ts +60 -0
- package/dist/session-usage-limits.d.ts +89 -0
- package/dist/site-agent-clamp.d.ts +71 -0
- package/dist/site-session-origin.d.ts +39 -2
- package/dist/transcription.d.ts +120 -3
- package/dist/turn-surface.d.ts +20 -0
- package/dist/user-presence.d.ts +32 -0
- package/dist/workflow-wake-contract.d.ts +13 -1
- package/package.json +15 -22
- package/src/access/index.ts +961 -38
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +9 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +3 -2
- package/src/application/connect-authority.ts +3 -3
- package/src/application/connect-operation.ts +37 -3
- package/src/application/external-continuation.ts +20 -3
- package/src/application/external-link-work-admission.ts +3 -0
- package/src/application/external-workspace-members.ts +51 -3
- package/src/application/modal-native-live-declaration-join.ts +196 -0
- package/src/application/modal-native-original-configuration.ts +261 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +172 -55
- package/src/application/organization-privacy.ts +161 -0
- package/src/application/prepared-mcp-connection.ts +133 -0
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +73 -1
- package/src/application/session-mcp-credential-rotation.ts +61 -18
- package/src/application/session-tenancy.ts +262 -44
- package/src/application/user-resource-grants.ts +1 -29
- package/src/billing/agent-run-admission.ts +117 -0
- package/src/billing/limits.ts +138 -14
- package/src/billing/model-call-admission.ts +118 -0
- package/src/billing/model-usage-settlement.ts +472 -0
- package/src/codex-model-availability.ts +264 -0
- package/src/default-session-model.ts +692 -0
- package/src/dependencies.ts +48 -6
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +76 -2
- package/src/domain/capabilities.ts +830 -181
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +572 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +44 -6
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +20 -0
- package/src/domain/editable-artifacts/types.ts +1 -1
- package/src/domain/encryption-key.ts +26 -0
- package/src/domain/file-owner.ts +16 -1
- package/src/domain/github-action-policies.ts +1 -1
- package/src/domain/github-skill-source.ts +3 -1
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +167 -43
- package/src/domain/knowledge-files.ts +10 -3
- package/src/domain/knowledge-messages.ts +1 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +617 -5
- package/src/domain/knowledge-search.ts +240 -23
- package/src/domain/mcp-account-bindings.ts +169 -0
- package/src/domain/mcp-account-routes.ts +152 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/personal-connection-delegations.ts +399 -257
- package/src/domain/pr-review.ts +24 -8
- package/src/domain/realtime-voice-billing.ts +425 -0
- package/src/domain/resources.ts +50 -4
- package/src/domain/scheduled-task-access.ts +1089 -0
- package/src/domain/scheduled-task-account-audit.ts +90 -0
- package/src/domain/scheduled-task-audience.ts +249 -0
- package/src/domain/scheduled-tasks.ts +1313 -374
- package/src/domain/session-connection-accounts.ts +239 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +60 -0
- package/src/domain/session-tool-policy.ts +489 -14
- package/src/domain/sessions.ts +2254 -717
- package/src/domain/site-audience.ts +130 -0
- package/src/domain/skills.ts +9 -0
- package/src/domain/slack-bot.ts +141 -4
- package/src/domain/slack-publication-secret-safety.ts +1 -1
- package/src/domain/vaults/index.ts +19 -0
- package/src/domain/vaults/mapping.ts +190 -0
- package/src/domain/vaults/provider-registry.ts +201 -0
- package/src/domain/vaults/provider-sync.ts +531 -0
- package/src/domain/vaults/provider-writes.ts +288 -0
- package/src/domain/vaults/secret-resolver.ts +464 -0
- package/src/domain/vaults/types.ts +26 -0
- package/src/domain/vaults/vault-service.ts +1316 -0
- package/src/domain/voice-input-billing.ts +330 -0
- package/src/domain/web-search-billing.ts +169 -0
- package/src/domain/workspace-credits.ts +13 -0
- package/src/editable-artifact-live/application.ts +8 -1
- package/src/editable-artifact-live/server.ts +154 -6
- package/src/editable-artifact-live/ticket.ts +86 -1
- package/src/editable-artifact-live/types.ts +10 -0
- package/src/goal-admission.ts +220 -0
- package/src/host-session-usage-limit.ts +25 -0
- package/src/index.ts +75 -11
- package/src/managed-session.ts +137 -6
- package/src/model-call.ts +79 -0
- package/src/model-catalog.ts +345 -91
- package/src/model-provider-credentials.ts +394 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +23 -111
- package/src/sandbox/fleet.ts +253 -28
- package/src/sandbox/routing.ts +204 -43
- package/src/sandbox/runtime-settings.ts +159 -85
- package/src/session-authorization.ts +172 -23
- package/src/session-usage-limit-metrics.ts +95 -0
- package/src/session-usage-limits.ts +415 -0
- package/src/site-agent-clamp.ts +226 -0
- package/src/site-session-origin.ts +84 -5
- package/src/transcription.ts +123 -4
- package/src/turn-surface.ts +47 -0
- package/src/user-presence.ts +134 -0
- package/src/workflow-wake-contract.ts +14 -1
- package/dist/application/host-mcp-owner.d.ts +0 -6
- package/dist/application/host-mcp-resolvers.d.ts +0 -13
- package/dist/chunk-5WZYFL45.js +0 -266
- package/dist/chunk-5WZYFL45.js.map +0 -1
- package/dist/chunk-H7X52RI2.js.map +0 -1
- package/dist/chunk-OF65T3PM.js.map +0 -1
- package/dist/chunk-VCZFFEPB.js.map +0 -1
- package/dist/chunk-ZYCNLYGH.js.map +0 -1
- package/dist/domain/connection-authority.d.ts +0 -97
- package/dist/domain/connection-authority.js +0 -169
- package/dist/domain/connection-authority.js.map +0 -1
- package/dist/domain/environments.d.ts +0 -33
- package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
- package/dist/domain/host-mcp-task-admission.d.ts +0 -18
- package/dist/domain/packs.d.ts +0 -59
- package/dist/domain/product-integration-pack.d.ts +0 -44
- package/dist/domain/product-integration-skill.gen.d.ts +0 -5
- package/dist/remote-mcp-credentials.d.ts +0 -23
- package/dist/remote-mcp-credentials.js +0 -11
- package/dist/remote-mcp-credentials.js.map +0 -1
- package/src/application/host-mcp-owner.ts +0 -41
- package/src/application/host-mcp-resolvers.ts +0 -84
- package/src/domain/connection-authority.ts +0 -329
- package/src/domain/environments.ts +0 -120
- package/src/domain/host-mcp-authority-source-admission.ts +0 -25
- package/src/domain/host-mcp-task-admission.ts +0 -121
- package/src/domain/packs.ts +0 -699
- package/src/domain/product-integration-pack.ts +0 -42
- package/src/domain/product-integration-skill.gen.ts +0 -52
- package/src/remote-mcp-credentials.ts +0 -361
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
3
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
4
|
-
import {
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { withLockedCapabilityInstallation } from "@opengeni/db/capability-reconciliation";
|
|
6
|
+
import {
|
|
7
|
+
environmentsEncryptionKeyBytes,
|
|
8
|
+
findIntegrationsOauthClient,
|
|
9
|
+
parseIntegrationsOauthClientsJson,
|
|
10
|
+
type Settings,
|
|
11
|
+
} from "@opengeni/config";
|
|
12
|
+
import { pinnedFetch } from "@opengeni/network";
|
|
5
13
|
import {
|
|
6
14
|
CapabilityCatalogItem,
|
|
7
15
|
capabilityCatalogItemIsTrustedForExposure,
|
|
8
16
|
FIKEN_PROVIDER_DOMAIN,
|
|
9
17
|
FIRST_PARTY_MCP_TOOL_NAMES,
|
|
18
|
+
OPENGENI_BROWSER_EXTENSION_URL,
|
|
10
19
|
type AccessGrant,
|
|
11
20
|
type CapabilityAction,
|
|
12
21
|
type CapabilityCatalogResponse,
|
|
@@ -16,7 +25,7 @@ import {
|
|
|
16
25
|
type EnableCapabilityRequest,
|
|
17
26
|
type McpServerConnectionRef,
|
|
18
27
|
type McpPersonalConnectionDelegation,
|
|
19
|
-
|
|
28
|
+
assertOrganizationIntegrationAllowed,
|
|
20
29
|
type SocialConnection,
|
|
21
30
|
} from "@opengeni/contracts";
|
|
22
31
|
import {
|
|
@@ -34,8 +43,12 @@ import {
|
|
|
34
43
|
getCapabilityCatalogItem,
|
|
35
44
|
getCapabilityInstallation,
|
|
36
45
|
getConnectionMetadata,
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
readCodexCutoverDisposition,
|
|
47
|
+
resolveSubscriptionCoreCodexAppsDesignation,
|
|
48
|
+
rlsContextForWorkspace,
|
|
49
|
+
subscriptionCoreCodexAppsRequestAuth,
|
|
50
|
+
type SubscriptionCoreCodexAppsRequestAuth,
|
|
51
|
+
type CodexCutoverDisposition,
|
|
39
52
|
getStoredCapabilityHeaderCiphertext,
|
|
40
53
|
listCapabilityCatalogItems,
|
|
41
54
|
listCapabilityInstallations,
|
|
@@ -43,7 +56,6 @@ import {
|
|
|
43
56
|
listEnabledMcpCapabilityServers,
|
|
44
57
|
listInstalledApiIntegrations,
|
|
45
58
|
listInstalledSkills,
|
|
46
|
-
listPackInstallations,
|
|
47
59
|
listSocialConnections,
|
|
48
60
|
mcpServerIdForCapability,
|
|
49
61
|
upsertCapabilityCatalogItem,
|
|
@@ -53,11 +65,15 @@ import {
|
|
|
53
65
|
type InstalledSkillSummary,
|
|
54
66
|
} from "@opengeni/db";
|
|
55
67
|
import { HTTPException } from "hono/http-exception";
|
|
56
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
withOrganizationIntegrationAcquisition,
|
|
70
|
+
withOrganizationIntegrationPolicyFence,
|
|
71
|
+
} from "@opengeni/db/organization-integration-policy";
|
|
72
|
+
|
|
57
73
|
import { isFikenConnection, preferredFikenConnection } from "./fiken";
|
|
74
|
+
import { isOnePasswordConnection, onePasswordConnectionMetadata } from "./vaults/provider-registry";
|
|
58
75
|
import { listSkillLibraryEntries, type SkillLibraryEntry } from "@opengeni/runtime/skill-library";
|
|
59
|
-
import {
|
|
60
|
-
import { assertHostMcpAuthoritySourceAdmissionEnabled } from "./host-mcp-authority-source-admission";
|
|
76
|
+
import { assertNativeMcpConnectionRef } from "./native-mcp-connection-admission";
|
|
61
77
|
|
|
62
78
|
const officialMcpRegistryUrl = "https://registry.modelcontextprotocol.io";
|
|
63
79
|
const firstPartyMcpServerIds = new Set(["opengeni", "files", "docs"]);
|
|
@@ -66,7 +82,6 @@ const mcpRegistryMaxPages = 3;
|
|
|
66
82
|
const mcpCapabilityProbeTimeoutMs = 15000;
|
|
67
83
|
const maxMcpCredentialHeaders = 16;
|
|
68
84
|
const maxMcpCredentialHeaderValueLength = 4096;
|
|
69
|
-
const officialGmailMcpUrl = "https://gmailmcp.googleapis.com/mcp/v1";
|
|
70
85
|
// RFC 9110 field-name token characters.
|
|
71
86
|
const mcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
72
87
|
|
|
@@ -75,30 +90,36 @@ export async function buildCapabilityCatalog(input: {
|
|
|
75
90
|
workspaceId: string;
|
|
76
91
|
settings: Settings;
|
|
77
92
|
subjectId?: string | null;
|
|
93
|
+
/** The workspace's organization, when the caller knows it (skips a lookup). */
|
|
94
|
+
accountId?: string;
|
|
78
95
|
}): Promise<CapabilityCatalogResponse> {
|
|
79
96
|
const [
|
|
80
97
|
persistedItems,
|
|
81
98
|
capabilityInstallations,
|
|
82
|
-
packInstallations,
|
|
83
|
-
workspacePacks,
|
|
84
99
|
socialConnections,
|
|
85
100
|
workspaceConnections,
|
|
86
101
|
curatedLibrarySkills,
|
|
87
102
|
installedSkills,
|
|
88
|
-
|
|
103
|
+
codexAppsDesignation,
|
|
104
|
+
runnableMcpServers,
|
|
89
105
|
] = await Promise.all([
|
|
90
106
|
listCapabilityCatalogItems(input.db, input.workspaceId),
|
|
91
107
|
listCapabilityInstallations(input.db, input.workspaceId),
|
|
92
|
-
listPackInstallations(input.db, input.workspaceId),
|
|
93
|
-
listWorkspaceCapabilityPacks(input.db, input.workspaceId),
|
|
94
108
|
listSocialConnections(input.db, input.workspaceId, 500, input.subjectId),
|
|
95
109
|
listConnectionsMetadata(input.db, input.workspaceId, null),
|
|
96
110
|
discoverCuratedSkillLibraryItems(),
|
|
97
111
|
listInstalledSkills(input.db, input.workspaceId),
|
|
98
112
|
input.settings.codexConnectedAppsEnabled
|
|
99
|
-
?
|
|
113
|
+
? resolveCodexAppsDesignationForRun(
|
|
114
|
+
input.db,
|
|
115
|
+
input.workspaceId,
|
|
116
|
+
input.accountId ? { accountId: input.accountId } : {},
|
|
117
|
+
)
|
|
100
118
|
: Promise.resolve(null),
|
|
119
|
+
listEnabledMcpCapabilityServers(input.db, input.workspaceId),
|
|
101
120
|
]);
|
|
121
|
+
const runnableCapabilityIds = new Set(runnableMcpServers.map((server) => server.capabilityId));
|
|
122
|
+
const operatorOAuthClientConfigured = operatorOAuthClientResolver(input.settings);
|
|
102
123
|
const catalogInstallations = capabilityInstallations.filter(
|
|
103
124
|
(installation) => installation.kind === "mcp",
|
|
104
125
|
);
|
|
@@ -110,19 +131,11 @@ export async function buildCapabilityCatalog(input: {
|
|
|
110
131
|
.filter((skill) => skill.owners.some((owner) => owner.kind === "direct"))
|
|
111
132
|
.map((skill) => [skill.capabilityId, skill]),
|
|
112
133
|
);
|
|
113
|
-
const activePackIds = new Set(
|
|
114
|
-
packInstallations
|
|
115
|
-
.filter((installation) => installation.status === "active")
|
|
116
|
-
.map((installation) => installation.packId),
|
|
117
|
-
);
|
|
118
|
-
const builtInPackIds = new Set(listCapabilityPacks().map((pack) => pack.id));
|
|
119
134
|
const builtIns = [
|
|
120
|
-
...workspacePacks.map((pack) =>
|
|
121
|
-
packCatalogItem(pack, builtInPackIds.has(pack.id) ? "built_in" : "manual"),
|
|
122
|
-
),
|
|
123
135
|
...configuredMcpCatalogItems(input.settings),
|
|
124
136
|
...providerIntegrationCatalogItems(socialConnections),
|
|
125
137
|
fikenCatalogItem(workspaceConnections.filter(isFikenConnection)),
|
|
138
|
+
onePasswordCatalogItem(workspaceConnections.filter(isOnePasswordConnection)),
|
|
126
139
|
...curatedLibrarySkills.filter((item) => installedSkillById.has(item.id)),
|
|
127
140
|
...installedSkills
|
|
128
141
|
.filter(
|
|
@@ -132,7 +145,7 @@ export async function buildCapabilityCatalog(input: {
|
|
|
132
145
|
.map(installedSkillCatalogItem),
|
|
133
146
|
];
|
|
134
147
|
const codexApps = input.settings.codexConnectedAppsEnabled
|
|
135
|
-
? codexAppsCatalogItem(
|
|
148
|
+
? codexAppsCatalogItem(codexAppsDesignation !== null)
|
|
136
149
|
: null;
|
|
137
150
|
const items = dedupeCatalogItems([
|
|
138
151
|
...builtIns,
|
|
@@ -151,8 +164,28 @@ export async function buildCapabilityCatalog(input: {
|
|
|
151
164
|
const projected =
|
|
152
165
|
item.kind === "skill"
|
|
153
166
|
? applyInstalledSkillEnablement(item, installedSkillById.get(item.id))
|
|
154
|
-
: applyCapabilityEnablement(item, capabilityInstallationById.get(item.id)
|
|
155
|
-
|
|
167
|
+
: applyCapabilityEnablement(item, capabilityInstallationById.get(item.id));
|
|
168
|
+
// An installed connector can become unrunnable after a lifecycle or
|
|
169
|
+
// ownership change. Use the execution registry rather than presenting
|
|
170
|
+
// its stale catalog definition as a selectable tool.
|
|
171
|
+
const runtimeProjected =
|
|
172
|
+
projected.kind === "mcp" &&
|
|
173
|
+
projected.enabled &&
|
|
174
|
+
projected.source !== "configured" &&
|
|
175
|
+
projected.source !== "built_in" &&
|
|
176
|
+
!runnableCapabilityIds.has(projected.id)
|
|
177
|
+
? {
|
|
178
|
+
...projected,
|
|
179
|
+
runtime: {
|
|
180
|
+
...projected.runtime,
|
|
181
|
+
available: false,
|
|
182
|
+
notes: "This installed connection is unavailable. Review its connection settings.",
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
: projected;
|
|
186
|
+
return applyCapabilityLifecycle(
|
|
187
|
+
applyOperatorOAuthClientRequirement(runtimeProjected, operatorOAuthClientConfigured),
|
|
188
|
+
);
|
|
156
189
|
})
|
|
157
190
|
.sort(compareCatalogItems);
|
|
158
191
|
return {
|
|
@@ -161,6 +194,40 @@ export async function buildCapabilityCatalog(input: {
|
|
|
161
194
|
};
|
|
162
195
|
}
|
|
163
196
|
|
|
197
|
+
/**
|
|
198
|
+
* `metadata.oauthClientRequirement` is stamped by the catalog import from
|
|
199
|
+
* `data/catalog/oauth-client-requirements.json` for providers that refuse
|
|
200
|
+
* OAuth self-registration. Whether the row can connect here is a deployment
|
|
201
|
+
* fact, so the catalog projects it rather than every surface guessing. The
|
|
202
|
+
* matcher is the one the OAuth start uses to find an operator client.
|
|
203
|
+
*/
|
|
204
|
+
function operatorOAuthClientResolver(settings: Settings): (issuer: string) => boolean {
|
|
205
|
+
let configured: ReturnType<typeof parseIntegrationsOauthClientsJson> = {};
|
|
206
|
+
try {
|
|
207
|
+
configured = parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
|
|
208
|
+
} catch {
|
|
209
|
+
// Startup validates this setting; a malformed value can configure nothing.
|
|
210
|
+
}
|
|
211
|
+
return (issuer) => findIntegrationsOauthClient(configured, [issuer]) !== null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function applyOperatorOAuthClientRequirement(
|
|
215
|
+
item: CapabilityCatalogItem,
|
|
216
|
+
configured: (issuer: string) => boolean,
|
|
217
|
+
): CapabilityCatalogItem {
|
|
218
|
+
if (item.kind !== "mcp") return item;
|
|
219
|
+
const requirement = item.metadata.oauthClientRequirement;
|
|
220
|
+
const issuer =
|
|
221
|
+
requirement && typeof requirement === "object" && !Array.isArray(requirement)
|
|
222
|
+
? (requirement as Record<string, unknown>).issuer
|
|
223
|
+
: undefined;
|
|
224
|
+
if (typeof issuer !== "string" || !issuer) return item;
|
|
225
|
+
return {
|
|
226
|
+
...item,
|
|
227
|
+
runtime: { ...item.runtime, operatorOAuthClient: { configured: configured(issuer) } },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
164
231
|
export async function createCatalogItem(input: {
|
|
165
232
|
db: Database;
|
|
166
233
|
accountId: string;
|
|
@@ -168,11 +235,7 @@ export async function createCatalogItem(input: {
|
|
|
168
235
|
payload: CreateCapabilityCatalogItemRequest;
|
|
169
236
|
}): Promise<CapabilityCatalogItem> {
|
|
170
237
|
const id = input.payload.id?.trim() || generatedCapabilityId(input.payload);
|
|
171
|
-
|
|
172
|
-
throw new HTTPException(422, {
|
|
173
|
-
message: "packs are managed by OpenGeni and cannot be manually created",
|
|
174
|
-
});
|
|
175
|
-
}
|
|
238
|
+
|
|
176
239
|
if (id.startsWith("skill:")) {
|
|
177
240
|
throw new HTTPException(422, {
|
|
178
241
|
message: "Skills are installed through the Skill library or source import flow",
|
|
@@ -240,6 +303,14 @@ type EnableCapabilityInput = {
|
|
|
240
303
|
capabilityId: string;
|
|
241
304
|
payload: EnableCapabilityRequest;
|
|
242
305
|
probeMcpServer?: McpCapabilityProbe;
|
|
306
|
+
/** Server-owned receipt from a just-verified native Connect credential.
|
|
307
|
+
* Never projected into EnableCapabilityRequest or accepted from a client. */
|
|
308
|
+
verifiedConnection?: {
|
|
309
|
+
id: string;
|
|
310
|
+
version: number;
|
|
311
|
+
endpointUrl: string;
|
|
312
|
+
connectivity: Record<string, unknown>;
|
|
313
|
+
};
|
|
243
314
|
};
|
|
244
315
|
|
|
245
316
|
export async function enableCapability(
|
|
@@ -252,6 +323,7 @@ export async function enableCapability(
|
|
|
252
323
|
/** Probe outside a durable Connect commit; persist the exact prepared settings
|
|
253
324
|
* inside the caller's authorized receipt transaction. Native enable uses this too. */
|
|
254
325
|
export async function prepareCapabilityEnable(input: EnableCapabilityInput) {
|
|
326
|
+
input = { ...input, payload: structuredClone(input.payload) };
|
|
255
327
|
const item = await requireCatalogItem(
|
|
256
328
|
input.db,
|
|
257
329
|
input.workspaceId,
|
|
@@ -277,11 +349,7 @@ export async function prepareCapabilityEnable(input: EnableCapabilityInput) {
|
|
|
277
349
|
message: "Install Plugins through the Plugin Package flow",
|
|
278
350
|
});
|
|
279
351
|
}
|
|
280
|
-
|
|
281
|
-
throw new HTTPException(409, {
|
|
282
|
-
message: "Install Packs through the Pack installation preview flow",
|
|
283
|
-
});
|
|
284
|
-
}
|
|
352
|
+
|
|
285
353
|
if (item.kind === "mcp" && !item.runtime.available) {
|
|
286
354
|
throw new HTTPException(422, {
|
|
287
355
|
message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled",
|
|
@@ -297,16 +365,69 @@ export async function prepareCapabilityEnable(input: EnableCapabilityInput) {
|
|
|
297
365
|
delete installationConfig.headerNames;
|
|
298
366
|
delete installationConfig.connectionRef;
|
|
299
367
|
if (item.kind === "mcp") {
|
|
300
|
-
|
|
368
|
+
if (
|
|
369
|
+
item.endpointUrl?.replace(/\/+$/u, "") === "https://gmailmcp.googleapis.com/mcp/v1" &&
|
|
370
|
+
(input.payload.connectionRef?.subjectScope !== "subject" ||
|
|
371
|
+
Object.keys(input.payload.headers ?? {}).length > 0)
|
|
372
|
+
) {
|
|
373
|
+
throw new HTTPException(422, {
|
|
374
|
+
message: "Gmail requires a personal-owned connection reference",
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
// This generic, potentially workspace-authored catalog is not proof of a
|
|
378
|
+
// curated provider identity. Dedicated Connect adapters classify separately.
|
|
379
|
+
const unchanged = await withOrganizationIntegrationPolicyFence(
|
|
380
|
+
input.db,
|
|
381
|
+
input,
|
|
382
|
+
async (tx, policy) => {
|
|
383
|
+
const existing = await unchangedMcpInstallation(
|
|
384
|
+
{ ...input, db: tx },
|
|
385
|
+
item,
|
|
386
|
+
installationConfig,
|
|
387
|
+
);
|
|
388
|
+
if (existing) return existing;
|
|
389
|
+
assertOrganizationIntegrationAllowed(policy, "custom:mcp");
|
|
390
|
+
return null;
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
if (unchanged) {
|
|
394
|
+
return {
|
|
395
|
+
commit: (db: Database) =>
|
|
396
|
+
withOrganizationIntegrationPolicyFence(db, input, async (tx) => {
|
|
397
|
+
const current = await unchangedMcpInstallation(
|
|
398
|
+
{ ...input, db: tx },
|
|
399
|
+
item,
|
|
400
|
+
installationConfig,
|
|
401
|
+
);
|
|
402
|
+
if (!current || !isDeepStrictEqual(current, unchanged)) {
|
|
403
|
+
throw new HTTPException(409, {
|
|
404
|
+
message: "Capability installation changed; reload before reconciling",
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return current;
|
|
408
|
+
}),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const headers = input.verifiedConnection
|
|
412
|
+
? null
|
|
413
|
+
: await resolveMcpCredentialHeaders(input, item);
|
|
301
414
|
const connectionRef = input.payload.connectionRef
|
|
302
415
|
? await validateMcpCapabilityConnectionRef(input, item, input.payload.connectionRef)
|
|
303
416
|
: null;
|
|
304
417
|
assertRequiredMcpCredentialHeaders(item, headers, connectionRef);
|
|
418
|
+
if (input.verifiedConnection) await validateVerifiedMcpConnection(input, item);
|
|
305
419
|
installationMetadata = {
|
|
306
420
|
...installationMetadata,
|
|
307
|
-
...(
|
|
308
|
-
?
|
|
309
|
-
:
|
|
421
|
+
...(input.verifiedConnection
|
|
422
|
+
? input.verifiedConnection.connectivity
|
|
423
|
+
: connectionRef && !headers
|
|
424
|
+
? authDeferredMcpConnectivity()
|
|
425
|
+
: await validateMcpCapabilityConnection(
|
|
426
|
+
item,
|
|
427
|
+
input.probeMcpServer,
|
|
428
|
+
headers ?? undefined,
|
|
429
|
+
input.settings,
|
|
430
|
+
)),
|
|
310
431
|
};
|
|
311
432
|
if (connectionRef) {
|
|
312
433
|
installationConfig.connectionRef = connectionRef;
|
|
@@ -325,8 +446,92 @@ export async function prepareCapabilityEnable(input: EnableCapabilityInput) {
|
|
|
325
446
|
kind: item.kind,
|
|
326
447
|
config: installationConfig,
|
|
327
448
|
metadata: installationMetadata,
|
|
449
|
+
...(input.payload.onlyIfUninstalled ? { onlyIfUninstalled: true } : {}),
|
|
450
|
+
};
|
|
451
|
+
return {
|
|
452
|
+
commit: (db: Database) =>
|
|
453
|
+
item.kind === "mcp"
|
|
454
|
+
? withOrganizationIntegrationAcquisition(db, installation, ["custom:mcp"], async (tx) => {
|
|
455
|
+
if (input.verifiedConnection)
|
|
456
|
+
await validateVerifiedMcpConnection({ ...input, db: tx }, item);
|
|
457
|
+
return enableCapabilityInstallation(tx, installation);
|
|
458
|
+
})
|
|
459
|
+
: enableCapabilityInstallation(db, installation),
|
|
328
460
|
};
|
|
329
|
-
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function validateVerifiedMcpConnection(
|
|
464
|
+
input: EnableCapabilityInput,
|
|
465
|
+
item: CapabilityCatalogItem,
|
|
466
|
+
) {
|
|
467
|
+
const proof = input.verifiedConnection;
|
|
468
|
+
const ref = input.payload.connectionRef;
|
|
469
|
+
if (
|
|
470
|
+
!proof ||
|
|
471
|
+
!ref ||
|
|
472
|
+
item.endpointUrl !== proof.endpointUrl ||
|
|
473
|
+
ref.resource !== proof.endpointUrl
|
|
474
|
+
)
|
|
475
|
+
throw new HTTPException(409, { message: "MCP verification no longer matches the connection" });
|
|
476
|
+
const connection = await getConnectionMetadata(
|
|
477
|
+
input.db,
|
|
478
|
+
input.workspaceId,
|
|
479
|
+
proof.id,
|
|
480
|
+
input.grant.subjectId,
|
|
481
|
+
);
|
|
482
|
+
if (
|
|
483
|
+
!connection ||
|
|
484
|
+
connection.version !== proof.version ||
|
|
485
|
+
connection.status !== "active" ||
|
|
486
|
+
connection.providerDomain !== ref.providerDomain ||
|
|
487
|
+
connection.kind !== ref.kind ||
|
|
488
|
+
connection.subjectId !== (ref.subjectScope === "subject" ? input.grant.subjectId : null) ||
|
|
489
|
+
connection.metadata.mcpUrl !== proof.endpointUrl
|
|
490
|
+
)
|
|
491
|
+
throw new HTTPException(409, { message: "MCP connection changed after verification" });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** A no-effect reconciliation is not a new acquisition. Validate ordinary
|
|
495
|
+
* connection visibility before locking the installation, and compare actual
|
|
496
|
+
* stored credentials rather than trusting caller-supplied identity hints. */
|
|
497
|
+
async function unchangedMcpInstallation(
|
|
498
|
+
input: EnableCapabilityInput,
|
|
499
|
+
item: CapabilityCatalogItem,
|
|
500
|
+
requestedConfig: Record<string, unknown>,
|
|
501
|
+
): Promise<CapabilityInstallation | null> {
|
|
502
|
+
const connectionRef = input.payload.connectionRef
|
|
503
|
+
? await validateMcpCapabilityConnectionRef(input, item, input.payload.connectionRef)
|
|
504
|
+
: null;
|
|
505
|
+
const existing = await withLockedCapabilityInstallation(
|
|
506
|
+
input.db,
|
|
507
|
+
input.workspaceId,
|
|
508
|
+
item.id,
|
|
509
|
+
(tx) => getCapabilityInstallation(tx, input.workspaceId, item.id),
|
|
510
|
+
);
|
|
511
|
+
if (!existing || existing.kind !== "mcp" || existing.status !== "active") return null;
|
|
512
|
+
const {
|
|
513
|
+
headerNames: _names,
|
|
514
|
+
headersEncrypted: _encrypted,
|
|
515
|
+
headers: _headers,
|
|
516
|
+
...storedConfig
|
|
517
|
+
} = existing.config;
|
|
518
|
+
const config = { ...requestedConfig, ...(connectionRef ? { connectionRef } : {}) };
|
|
519
|
+
const { mcpConnectivity: _storedConnectivity, ...storedMetadata } = existing.metadata;
|
|
520
|
+
const { mcpConnectivity: _requestedConnectivity, ...requestedMetadata } = input.payload.metadata;
|
|
521
|
+
if (
|
|
522
|
+
!isDeepStrictEqual(config, storedConfig) ||
|
|
523
|
+
!isDeepStrictEqual(requestedMetadata, storedMetadata)
|
|
524
|
+
)
|
|
525
|
+
return null;
|
|
526
|
+
const provided = normalizedMcpCredentialHeaders(input.payload.headers);
|
|
527
|
+
if (provided) {
|
|
528
|
+
const stored = await resolveMcpCredentialHeaders(
|
|
529
|
+
{ ...input, payload: { ...input.payload, headers: {} } },
|
|
530
|
+
item,
|
|
531
|
+
);
|
|
532
|
+
if (!isDeepStrictEqual(provided, stored)) return null;
|
|
533
|
+
}
|
|
534
|
+
return existing;
|
|
330
535
|
}
|
|
331
536
|
|
|
332
537
|
/**
|
|
@@ -425,24 +630,11 @@ async function validateMcpCapabilityConnectionRef(
|
|
|
425
630
|
ref: McpServerConnectionRef,
|
|
426
631
|
): Promise<McpServerConnectionRef> {
|
|
427
632
|
const subjectScope = ref.subjectScope ?? "workspace";
|
|
428
|
-
const endpointUrl = item.endpointUrl?.replace(/\/+$/, "");
|
|
429
|
-
// Gmail is a consumer mailbox; Slack's hosted MCP issues user tokens only and
|
|
430
|
-
// shared Slack authority belongs to the OpenGeni workspace bot. Neither may
|
|
431
|
-
// become a workspace-owned connection reference.
|
|
432
|
-
const personalOnly =
|
|
433
|
-
item.metadata.connectionOwnership === "personal_only" ||
|
|
434
|
-
endpointUrl === officialGmailMcpUrl ||
|
|
435
|
-
endpointUrl === OPENGENI_PERSONAL_SLACK_MCP_URL;
|
|
436
|
-
if (personalOnly && subjectScope !== "subject") {
|
|
437
|
-
throw new HTTPException(422, {
|
|
438
|
-
message:
|
|
439
|
-
"this capability requires a personal connection; each workspace member must connect their own account",
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
633
|
const normalized: McpServerConnectionRef = {
|
|
443
634
|
providerDomain: ref.providerDomain.trim(),
|
|
444
635
|
subjectScope,
|
|
445
636
|
...(ref.connectionId ? { connectionId: ref.connectionId } : {}),
|
|
637
|
+
...(ref.accountSelection ? { accountSelection: ref.accountSelection } : {}),
|
|
446
638
|
...(ref.authoritySource === "host" ? { authoritySource: "host" as const } : {}),
|
|
447
639
|
...(ref.provider ? { provider: ref.provider.trim() } : {}),
|
|
448
640
|
...(ref.kind ? { kind: ref.kind } : {}),
|
|
@@ -468,7 +660,7 @@ async function validateMcpCapabilityConnectionRef(
|
|
|
468
660
|
});
|
|
469
661
|
}
|
|
470
662
|
if (normalized.authoritySource === "host") {
|
|
471
|
-
|
|
663
|
+
assertNativeMcpConnectionRef(normalized);
|
|
472
664
|
return normalized;
|
|
473
665
|
}
|
|
474
666
|
|
|
@@ -480,7 +672,11 @@ async function validateMcpCapabilityConnectionRef(
|
|
|
480
672
|
input.grant.subjectId,
|
|
481
673
|
)
|
|
482
674
|
: null;
|
|
483
|
-
if (
|
|
675
|
+
if (
|
|
676
|
+
!connection &&
|
|
677
|
+
!normalized.connectionId &&
|
|
678
|
+
(subjectScope === "subject" || normalized.accountSelection === "all_eligible")
|
|
679
|
+
) {
|
|
484
680
|
const visible = await listConnectionsMetadata(
|
|
485
681
|
input.db,
|
|
486
682
|
input.workspaceId,
|
|
@@ -489,7 +685,7 @@ async function validateMcpCapabilityConnectionRef(
|
|
|
489
685
|
connection =
|
|
490
686
|
visible.find(
|
|
491
687
|
(candidate) =>
|
|
492
|
-
candidate.subjectId === input.grant.subjectId &&
|
|
688
|
+
candidate.subjectId === (subjectScope === "subject" ? input.grant.subjectId : null) &&
|
|
493
689
|
candidate.providerDomain === normalized.providerDomain &&
|
|
494
690
|
(!normalized.kind || candidate.kind === normalized.kind) &&
|
|
495
691
|
candidate.status === "active",
|
|
@@ -589,6 +785,7 @@ export type McpCapabilityProbeInput = {
|
|
|
589
785
|
url: string;
|
|
590
786
|
timeoutMs: number;
|
|
591
787
|
headers?: Record<string, string>;
|
|
788
|
+
settings?: Settings;
|
|
592
789
|
};
|
|
593
790
|
|
|
594
791
|
export type McpCapabilityProbeResult = {
|
|
@@ -603,6 +800,7 @@ export async function validateMcpCapabilityConnection(
|
|
|
603
800
|
item: CapabilityCatalogItem,
|
|
604
801
|
probe: McpCapabilityProbe = probeStreamableHttpMcpServer,
|
|
605
802
|
headers?: Record<string, string>,
|
|
803
|
+
settings?: Settings,
|
|
606
804
|
): Promise<Record<string, unknown>> {
|
|
607
805
|
if (item.kind !== "mcp") {
|
|
608
806
|
return {};
|
|
@@ -619,6 +817,7 @@ export async function validateMcpCapabilityConnection(
|
|
|
619
817
|
url: item.endpointUrl,
|
|
620
818
|
timeoutMs: mcpCapabilityProbeTimeoutMs,
|
|
621
819
|
...(headers ? { headers } : {}),
|
|
820
|
+
...(settings ? { settings } : {}),
|
|
622
821
|
});
|
|
623
822
|
return {
|
|
624
823
|
mcpConnectivity: {
|
|
@@ -637,6 +836,8 @@ export async function validateMcpCapabilityConnection(
|
|
|
637
836
|
async function probeStreamableHttpMcpServer(
|
|
638
837
|
input: McpCapabilityProbeInput,
|
|
639
838
|
): Promise<McpCapabilityProbeResult> {
|
|
839
|
+
const settings = input.settings;
|
|
840
|
+
if (!settings) throw new Error("Network policy is unavailable for MCP validation");
|
|
640
841
|
const controller = new AbortController();
|
|
641
842
|
const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
642
843
|
const client = new Client(
|
|
@@ -649,6 +850,11 @@ async function probeStreamableHttpMcpServer(
|
|
|
649
850
|
signal: controller.signal,
|
|
650
851
|
...(input.headers ? { headers: input.headers } : {}),
|
|
651
852
|
},
|
|
853
|
+
fetch: (url, init) =>
|
|
854
|
+
pinnedFetch(url, init, settings, {
|
|
855
|
+
label: "MCP capability probe",
|
|
856
|
+
requireHttpsOutsideLocalTest: true,
|
|
857
|
+
}),
|
|
652
858
|
});
|
|
653
859
|
await client.connect(transport as unknown as Transport, {
|
|
654
860
|
timeout: input.timeoutMs,
|
|
@@ -674,9 +880,9 @@ function mcpProbeErrorMessage(error: unknown, endpointUrl: string): string {
|
|
|
674
880
|
normalized,
|
|
675
881
|
)
|
|
676
882
|
) {
|
|
677
|
-
return `
|
|
883
|
+
return `Opengeni could not reach a valid Streamable HTTP MCP server at ${endpoint}. Check the endpoint URL or choose a different catalog entry.`;
|
|
678
884
|
}
|
|
679
|
-
return `
|
|
885
|
+
return `Opengeni could not initialize ${endpoint}. Check the endpoint configuration or try again.`;
|
|
680
886
|
}
|
|
681
887
|
|
|
682
888
|
function safeEndpointLabel(endpointUrl: string): string {
|
|
@@ -715,11 +921,7 @@ export async function disableCapability(input: {
|
|
|
715
921
|
message: "Remove Plugins through the Plugin Package flow",
|
|
716
922
|
});
|
|
717
923
|
}
|
|
718
|
-
|
|
719
|
-
throw new HTTPException(409, {
|
|
720
|
-
message: "Uninstall Packs through the Pack uninstall preview flow",
|
|
721
|
-
});
|
|
722
|
-
}
|
|
924
|
+
|
|
723
925
|
if (item.source === "built_in" || item.source === "configured") {
|
|
724
926
|
throw new HTTPException(409, {
|
|
725
927
|
message:
|
|
@@ -742,27 +944,111 @@ export async function settingsWithEnabledCapabilityMcpServers(
|
|
|
742
944
|
subjectId?: string;
|
|
743
945
|
personalConnectionDelegations?: readonly McpPersonalConnectionDelegation[];
|
|
744
946
|
onResolvedApiIntegrations?: (integrations: readonly ApiIntegrationRuntime[]) => void;
|
|
947
|
+
/** Every enabled capability server read, before static-id shadowing. */
|
|
948
|
+
onResolvedCapabilityServers?: (servers: readonly EnabledMcpCapabilityServer[]) => void;
|
|
949
|
+
/**
|
|
950
|
+
* The Codex Apps designation the caller already resolves (a claim reads
|
|
951
|
+
* its cutover once), or what it knows for resolving it here.
|
|
952
|
+
*/
|
|
953
|
+
codexApps?: Promise<CodexAppsDesignationForRun | null> | CodexAppsRunContext;
|
|
954
|
+
/**
|
|
955
|
+
* `db` is one bounded transaction: run the reads one after another, since
|
|
956
|
+
* its RLS scopes must not overlap.
|
|
957
|
+
*/
|
|
958
|
+
oneReadAtATime?: boolean;
|
|
745
959
|
},
|
|
746
960
|
): Promise<Settings> {
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
961
|
+
const readApiIntegrations = () =>
|
|
962
|
+
options?.subjectId
|
|
963
|
+
? listInstalledApiIntegrations(db, workspaceId, options.subjectId)
|
|
964
|
+
: listInstalledApiIntegrationsForDelegations(
|
|
965
|
+
db,
|
|
966
|
+
workspaceId,
|
|
967
|
+
options?.personalConnectionDelegations ?? [],
|
|
968
|
+
);
|
|
969
|
+
// Registration is dropped below when Apps are off for the deployment, so
|
|
970
|
+
// resolving the designation then would be pure cost.
|
|
971
|
+
const readCodexAppsDesignation = () =>
|
|
972
|
+
!settings.codexConnectedAppsEnabled
|
|
973
|
+
? Promise.resolve(null)
|
|
974
|
+
: options?.codexApps instanceof Promise
|
|
975
|
+
? options.codexApps
|
|
976
|
+
: resolveCodexAppsDesignationForRun(db, workspaceId, options?.codexApps ?? {});
|
|
977
|
+
let enabled: Awaited<ReturnType<typeof listEnabledMcpCapabilityServers>>;
|
|
978
|
+
let apiIntegrations: Awaited<ReturnType<typeof readApiIntegrations>>;
|
|
979
|
+
let codexAppsDesignation: Awaited<ReturnType<typeof readCodexAppsDesignation>>;
|
|
980
|
+
if (options?.oneReadAtATime) {
|
|
981
|
+
enabled = await listEnabledMcpCapabilityServers(db, workspaceId);
|
|
982
|
+
apiIntegrations = await readApiIntegrations();
|
|
983
|
+
codexAppsDesignation = await readCodexAppsDesignation();
|
|
984
|
+
} else {
|
|
985
|
+
// The integration read starts first: callers that pass a transaction rely
|
|
986
|
+
// on its subject scope being established before the other reads begin.
|
|
987
|
+
const apiIntegrationsPromise = readApiIntegrations();
|
|
988
|
+
[enabled, apiIntegrations, codexAppsDesignation] = await Promise.all([
|
|
989
|
+
listEnabledMcpCapabilityServers(db, workspaceId),
|
|
990
|
+
apiIntegrationsPromise,
|
|
991
|
+
readCodexAppsDesignation(),
|
|
992
|
+
]);
|
|
993
|
+
}
|
|
759
994
|
options?.onResolvedApiIntegrations?.(apiIntegrations);
|
|
995
|
+
options?.onResolvedCapabilityServers?.(enabled);
|
|
996
|
+
return settingsWithCodexAppsMcpServer(
|
|
997
|
+
settingsWithApiIntegrationServers(
|
|
998
|
+
settingsWithMcpCapabilityServers(settings, enabled),
|
|
999
|
+
apiIntegrations,
|
|
1000
|
+
),
|
|
1001
|
+
codexAppsDesignation !== null,
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* `settingsWithEnabledCapabilityMcpServers` for a live target fence: only the
|
|
1007
|
+
* rows that can produce `serverIds`. That is the capability installations that
|
|
1008
|
+
* produced them at preparation plus any catalog item explicitly claiming one of
|
|
1009
|
+
* those ids (shadowing), integrations with exactly those runtime keys (read
|
|
1010
|
+
* with the same subject visibility), and the Codex designation only when Codex
|
|
1011
|
+
* Apps itself is a target. Static servers come from `settings`, unchanged.
|
|
1012
|
+
* Restricted to `serverIds`, the result equals the complete projection.
|
|
1013
|
+
*/
|
|
1014
|
+
export async function settingsWithEnabledCapabilityMcpServersForTargets(
|
|
1015
|
+
db: Database,
|
|
1016
|
+
workspaceId: string,
|
|
1017
|
+
settings: Settings,
|
|
1018
|
+
options: {
|
|
1019
|
+
subjectId: string;
|
|
1020
|
+
accountId: string;
|
|
1021
|
+
serverIds: readonly string[];
|
|
1022
|
+
capabilityIds: readonly string[];
|
|
1023
|
+
onResolvedApiIntegrations?: (integrations: readonly ApiIntegrationRuntime[]) => void;
|
|
1024
|
+
/** Checked between reads: a stopped caller starts no further read. */
|
|
1025
|
+
signal?: AbortSignal;
|
|
1026
|
+
},
|
|
1027
|
+
): Promise<Settings> {
|
|
1028
|
+
const serverIds = new Set(options.serverIds);
|
|
1029
|
+
// One RLS scope at a time: `db` may be a single bounded transaction.
|
|
1030
|
+
const enabled = (
|
|
1031
|
+
await listEnabledMcpCapabilityServers(db, workspaceId, {
|
|
1032
|
+
capabilityIds: options.capabilityIds,
|
|
1033
|
+
serverIds: [...serverIds],
|
|
1034
|
+
})
|
|
1035
|
+
).filter((server) => serverIds.has(server.id));
|
|
1036
|
+
options.signal?.throwIfAborted();
|
|
1037
|
+
const apiIntegrations = await listInstalledApiIntegrations(db, workspaceId, options.subjectId, [
|
|
1038
|
+
...serverIds,
|
|
1039
|
+
]);
|
|
1040
|
+
options.signal?.throwIfAborted();
|
|
1041
|
+
const codexAppsDesignation =
|
|
1042
|
+
settings.codexConnectedAppsEnabled && serverIds.has(CODEX_APPS_MCP_SERVER_ID)
|
|
1043
|
+
? await resolveCodexAppsDesignationForRun(db, workspaceId, { accountId: options.accountId })
|
|
1044
|
+
: null;
|
|
1045
|
+
options.onResolvedApiIntegrations?.(apiIntegrations);
|
|
760
1046
|
return settingsWithCodexAppsMcpServer(
|
|
761
1047
|
settingsWithApiIntegrationServers(
|
|
762
1048
|
settingsWithMcpCapabilityServers(settings, enabled),
|
|
763
1049
|
apiIntegrations,
|
|
764
1050
|
),
|
|
765
|
-
|
|
1051
|
+
codexAppsDesignation !== null,
|
|
766
1052
|
);
|
|
767
1053
|
}
|
|
768
1054
|
|
|
@@ -773,7 +1059,7 @@ export function apiIntegrationsMatchingDelegations(
|
|
|
773
1059
|
const exact = new Set(
|
|
774
1060
|
delegations.map((delegation) =>
|
|
775
1061
|
[
|
|
776
|
-
delegation.serverId,
|
|
1062
|
+
delegation.canonicalServerId ?? delegation.serverId,
|
|
777
1063
|
delegation.connectionId,
|
|
778
1064
|
delegation.providerDomain.toLowerCase(),
|
|
779
1065
|
delegation.kind ?? "",
|
|
@@ -842,19 +1128,77 @@ export function settingsWithApiIntegrationServers(
|
|
|
842
1128
|
}
|
|
843
1129
|
|
|
844
1130
|
/**
|
|
845
|
-
*
|
|
846
|
-
*
|
|
1131
|
+
* The workspace's executable Codex Apps designation. Without a Codex cutover
|
|
1132
|
+
* row this is the legacy designation (`resolveCodexAppsCredentialIdForRun`).
|
|
1133
|
+
* With an enabled cutover it is the core designation, which the database
|
|
1134
|
+
* rechecks (designated shared Codex connection, still in the workspace's
|
|
1135
|
+
* scope, active). A disabled cutover row designates nothing and reads no
|
|
1136
|
+
* legacy Codex table.
|
|
1137
|
+
*/
|
|
1138
|
+
export type CodexAppsDesignationForRun = {
|
|
1139
|
+
source: "core";
|
|
1140
|
+
accountId: string;
|
|
1141
|
+
connectionId: string;
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* What the caller already knows, so the resolver does not read it again: the
|
|
1146
|
+
* workspace's organization (skips the workspace lookup) and the organization's
|
|
1147
|
+
* Codex cutover disposition (skips the cutover read; a claim has read it).
|
|
1148
|
+
*/
|
|
1149
|
+
export type CodexAppsRunContext = {
|
|
1150
|
+
accountId?: string;
|
|
1151
|
+
disposition?: CodexCutoverDisposition | Promise<CodexCutoverDisposition>;
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
export async function resolveCodexAppsDesignationForRun(
|
|
1155
|
+
db: Database,
|
|
1156
|
+
workspaceId: string,
|
|
1157
|
+
known: CodexAppsRunContext = {},
|
|
1158
|
+
): Promise<CodexAppsDesignationForRun | null> {
|
|
1159
|
+
let owner = known.accountId;
|
|
1160
|
+
const accountId = async () =>
|
|
1161
|
+
(owner ??= (await rlsContextForWorkspace(db, workspaceId)).accountId);
|
|
1162
|
+
const disposition =
|
|
1163
|
+
known.disposition !== undefined
|
|
1164
|
+
? await known.disposition
|
|
1165
|
+
: await readCodexCutoverDisposition(db, await accountId(), workspaceId);
|
|
1166
|
+
if (disposition === "maintenance") return null;
|
|
1167
|
+
const coreAccountId = await accountId();
|
|
1168
|
+
const designation = await resolveSubscriptionCoreCodexAppsDesignation(db, {
|
|
1169
|
+
accountId: coreAccountId,
|
|
1170
|
+
workspaceId,
|
|
1171
|
+
});
|
|
1172
|
+
return designation?.status === "active"
|
|
1173
|
+
? { source: "core", accountId: coreAccountId, connectionId: designation.connectionId }
|
|
1174
|
+
: null;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/** Runtime Apps authentication for whichever designation `resolveCodexAppsDesignationForRun` returned. */
|
|
1178
|
+
export function codexAppsRequestAuthForDesignation(
|
|
1179
|
+
db: Database,
|
|
1180
|
+
settings: Settings,
|
|
1181
|
+
workspaceId: string,
|
|
1182
|
+
designation: CodexAppsDesignationForRun,
|
|
1183
|
+
): SubscriptionCoreCodexAppsRequestAuth {
|
|
1184
|
+
return subscriptionCoreCodexAppsRequestAuth(db, settings, {
|
|
1185
|
+
accountId: designation.accountId,
|
|
1186
|
+
workspaceId,
|
|
1187
|
+
connectionId: designation.connectionId,
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Resolve the legacy executable Apps authority. Organizations with a Codex
|
|
1193
|
+
* cutover row (enabled or disabled) have none here: their designation lives
|
|
1194
|
+
* on the core (`resolveCodexAppsDesignationForRun`).
|
|
847
1195
|
*/
|
|
848
1196
|
export async function resolveCodexAppsCredentialIdForRun(
|
|
849
1197
|
db: Database,
|
|
850
1198
|
workspaceId: string,
|
|
851
1199
|
): Promise<string | null> {
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
const grant = await getWorkspaceGrant(db, authorization.ownerSubjectId, workspaceId);
|
|
855
|
-
return grant && hasPermission(grant.permissions, "connections:write")
|
|
856
|
-
? authorization.credentialId
|
|
857
|
-
: null;
|
|
1200
|
+
const designation = await resolveCodexAppsDesignationForRun(db, workspaceId);
|
|
1201
|
+
return designation?.connectionId ?? null;
|
|
858
1202
|
}
|
|
859
1203
|
|
|
860
1204
|
/**
|
|
@@ -1055,56 +1399,10 @@ async function requireCatalogItem(
|
|
|
1055
1399
|
return item;
|
|
1056
1400
|
}
|
|
1057
1401
|
|
|
1058
|
-
function packCatalogItem(
|
|
1059
|
-
pack: ReturnType<typeof listCapabilityPacks>[number],
|
|
1060
|
-
source: "built_in" | "manual",
|
|
1061
|
-
): CapabilityCatalogItem {
|
|
1062
|
-
const customMetadata = { ...pack.metadata };
|
|
1063
|
-
for (const key of [
|
|
1064
|
-
"packId",
|
|
1065
|
-
"version",
|
|
1066
|
-
"connectors",
|
|
1067
|
-
"knowledge",
|
|
1068
|
-
"scheduledTaskTemplates",
|
|
1069
|
-
"sandboxImage",
|
|
1070
|
-
"sandboxProviderImages",
|
|
1071
|
-
"skills",
|
|
1072
|
-
]) {
|
|
1073
|
-
delete customMetadata[key];
|
|
1074
|
-
}
|
|
1075
|
-
return CapabilityCatalogItem.parse({
|
|
1076
|
-
id: `pack:${pack.id}`,
|
|
1077
|
-
kind: "pack",
|
|
1078
|
-
source,
|
|
1079
|
-
name: pack.name,
|
|
1080
|
-
description: pack.description,
|
|
1081
|
-
category: pack.category,
|
|
1082
|
-
tags: [pack.role, pack.category, "pack"],
|
|
1083
|
-
tools: pack.tools,
|
|
1084
|
-
runtime: {
|
|
1085
|
-
available: true,
|
|
1086
|
-
notes: "Enables role-scoped tools, connectors, knowledge, and scheduled-task templates.",
|
|
1087
|
-
},
|
|
1088
|
-
metadata: {
|
|
1089
|
-
...customMetadata,
|
|
1090
|
-
packId: pack.id,
|
|
1091
|
-
version: pack.version,
|
|
1092
|
-
connectors: pack.connectors,
|
|
1093
|
-
knowledge: pack.knowledge,
|
|
1094
|
-
scheduledTaskTemplates: pack.scheduledTaskTemplates,
|
|
1095
|
-
...(pack.variableSet ? { variableSet: pack.variableSet } : {}),
|
|
1096
|
-
// Runtime composition surface only: skill names, never file content.
|
|
1097
|
-
...(pack.sandboxImage ? { sandboxImage: pack.sandboxImage } : {}),
|
|
1098
|
-
...(pack.sandboxProviderImages ? { sandboxProviderImages: pack.sandboxProviderImages } : {}),
|
|
1099
|
-
...(pack.skills.length > 0 ? { skills: pack.skills.map((skill) => skill.name) } : {}),
|
|
1100
|
-
},
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
1402
|
function configuredMcpCatalogItems(settings: Settings): CapabilityCatalogItem[] {
|
|
1105
1403
|
return (
|
|
1106
1404
|
settings.mcpServers
|
|
1107
|
-
//
|
|
1405
|
+
// Opengeni, Files, and Document Search are native runtime surfaces. They
|
|
1108
1406
|
// remain available to sessions through configuration, but are not things a
|
|
1109
1407
|
// user installs, connects, or enables in the Capabilities control center.
|
|
1110
1408
|
.filter(
|
|
@@ -1121,12 +1419,23 @@ function configuredMcpCatalogItems(settings: Settings): CapabilityCatalogItem[]
|
|
|
1121
1419
|
category: "configured",
|
|
1122
1420
|
tags: ["mcp", ...(server.allowedTools?.length ? ["limited-tools"] : [])],
|
|
1123
1421
|
endpointUrl: server.url,
|
|
1422
|
+
authKind:
|
|
1423
|
+
server.connectionRef?.authoritySource !== "host" &&
|
|
1424
|
+
server.connectionRef?.kind === "oauth2"
|
|
1425
|
+
? "oauth2"
|
|
1426
|
+
: null,
|
|
1427
|
+
// Deployment-managed personal selectors need the same account picker
|
|
1428
|
+
// as installed connectors. Fixed bindings remain server-resolved.
|
|
1429
|
+
connectionRef:
|
|
1430
|
+
server.connectionRef?.subjectScope === "subject" && !server.connectionRef.connectionId
|
|
1431
|
+
? installationConnectionRef({ connectionRef: server.connectionRef })
|
|
1432
|
+
: null,
|
|
1124
1433
|
tools: [{ kind: "mcp", id: server.id }],
|
|
1125
1434
|
runtime: {
|
|
1126
1435
|
available: true,
|
|
1127
1436
|
mcpServerId: server.id,
|
|
1128
1437
|
transport: "streamable-http",
|
|
1129
|
-
notes: "Managed by this
|
|
1438
|
+
notes: "Managed by this Opengeni deployment through OPENGENI_MCP_SERVERS.",
|
|
1130
1439
|
},
|
|
1131
1440
|
metadata: {
|
|
1132
1441
|
mcpServerId: server.id,
|
|
@@ -1262,7 +1571,7 @@ function fikenCatalogItem(fikenConnections: ConnectionMetadata[]): CapabilityCat
|
|
|
1262
1571
|
runtime: {
|
|
1263
1572
|
available: true,
|
|
1264
1573
|
mcpServerId: "opengeni",
|
|
1265
|
-
notes: "Fiken access is provided through
|
|
1574
|
+
notes: "Fiken access is provided through Opengeni's first-party fiken tools.",
|
|
1266
1575
|
},
|
|
1267
1576
|
enabled: fikenEnabled,
|
|
1268
1577
|
enabledReason: fikenEnabled
|
|
@@ -1273,6 +1582,7 @@ function fikenCatalogItem(fikenConnections: ConnectionMetadata[]): CapabilityCat
|
|
|
1273
1582
|
metadata: {
|
|
1274
1583
|
connectorMode: "first_party_fiken",
|
|
1275
1584
|
ownership: "workspace",
|
|
1585
|
+
connectionStatus: fikenConnection?.status ?? null,
|
|
1276
1586
|
// Derived from the contracts catalog so a new fiken_* tool cannot be
|
|
1277
1587
|
// registered without also appearing on the capability tile.
|
|
1278
1588
|
firstPartyMcpTools: FIRST_PARTY_MCP_TOOL_NAMES.filter((name) => name.startsWith("fiken_")),
|
|
@@ -1280,6 +1590,52 @@ function fikenCatalogItem(fikenConnections: ConnectionMetadata[]): CapabilityCat
|
|
|
1280
1590
|
});
|
|
1281
1591
|
}
|
|
1282
1592
|
|
|
1593
|
+
/**
|
|
1594
|
+
* The 1Password tile (design §10.9). Connecting a service account happens in
|
|
1595
|
+
* Settings > Secrets (`POST /connections/1password/install`); its vaults are
|
|
1596
|
+
* then reached through secret references and the secrets tools, not MCP.
|
|
1597
|
+
* Enabled while a workspace-shared 1Password connection is active.
|
|
1598
|
+
*/
|
|
1599
|
+
function onePasswordCatalogItem(connections: ConnectionMetadata[]): CapabilityCatalogItem {
|
|
1600
|
+
const active = connections.filter((connection) => connection.status === "active");
|
|
1601
|
+
const enabled = active.length > 0;
|
|
1602
|
+
return CapabilityCatalogItem.parse({
|
|
1603
|
+
id: "api:1password",
|
|
1604
|
+
kind: "api",
|
|
1605
|
+
source: "built_in",
|
|
1606
|
+
name: "1Password",
|
|
1607
|
+
description:
|
|
1608
|
+
"Connect a 1Password service account so its vaults appear as read-only or read-write secret vaults.",
|
|
1609
|
+
category: "security",
|
|
1610
|
+
tags: ["api", "1password", "password-manager", "secrets", "vaults"],
|
|
1611
|
+
homepageUrl: "https://1password.com",
|
|
1612
|
+
authModel: "service_account_token",
|
|
1613
|
+
providerDomain: "1password.com",
|
|
1614
|
+
surfaceType: "first_party_onepassword",
|
|
1615
|
+
authKind: "api_key",
|
|
1616
|
+
tools: [],
|
|
1617
|
+
runtime: {
|
|
1618
|
+
available: true,
|
|
1619
|
+
notes: "1Password vaults are used through secret references and the secrets tools.",
|
|
1620
|
+
},
|
|
1621
|
+
enabled,
|
|
1622
|
+
enabledReason: enabled
|
|
1623
|
+
? `${active.length === 1 ? "1Password connection" : `${active.length} 1Password connections`} active`
|
|
1624
|
+
: null,
|
|
1625
|
+
metadata: {
|
|
1626
|
+
connectorMode: "first_party_onepassword",
|
|
1627
|
+
connectionCount: connections.length,
|
|
1628
|
+
planTiers: [
|
|
1629
|
+
...new Set(
|
|
1630
|
+
connections.map(
|
|
1631
|
+
(connection) => onePasswordConnectionMetadata(connection.metadata)?.planTier,
|
|
1632
|
+
),
|
|
1633
|
+
),
|
|
1634
|
+
].filter(Boolean),
|
|
1635
|
+
},
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1283
1639
|
function providerIntegrationCatalogItems(
|
|
1284
1640
|
socialConnections: SocialConnection[],
|
|
1285
1641
|
): CapabilityCatalogItem[] {
|
|
@@ -1304,11 +1660,11 @@ function providerIntegrationCatalogItems(
|
|
|
1304
1660
|
available: true,
|
|
1305
1661
|
mcpServerId: "opengeni",
|
|
1306
1662
|
notes:
|
|
1307
|
-
"
|
|
1663
|
+
"Opengeni's social provider adapter routes every call through an exact visible account Connection.",
|
|
1308
1664
|
},
|
|
1309
1665
|
enabled,
|
|
1310
1666
|
enabledReason: socialConnectionSummary(counts),
|
|
1311
|
-
provenance: "
|
|
1667
|
+
provenance: "Opengeni provider adapter",
|
|
1312
1668
|
metadata: {
|
|
1313
1669
|
providerAdapter: "social",
|
|
1314
1670
|
provider: definition.provider,
|
|
@@ -1326,14 +1682,21 @@ function providerIntegrationCatalogItems(
|
|
|
1326
1682
|
* recommend the owning product flow without manufacturing an enabled catalog
|
|
1327
1683
|
* row for GitHub resources, Documents, schedules, or other platform features.
|
|
1328
1684
|
*/
|
|
1329
|
-
|
|
1330
|
-
|
|
1685
|
+
/** The in-chat card id for connecting a person's own computer. */
|
|
1686
|
+
export const CONNECTED_MACHINE_CAPABILITY_ID = "api:connected-machine";
|
|
1687
|
+
|
|
1688
|
+
export { OPENGENI_BROWSER_EXTENSION_URL };
|
|
1689
|
+
|
|
1690
|
+
export function nativeConnectionCapabilityRecommendations(
|
|
1691
|
+
options: { connectedMachines?: boolean } = {},
|
|
1692
|
+
): CapabilityCatalogItem[] {
|
|
1693
|
+
const items = [
|
|
1331
1694
|
CapabilityCatalogItem.parse({
|
|
1332
1695
|
id: "api:github-app",
|
|
1333
1696
|
kind: "api",
|
|
1334
1697
|
source: "built_in",
|
|
1335
1698
|
name: "GitHub App",
|
|
1336
|
-
description: "Connect repositories through
|
|
1699
|
+
description: "Connect repositories through Opengeni's GitHub resource picker.",
|
|
1337
1700
|
category: "source-control",
|
|
1338
1701
|
tags: ["github", "repositories", "source-control"],
|
|
1339
1702
|
homepageUrl: "https://github.com",
|
|
@@ -1361,6 +1724,66 @@ export function nativeConnectionCapabilityRecommendations(): CapabilityCatalogIt
|
|
|
1361
1724
|
},
|
|
1362
1725
|
}),
|
|
1363
1726
|
];
|
|
1727
|
+
if (options.connectedMachines) {
|
|
1728
|
+
items.push(
|
|
1729
|
+
CapabilityCatalogItem.parse({
|
|
1730
|
+
id: CONNECTED_MACHINE_CAPABILITY_ID,
|
|
1731
|
+
kind: "api",
|
|
1732
|
+
source: "built_in",
|
|
1733
|
+
name: "Connected Machine",
|
|
1734
|
+
description:
|
|
1735
|
+
"Run this chat on the person's own computer or server (Mac, Linux or Windows), and optionally let agents use their Chrome through the Opengeni Browser extension.",
|
|
1736
|
+
category: "compute",
|
|
1737
|
+
tags: [
|
|
1738
|
+
"machine",
|
|
1739
|
+
"computer",
|
|
1740
|
+
"laptop",
|
|
1741
|
+
"desktop",
|
|
1742
|
+
"mac",
|
|
1743
|
+
"macbook",
|
|
1744
|
+
"linux",
|
|
1745
|
+
"windows",
|
|
1746
|
+
"server",
|
|
1747
|
+
"gpu",
|
|
1748
|
+
"local",
|
|
1749
|
+
"self-hosted",
|
|
1750
|
+
"chrome",
|
|
1751
|
+
"browser",
|
|
1752
|
+
"extension",
|
|
1753
|
+
"terminal",
|
|
1754
|
+
],
|
|
1755
|
+
homepageUrl: null,
|
|
1756
|
+
providerDomain: "opengeni.ai",
|
|
1757
|
+
authModel: "connected_machine_enrollment",
|
|
1758
|
+
authKind: "none",
|
|
1759
|
+
surfaceType: "first_party_connected_machine",
|
|
1760
|
+
tools: [{ kind: "mcp", id: "opengeni" }],
|
|
1761
|
+
runtime: {
|
|
1762
|
+
available: true,
|
|
1763
|
+
notes: `The person runs a one-line connect command on their machine from the chat card. The Opengeni Browser Chrome extension (${OPENGENI_BROWSER_EXTENSION_URL}) works only after the machine is connected.`,
|
|
1764
|
+
},
|
|
1765
|
+
lifecycle: {
|
|
1766
|
+
status: "available",
|
|
1767
|
+
readiness: "setup_required",
|
|
1768
|
+
detail: "Connect a machine from the chat card.",
|
|
1769
|
+
managedBy: "platform",
|
|
1770
|
+
},
|
|
1771
|
+
actions: ["connect", "inspect"],
|
|
1772
|
+
metadata: {
|
|
1773
|
+
firstPartyMcpTools: [
|
|
1774
|
+
"sandboxes_list",
|
|
1775
|
+
"sandbox_swap",
|
|
1776
|
+
"run_on",
|
|
1777
|
+
"connected_machine_enroll_token",
|
|
1778
|
+
"connected_machine_enable_screen_control",
|
|
1779
|
+
],
|
|
1780
|
+
browserExtensionUrl: OPENGENI_BROWSER_EXTENSION_URL,
|
|
1781
|
+
recommendationOnly: true,
|
|
1782
|
+
},
|
|
1783
|
+
}),
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
return items;
|
|
1364
1787
|
}
|
|
1365
1788
|
|
|
1366
1789
|
function socialConnectionCounts(
|
|
@@ -1521,31 +1944,21 @@ function socialProviderConnectionCounts(item: CapabilityCatalogItem): {
|
|
|
1521
1944
|
export function applyCapabilityEnablement(
|
|
1522
1945
|
item: CapabilityCatalogItem,
|
|
1523
1946
|
installation: CapabilityInstallation | undefined,
|
|
1524
|
-
activePackIds: Set<string>,
|
|
1525
1947
|
): CapabilityCatalogItem {
|
|
1948
|
+
if (item.surfaceType === "first_party_fiken" || item.surfaceType === "first_party_onepassword") {
|
|
1949
|
+
// Fiken and 1Password are API items with connection-derived state, not
|
|
1950
|
+
// generic API installations. Preserve that state before the non-MCP fallback below.
|
|
1951
|
+
return { ...item, connectionRef: null };
|
|
1952
|
+
}
|
|
1526
1953
|
if (item.kind === "skill" || item.kind === "api" || item.kind === "plugin") {
|
|
1527
1954
|
return { ...item, enabled: false, enabledReason: null, connectionRef: null };
|
|
1528
1955
|
}
|
|
1529
|
-
if (item.kind === "pack") {
|
|
1530
|
-
const enabled = activePackIds.has(packIdFromCapabilityId(item.id));
|
|
1531
|
-
return {
|
|
1532
|
-
...item,
|
|
1533
|
-
enabled,
|
|
1534
|
-
enabledReason: enabled ? "enabled" : null,
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
1956
|
if (isSocialProviderIntegration(item)) {
|
|
1538
1957
|
// Provider-integration state is derived from every authoritative visible
|
|
1539
1958
|
// social Connection while the catalog is built. The catalog summary never
|
|
1540
1959
|
// publishes a personal connection UUID or collapses many accounts to one.
|
|
1541
1960
|
return { ...item, connectionRef: null };
|
|
1542
1961
|
}
|
|
1543
|
-
if (item.surfaceType === "first_party_fiken") {
|
|
1544
|
-
// Fiken connector state is derived from the authoritative workspace
|
|
1545
|
-
// connection row while the catalog is built; browseable never means an
|
|
1546
|
-
// account is already connected.
|
|
1547
|
-
return { ...item, connectionRef: null };
|
|
1548
|
-
}
|
|
1549
1962
|
if (item.surfaceType === "codex_apps") {
|
|
1550
1963
|
// Unlike ordinary built-ins, Apps availability is derived from the exact
|
|
1551
1964
|
// active workspace designation above. Do not overwrite it with the
|
|
@@ -1557,7 +1970,7 @@ export function applyCapabilityEnablement(
|
|
|
1557
1970
|
...item,
|
|
1558
1971
|
enabled: true,
|
|
1559
1972
|
enabledReason: "managed by deployment",
|
|
1560
|
-
connectionRef:
|
|
1973
|
+
connectionRef: item.connectionRef,
|
|
1561
1974
|
};
|
|
1562
1975
|
}
|
|
1563
1976
|
if (item.source === "built_in") {
|
|
@@ -1568,11 +1981,17 @@ export function applyCapabilityEnablement(
|
|
|
1568
1981
|
}
|
|
1569
1982
|
const activeInstallation = installation?.status === "active";
|
|
1570
1983
|
const enabled = !!activeInstallation && capabilityInstallationRuntimeReady(item, installation);
|
|
1984
|
+
const connectionRef =
|
|
1985
|
+
enabled && installation ? installationConnectionRef(installation.config) : null;
|
|
1571
1986
|
return {
|
|
1572
1987
|
...item,
|
|
1988
|
+
// Older custom entries recorded the native credential kind on the
|
|
1989
|
+
// installation only. Project it without guessing from a URL or overwriting
|
|
1990
|
+
// an explicit catalog authentication contract.
|
|
1991
|
+
authKind: item.authKind ?? (connectionRef?.kind === "oauth2" ? "oauth2" : null),
|
|
1573
1992
|
enabled,
|
|
1574
1993
|
enabledReason: enabled ? "enabled" : null,
|
|
1575
|
-
connectionRef
|
|
1994
|
+
connectionRef,
|
|
1576
1995
|
};
|
|
1577
1996
|
}
|
|
1578
1997
|
|
|
@@ -1599,7 +2018,10 @@ function applyCapabilityLifecycle(item: CapabilityCatalogItem): CapabilityCatalo
|
|
|
1599
2018
|
detail: item.runtime.notes,
|
|
1600
2019
|
managedBy: item.source === "built_in" ? "platform" : null,
|
|
1601
2020
|
},
|
|
1602
|
-
actions:
|
|
2021
|
+
actions:
|
|
2022
|
+
item.enabled && item.kind === "mcp" && item.source !== "built_in"
|
|
2023
|
+
? ["disconnect", "inspect"]
|
|
2024
|
+
: ["inspect"],
|
|
1603
2025
|
};
|
|
1604
2026
|
}
|
|
1605
2027
|
|
|
@@ -1638,7 +2060,7 @@ function applyCapabilityLifecycle(item: CapabilityCatalogItem): CapabilityCatalo
|
|
|
1638
2060
|
|
|
1639
2061
|
const installed = item.enabled;
|
|
1640
2062
|
const actions: CapabilityAction[] = installed
|
|
1641
|
-
? item.kind === "
|
|
2063
|
+
? item.kind === "skill" || item.kind === "plugin"
|
|
1642
2064
|
? ["configure", "update", "uninstall", "inspect"]
|
|
1643
2065
|
: ["configure", "disconnect", "inspect"]
|
|
1644
2066
|
: item.kind === "mcp" || item.kind === "api"
|
|
@@ -1701,24 +2123,34 @@ function installationConnectionRef(
|
|
|
1701
2123
|
if (!ref || typeof ref !== "object") {
|
|
1702
2124
|
return null;
|
|
1703
2125
|
}
|
|
1704
|
-
const { authoritySource, connectionId, providerDomain, kind, subjectScope } =
|
|
1705
|
-
string,
|
|
1706
|
-
unknown
|
|
1707
|
-
>;
|
|
2126
|
+
const { authoritySource, connectionId, providerDomain, kind, subjectScope, accountSelection } =
|
|
2127
|
+
ref as Record<string, unknown>;
|
|
1708
2128
|
if (typeof providerDomain !== "string" || typeof kind !== "string") {
|
|
1709
2129
|
return null;
|
|
1710
2130
|
}
|
|
1711
2131
|
if (authoritySource === "host") {
|
|
1712
2132
|
// The internal installation/runtime ref retains the exact host binding.
|
|
1713
2133
|
// Public capability catalogs use the existing null representation for an
|
|
1714
|
-
// enabled capability without a native
|
|
2134
|
+
// enabled capability without a native Opengeni connection, so indefinitely
|
|
1715
2135
|
// open old browser bundles cannot treat a host UUID as native OAuth state.
|
|
1716
2136
|
return null;
|
|
1717
2137
|
}
|
|
2138
|
+
if (accountSelection === "all_eligible" && connectionId === undefined) {
|
|
2139
|
+
return {
|
|
2140
|
+
providerDomain,
|
|
2141
|
+
kind,
|
|
2142
|
+
accountSelection,
|
|
2143
|
+
...(subjectScope === "subject" ? { subjectScope } : {}),
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
1718
2146
|
if (subjectScope === "subject") {
|
|
1719
2147
|
// Never project a native personal connection UUID through workspace-visible
|
|
1720
2148
|
// capability configuration, including legacy rows that still contain one.
|
|
1721
|
-
return {
|
|
2149
|
+
return {
|
|
2150
|
+
providerDomain,
|
|
2151
|
+
kind,
|
|
2152
|
+
subjectScope: "subject",
|
|
2153
|
+
};
|
|
1722
2154
|
}
|
|
1723
2155
|
if (typeof connectionId !== "string") {
|
|
1724
2156
|
return null;
|
|
@@ -1753,13 +2185,42 @@ export type CapabilityCatalogSearchMatch = {
|
|
|
1753
2185
|
item: CapabilityCatalogItem;
|
|
1754
2186
|
score: number;
|
|
1755
2187
|
matchedOn: Array<"name" | "provider" | "tag" | "category" | "description" | "id">;
|
|
2188
|
+
/**
|
|
2189
|
+
* True when the item matched only through the typo-tolerant tier (an edit
|
|
2190
|
+
* distance hit), never through an exact, prefix, or substring hit.
|
|
2191
|
+
*/
|
|
2192
|
+
approximate: boolean;
|
|
2193
|
+
};
|
|
2194
|
+
|
|
2195
|
+
export type CapabilityCatalogSuggestion = {
|
|
2196
|
+
item: CapabilityCatalogItem;
|
|
2197
|
+
/** Name similarity to the query in [0, 1]; 1 means identical once normalized. */
|
|
2198
|
+
similarity: number;
|
|
1756
2199
|
};
|
|
1757
2200
|
|
|
2201
|
+
// Typo tolerance stays deterministic and bounded: tokens shorter than four
|
|
2202
|
+
// characters never fuzz, compared strings are capped, and only identity fields
|
|
2203
|
+
// (name, provider domain, tags) participate in the fuzzy tier.
|
|
2204
|
+
const FUZZY_MIN_TOKEN_LENGTH = 4;
|
|
2205
|
+
const FUZZY_PREFIX_MIN_TOKEN_LENGTH = 5;
|
|
2206
|
+
const FUZZY_MAX_COMPARED_LENGTH = 48;
|
|
2207
|
+
const FUZZY_MAX_TOKENS = 8;
|
|
2208
|
+
const FUZZY_FIELDS = new Set<CapabilityCatalogSearchMatch["matchedOn"][number]>([
|
|
2209
|
+
"name",
|
|
2210
|
+
"provider",
|
|
2211
|
+
"tag",
|
|
2212
|
+
]);
|
|
2213
|
+
|
|
1758
2214
|
/**
|
|
1759
2215
|
* Deterministically rank the already-merged workspace catalog for an agent.
|
|
1760
2216
|
* The caller still owns live authorization checks (GitHub binding, OAuth row,
|
|
1761
2217
|
* and so on); this function searches metadata only and never probes a provider
|
|
1762
2218
|
* or exposes credential/setup prose to the model.
|
|
2219
|
+
*
|
|
2220
|
+
* Exact word, prefix, and substring hits rank first. A typo-tolerant tier
|
|
2221
|
+
* (bounded Damerau-Levenshtein on name, provider-domain, and tag words, plus
|
|
2222
|
+
* the space-stripped name against the space-stripped query) contributes a
|
|
2223
|
+
* smaller score so a one-letter spelling difference still finds the entry.
|
|
1763
2224
|
*/
|
|
1764
2225
|
export function searchCapabilityCatalogItems(
|
|
1765
2226
|
items: readonly CapabilityCatalogItem[],
|
|
@@ -1769,11 +2230,13 @@ export function searchCapabilityCatalogItems(
|
|
|
1769
2230
|
const phrase = normalizeCapabilitySearchText(query);
|
|
1770
2231
|
const tokens = [...new Set(phrase.split(" ").filter(Boolean))].slice(0, 24);
|
|
1771
2232
|
if (tokens.length === 0) return [];
|
|
2233
|
+
const compactPhrase = phrase.replaceAll(" ", "");
|
|
2234
|
+
const fuzzyTokens = tokens.slice(0, FUZZY_MAX_TOKENS);
|
|
1772
2235
|
const boundedLimit = Math.min(20, Math.max(1, Math.trunc(limit)));
|
|
1773
2236
|
const weightedFields = (item: CapabilityCatalogItem) =>
|
|
1774
2237
|
[
|
|
1775
2238
|
["name", item.name, 100],
|
|
1776
|
-
["provider", item
|
|
2239
|
+
["provider", capabilityCatalogSearchProviderDomain(item) ?? "", 80],
|
|
1777
2240
|
["tag", item.tags.join(" "), 50],
|
|
1778
2241
|
["category", item.category, 30],
|
|
1779
2242
|
["id", item.id, 25],
|
|
@@ -1784,25 +2247,54 @@ export function searchCapabilityCatalogItems(
|
|
|
1784
2247
|
.filter((item) => capabilityCatalogItemIsTrustedForExposure(item))
|
|
1785
2248
|
.flatMap((item): CapabilityCatalogSearchMatch[] => {
|
|
1786
2249
|
let score = 0;
|
|
2250
|
+
let approximate = true;
|
|
1787
2251
|
const matchedOn = new Set<CapabilityCatalogSearchMatch["matchedOn"][number]>();
|
|
2252
|
+
const hit = (
|
|
2253
|
+
field: CapabilityCatalogSearchMatch["matchedOn"][number],
|
|
2254
|
+
points: number,
|
|
2255
|
+
fuzzy = false,
|
|
2256
|
+
) => {
|
|
2257
|
+
score += points;
|
|
2258
|
+
matchedOn.add(field);
|
|
2259
|
+
if (!fuzzy) approximate = false;
|
|
2260
|
+
};
|
|
1788
2261
|
for (const [field, raw, weight] of weightedFields(item)) {
|
|
1789
2262
|
const value = normalizeCapabilitySearchText(raw);
|
|
1790
2263
|
if (!value) continue;
|
|
1791
2264
|
const words = value.split(" ");
|
|
2265
|
+
const fuzzyField = FUZZY_FIELDS.has(field);
|
|
1792
2266
|
if (value.includes(phrase)) {
|
|
1793
|
-
|
|
1794
|
-
|
|
2267
|
+
hit(field, Math.round(weight * 1.5));
|
|
2268
|
+
} else if (fuzzyField && compactPhrase.length >= FUZZY_MIN_TOKEN_LENGTH) {
|
|
2269
|
+
// Compare with separators removed so "wispr flow" meets "WisprFlow"
|
|
2270
|
+
// and "wisprflow" meets "Wispr Flow"; a near miss is typo-tolerant.
|
|
2271
|
+
const compactValue = value.replaceAll(" ", "");
|
|
2272
|
+
const budget = fuzzyEditBudget(compactPhrase.length);
|
|
2273
|
+
if (compactValue.includes(compactPhrase)) {
|
|
2274
|
+
hit(field, weight);
|
|
2275
|
+
} else {
|
|
2276
|
+
const distance = boundedEditDistance(compactPhrase, compactValue, budget);
|
|
2277
|
+
if (distance.whole <= budget) {
|
|
2278
|
+
hit(field, Math.round(weight * 0.5), true);
|
|
2279
|
+
} else if (
|
|
2280
|
+
compactPhrase.length >= FUZZY_PREFIX_MIN_TOKEN_LENGTH &&
|
|
2281
|
+
distance.prefix <= budget
|
|
2282
|
+
) {
|
|
2283
|
+
hit(field, Math.round(weight * 0.3), true);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
1795
2286
|
}
|
|
1796
2287
|
for (const token of tokens) {
|
|
1797
2288
|
if (words.includes(token)) {
|
|
1798
|
-
|
|
1799
|
-
matchedOn.add(field);
|
|
2289
|
+
hit(field, weight);
|
|
1800
2290
|
} else if (words.some((word) => word.startsWith(token) || token.startsWith(word))) {
|
|
1801
|
-
|
|
1802
|
-
matchedOn.add(field);
|
|
2291
|
+
hit(field, Math.round(weight * 0.7));
|
|
1803
2292
|
} else if (value.includes(token)) {
|
|
1804
|
-
|
|
1805
|
-
|
|
2293
|
+
hit(field, Math.round(weight * 0.4));
|
|
2294
|
+
} else if (fuzzyField && fuzzyTokens.includes(token)) {
|
|
2295
|
+
const fuzzy = fuzzyWordMatch(token, words);
|
|
2296
|
+
if (fuzzy === "word") hit(field, Math.round(weight * 0.3), true);
|
|
2297
|
+
else if (fuzzy === "prefix") hit(field, Math.round(weight * 0.2), true);
|
|
1806
2298
|
}
|
|
1807
2299
|
}
|
|
1808
2300
|
}
|
|
@@ -1810,7 +2302,7 @@ export function searchCapabilityCatalogItems(
|
|
|
1810
2302
|
if (item.enabled) score += 12;
|
|
1811
2303
|
if (item.tier === "verified") score += 8;
|
|
1812
2304
|
if (item.source === "built_in") score += 6;
|
|
1813
|
-
return [{ item, score, matchedOn: [...matchedOn] }];
|
|
2305
|
+
return [{ item, score, matchedOn: [...matchedOn], approximate }];
|
|
1814
2306
|
})
|
|
1815
2307
|
.sort(
|
|
1816
2308
|
(left, right) =>
|
|
@@ -1822,6 +2314,167 @@ export function searchCapabilityCatalogItems(
|
|
|
1822
2314
|
.slice(0, boundedLimit);
|
|
1823
2315
|
}
|
|
1824
2316
|
|
|
2317
|
+
/**
|
|
2318
|
+
* Closest catalog names for a query that matched nothing. A miss must never
|
|
2319
|
+
* read as "this integration does not exist" while the workspace has entries,
|
|
2320
|
+
* so callers surface these as explicitly labelled suggestions. An empty
|
|
2321
|
+
* (all-punctuation) query returns the first entries by name for browsing.
|
|
2322
|
+
*/
|
|
2323
|
+
export function suggestCapabilityCatalogItems(
|
|
2324
|
+
items: readonly CapabilityCatalogItem[],
|
|
2325
|
+
query: string,
|
|
2326
|
+
limit = 5,
|
|
2327
|
+
): CapabilityCatalogSuggestion[] {
|
|
2328
|
+
const boundedLimit = Math.min(20, Math.max(1, Math.trunc(limit)));
|
|
2329
|
+
const compactPhrase = normalizeCapabilitySearchText(query)
|
|
2330
|
+
.replaceAll(" ", "")
|
|
2331
|
+
.slice(0, FUZZY_MAX_COMPARED_LENGTH);
|
|
2332
|
+
return items
|
|
2333
|
+
.filter((item) => capabilityCatalogItemIsTrustedForExposure(item))
|
|
2334
|
+
.map((item): CapabilityCatalogSuggestion => {
|
|
2335
|
+
if (!compactPhrase) return { item, similarity: 0 };
|
|
2336
|
+
let best = 0;
|
|
2337
|
+
for (const raw of [item.name, capabilityCatalogSearchProviderDomain(item) ?? ""]) {
|
|
2338
|
+
const candidate = normalizeCapabilitySearchText(raw)
|
|
2339
|
+
.replaceAll(" ", "")
|
|
2340
|
+
.slice(0, FUZZY_MAX_COMPARED_LENGTH);
|
|
2341
|
+
if (!candidate) continue;
|
|
2342
|
+
const distance = boundedEditDistance(compactPhrase, candidate, FUZZY_MAX_COMPARED_LENGTH);
|
|
2343
|
+
best = Math.max(
|
|
2344
|
+
best,
|
|
2345
|
+
1 - distance.whole / Math.max(compactPhrase.length, candidate.length),
|
|
2346
|
+
// A query that resembles the start of a longer name ("wspr" for
|
|
2347
|
+
// "Whisprflow") is as informative as a whole-name resemblance.
|
|
2348
|
+
1 - distance.prefix / compactPhrase.length,
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
return { item, similarity: Math.round(Math.max(0, best) * 100) / 100 };
|
|
2352
|
+
})
|
|
2353
|
+
.sort(
|
|
2354
|
+
(left, right) =>
|
|
2355
|
+
right.similarity - left.similarity ||
|
|
2356
|
+
Number(right.item.enabled) - Number(left.item.enabled) ||
|
|
2357
|
+
left.item.name.localeCompare(right.item.name) ||
|
|
2358
|
+
left.item.id.localeCompare(right.item.id),
|
|
2359
|
+
)
|
|
2360
|
+
.slice(0, boundedLimit);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
/**
|
|
2364
|
+
* The vendor domain search may match. A stored providerDomain wins. Workspace
|
|
2365
|
+
* and operator entries (manual, configured, built-in) without one fall back to
|
|
2366
|
+
* the registrable domain of their endpoint (api.wisprflow.ai -> wisprflow.ai).
|
|
2367
|
+
* Registry rows do not: their endpoints may sit on a third-party host. This is
|
|
2368
|
+
* a search-only projection and never changes the stored item or any
|
|
2369
|
+
* connection/authority matching.
|
|
2370
|
+
*/
|
|
2371
|
+
export function capabilityCatalogSearchProviderDomain(item: CapabilityCatalogItem): string | null {
|
|
2372
|
+
const stored = item.providerDomain?.trim();
|
|
2373
|
+
if (stored) return stored;
|
|
2374
|
+
if (item.source === "registry" || item.source === "public_registry") return null;
|
|
2375
|
+
for (const candidate of [item.endpointUrl, item.mcpUrl, item.homepageUrl]) {
|
|
2376
|
+
if (!candidate) continue;
|
|
2377
|
+
const domain = registrableDomain(candidate);
|
|
2378
|
+
if (domain) return domain;
|
|
2379
|
+
}
|
|
2380
|
+
return null;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
// Common two-label public suffixes. Without a full public-suffix list this
|
|
2384
|
+
// keeps hosts such as api.example.co.uk on example.co.uk rather than co.uk.
|
|
2385
|
+
const TWO_LABEL_PUBLIC_SUFFIXES = new Set([
|
|
2386
|
+
"ac.uk",
|
|
2387
|
+
"co.uk",
|
|
2388
|
+
"gov.uk",
|
|
2389
|
+
"org.uk",
|
|
2390
|
+
"com.au",
|
|
2391
|
+
"net.au",
|
|
2392
|
+
"org.au",
|
|
2393
|
+
"co.nz",
|
|
2394
|
+
"co.jp",
|
|
2395
|
+
"co.kr",
|
|
2396
|
+
"co.in",
|
|
2397
|
+
"co.za",
|
|
2398
|
+
"com.br",
|
|
2399
|
+
"com.cn",
|
|
2400
|
+
"com.mx",
|
|
2401
|
+
"com.sg",
|
|
2402
|
+
"com.tr",
|
|
2403
|
+
]);
|
|
2404
|
+
|
|
2405
|
+
function registrableDomain(url: string): string | null {
|
|
2406
|
+
let hostname: string;
|
|
2407
|
+
try {
|
|
2408
|
+
hostname = new URL(url).hostname.toLowerCase().replace(/\.$/, "");
|
|
2409
|
+
} catch {
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
if (!hostname || hostname.includes(":") || /^\d+(\.\d+){3}$/.test(hostname)) return null;
|
|
2413
|
+
const labels = hostname.split(".").filter(Boolean);
|
|
2414
|
+
if (labels.length <= 2) return labels.join(".") || null;
|
|
2415
|
+
const lastTwo = labels.slice(-2).join(".");
|
|
2416
|
+
return TWO_LABEL_PUBLIC_SUFFIXES.has(lastTwo) ? labels.slice(-3).join(".") : lastTwo;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function fuzzyEditBudget(length: number): number {
|
|
2420
|
+
if (length < FUZZY_MIN_TOKEN_LENGTH) return 0;
|
|
2421
|
+
return length >= 8 ? 2 : 1;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
function fuzzyWordMatch(token: string, words: readonly string[]): "word" | "prefix" | null {
|
|
2425
|
+
const budget = fuzzyEditBudget(token.length);
|
|
2426
|
+
if (budget === 0 || token.length > FUZZY_MAX_COMPARED_LENGTH) return null;
|
|
2427
|
+
let prefixHit = false;
|
|
2428
|
+
for (const word of words) {
|
|
2429
|
+
const lengthGap = Math.abs(word.length - token.length);
|
|
2430
|
+
const prefixCandidate =
|
|
2431
|
+
token.length >= FUZZY_PREFIX_MIN_TOKEN_LENGTH && word.length > token.length;
|
|
2432
|
+
if (lengthGap > budget && !prefixCandidate) continue;
|
|
2433
|
+
const distance = boundedEditDistance(token, word, budget);
|
|
2434
|
+
if (lengthGap <= budget && distance.whole <= budget) return "word";
|
|
2435
|
+
if (prefixCandidate && distance.prefix <= budget) prefixHit = true;
|
|
2436
|
+
}
|
|
2437
|
+
return prefixHit ? "prefix" : null;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
/**
|
|
2441
|
+
* Optimal-string-alignment (restricted Damerau-Levenshtein) distance from
|
|
2442
|
+
* `source` to all of `target` (`whole`) and to its closest prefix (`prefix`).
|
|
2443
|
+
* Both inputs are capped at FUZZY_MAX_COMPARED_LENGTH, and the computation
|
|
2444
|
+
* stops early once every cell of a row exceeds `budget`, returning budget + 1.
|
|
2445
|
+
*/
|
|
2446
|
+
function boundedEditDistance(
|
|
2447
|
+
source: string,
|
|
2448
|
+
target: string,
|
|
2449
|
+
budget: number,
|
|
2450
|
+
): { whole: number; prefix: number } {
|
|
2451
|
+
const a = source.slice(0, FUZZY_MAX_COMPARED_LENGTH);
|
|
2452
|
+
const b = target.slice(0, FUZZY_MAX_COMPARED_LENGTH);
|
|
2453
|
+
const over = budget + 1;
|
|
2454
|
+
const width = b.length + 1;
|
|
2455
|
+
let previousPrevious = new Array<number>(width).fill(0);
|
|
2456
|
+
let previous = Array.from({ length: width }, (_, index) => index);
|
|
2457
|
+
let current = new Array<number>(width).fill(0);
|
|
2458
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
2459
|
+
current[0] = i;
|
|
2460
|
+
let rowMinimum = current[0];
|
|
2461
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
2462
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2463
|
+
let value = Math.min(previous[j]! + 1, current[j - 1]! + 1, previous[j - 1]! + cost);
|
|
2464
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
2465
|
+
value = Math.min(value, previousPrevious[j - 2]! + 1);
|
|
2466
|
+
}
|
|
2467
|
+
current[j] = value;
|
|
2468
|
+
if (value < rowMinimum) rowMinimum = value;
|
|
2469
|
+
}
|
|
2470
|
+
if (rowMinimum > budget) return { whole: over, prefix: over };
|
|
2471
|
+
[previousPrevious, previous, current] = [previous, current, previousPrevious];
|
|
2472
|
+
}
|
|
2473
|
+
let prefix = previous[0]!;
|
|
2474
|
+
for (let j = 1; j <= b.length; j += 1) prefix = Math.min(prefix, previous[j]!);
|
|
2475
|
+
return { whole: Math.min(previous[b.length]!, over), prefix: Math.min(prefix, over) };
|
|
2476
|
+
}
|
|
2477
|
+
|
|
1825
2478
|
function normalizeCapabilitySearchText(value: string): string {
|
|
1826
2479
|
return value
|
|
1827
2480
|
.normalize("NFKD")
|
|
@@ -1844,10 +2497,6 @@ function publicRegistryCapabilityId(name: string, version: string, endpointUrl:
|
|
|
1844
2497
|
return `mcp-registry:${slugify(name)}-${shortHash(`${name}:${version}:${endpointUrl}`)}`;
|
|
1845
2498
|
}
|
|
1846
2499
|
|
|
1847
|
-
function packIdFromCapabilityId(capabilityId: string): string {
|
|
1848
|
-
return capabilityId.replace(/^pack:/, "");
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
2500
|
function uniqueTags(tags: string[]): string[] {
|
|
1852
2501
|
return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
|
|
1853
2502
|
}
|