@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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
|
+
import { ConnectionAccountSelectionDiagnostic } from "@opengeni/contracts";
|
|
3
|
+
|
|
4
|
+
/** An account choice needs attention; retrying the same work cannot fix it. */
|
|
5
|
+
export class ConnectionAccountSelectionError extends HTTPException {
|
|
6
|
+
override name = "ConnectionAccountSelectionError";
|
|
7
|
+
readonly diagnostic: ConnectionAccountSelectionDiagnostic;
|
|
8
|
+
constructor(
|
|
9
|
+
message: string,
|
|
10
|
+
diagnostic: ConnectionAccountSelectionDiagnostic = {
|
|
11
|
+
version: 1,
|
|
12
|
+
reason: "selection_unavailable",
|
|
13
|
+
accounts: [],
|
|
14
|
+
},
|
|
15
|
+
) {
|
|
16
|
+
super(422, { message });
|
|
17
|
+
this.diagnostic = ConnectionAccountSelectionDiagnostic.parse(diagnostic);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { slackRestMcpToolsForScopes } from "@opengeni/contracts/slack-rest-mcp";
|
|
2
|
+
import { connectionAccountIdentityLabel } from "@opengeni/contracts/connection-account-label";
|
|
3
|
+
import { mcpAccountBindingsFromVisibleConnections } from "./mcp-account-bindings";
|
|
4
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
7
|
+
import {
|
|
8
|
+
boundMcpResponseBody,
|
|
9
|
+
MCP_MAX_RESPONSE_BYTES,
|
|
10
|
+
assertMcpToolListWithinBounds,
|
|
11
|
+
} from "@opengeni/runtime/mcp-network";
|
|
12
|
+
import { createPinnedIntegrationTransport } from "@opengeni/capabilities";
|
|
13
|
+
import type { Settings } from "@opengeni/config";
|
|
14
|
+
import type {
|
|
15
|
+
AccessGrant,
|
|
16
|
+
ConnectorToolPermissionEntry,
|
|
17
|
+
ConnectorToolPermissionsResponse,
|
|
18
|
+
UpdateConnectorToolPermissionsRequest,
|
|
19
|
+
ConnectionMetadata,
|
|
20
|
+
McpServerConnectionRef,
|
|
21
|
+
SessionMcpToolPermissionsQuery,
|
|
22
|
+
SessionMcpToolPermissionsResponse,
|
|
23
|
+
UpdateSessionMcpToolPermissionsRequest,
|
|
24
|
+
} from "@opengeni/contracts";
|
|
25
|
+
import { mcpToolRecommendsApproval } from "@opengeni/contracts";
|
|
26
|
+
import {
|
|
27
|
+
buildConnectionTokenResolver,
|
|
28
|
+
getConnectionMetadata,
|
|
29
|
+
listEnabledMcpCapabilityServers,
|
|
30
|
+
listConnectorToolPermissionPolicies,
|
|
31
|
+
listChangedConnectorToolPermissions,
|
|
32
|
+
updateConnectorToolPermissionPolicies,
|
|
33
|
+
projectConnectorToolPermission,
|
|
34
|
+
connectorToolPolicyRevision,
|
|
35
|
+
ConnectorToolPermissionConflictError,
|
|
36
|
+
listInstalledApiIntegrations,
|
|
37
|
+
type Database,
|
|
38
|
+
} from "@opengeni/db";
|
|
39
|
+
import { HTTPException } from "hono/http-exception";
|
|
40
|
+
import {
|
|
41
|
+
GMAIL_REST_MCP_TOOLS,
|
|
42
|
+
gmailToolAvailableOnDeployment,
|
|
43
|
+
gmailToolSupportsScopes,
|
|
44
|
+
isOfficialGmailMcpConfig,
|
|
45
|
+
isOfficialSlackMcpConfig,
|
|
46
|
+
sessionMcpApprovalConnectionId,
|
|
47
|
+
} from "@opengeni/runtime";
|
|
48
|
+
import { hasPermission } from "../access";
|
|
49
|
+
import {
|
|
50
|
+
personalConnectionDelegationSourceForGrant,
|
|
51
|
+
visibleMcpAccountConnections,
|
|
52
|
+
} from "./personal-connection-delegations";
|
|
53
|
+
import {
|
|
54
|
+
buildCapabilityCatalog,
|
|
55
|
+
settingsWithMcpCapabilityServers,
|
|
56
|
+
settingsWithApiIntegrationServers,
|
|
57
|
+
} from "./capabilities";
|
|
58
|
+
|
|
59
|
+
type Input = {
|
|
60
|
+
db: Database;
|
|
61
|
+
settings: Settings;
|
|
62
|
+
workspaceId: string;
|
|
63
|
+
grant: AccessGrant;
|
|
64
|
+
capabilityId: string;
|
|
65
|
+
personalOwnerVerified: boolean;
|
|
66
|
+
connectionId?: string;
|
|
67
|
+
instanceKey?: string;
|
|
68
|
+
};
|
|
69
|
+
type ListedTool = {
|
|
70
|
+
name: string;
|
|
71
|
+
title?: string | undefined;
|
|
72
|
+
description?: string | undefined;
|
|
73
|
+
annotations?:
|
|
74
|
+
| {
|
|
75
|
+
readOnlyHint?: boolean | undefined;
|
|
76
|
+
destructiveHint?: boolean | undefined;
|
|
77
|
+
title?: string | undefined;
|
|
78
|
+
}
|
|
79
|
+
| undefined;
|
|
80
|
+
_meta?: Record<string, unknown> | undefined;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function connectorToolGroup(tool: ListedTool): "read" | "write" | "other" {
|
|
84
|
+
if (tool.annotations?.destructiveHint === true || tool.annotations?.readOnlyHint === false)
|
|
85
|
+
return "write";
|
|
86
|
+
if (tool.annotations?.readOnlyHint === true) return "read";
|
|
87
|
+
return "other";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A selector can supply discovery credentials only when one visible account
|
|
91
|
+
* matches. Never apply one account's approval choices to an arbitrary sibling. */
|
|
92
|
+
export function unpinnedConnectorToolPermissionAccount<T extends ConnectionMetadata>(
|
|
93
|
+
ref: McpServerConnectionRef,
|
|
94
|
+
visible: T[],
|
|
95
|
+
subjectId: string,
|
|
96
|
+
): T | null {
|
|
97
|
+
const matches = visible.filter(
|
|
98
|
+
(candidate) =>
|
|
99
|
+
candidate.subjectId === (ref.subjectScope === "subject" ? subjectId : null) &&
|
|
100
|
+
candidate.providerDomain === ref.providerDomain &&
|
|
101
|
+
(!ref.kind || candidate.kind === ref.kind) &&
|
|
102
|
+
candidate.status === "active",
|
|
103
|
+
);
|
|
104
|
+
if (matches.length > 1)
|
|
105
|
+
throw new HTTPException(409, {
|
|
106
|
+
message: "Choose one account to manage its tool permissions",
|
|
107
|
+
});
|
|
108
|
+
return matches[0] ?? null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function connectorToolPermissionReference(
|
|
112
|
+
ref: McpServerConnectionRef,
|
|
113
|
+
connectionId: string,
|
|
114
|
+
): McpServerConnectionRef {
|
|
115
|
+
return { ...ref, accountSelection: undefined, connectionId };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function resolveTarget(input: Input) {
|
|
119
|
+
if (input.capabilityId.startsWith("api:")) {
|
|
120
|
+
const integrations = (
|
|
121
|
+
await listInstalledApiIntegrations(
|
|
122
|
+
input.db,
|
|
123
|
+
input.workspaceId,
|
|
124
|
+
input.personalOwnerVerified ? input.grant.subjectId : undefined,
|
|
125
|
+
)
|
|
126
|
+
).filter((integration) => integration.capabilityId === input.capabilityId);
|
|
127
|
+
const accountId = (integration: (typeof integrations)[number]) =>
|
|
128
|
+
integration.connectionRef?.connectionId ??
|
|
129
|
+
sessionMcpApprovalConnectionId(integration.serverId, integration.baseUrl);
|
|
130
|
+
const integration = integrations.find(
|
|
131
|
+
(candidate) =>
|
|
132
|
+
(!input.instanceKey || candidate.instanceKey === input.instanceKey) &&
|
|
133
|
+
(!input.connectionId || accountId(candidate) === input.connectionId),
|
|
134
|
+
);
|
|
135
|
+
if (!integration) throw new HTTPException(404, { message: "Integration account unavailable" });
|
|
136
|
+
const server = settingsWithApiIntegrationServers({ ...input.settings, mcpServers: [] }, [
|
|
137
|
+
integration,
|
|
138
|
+
]).mcpServers[0]!;
|
|
139
|
+
const connection = integration.connectionRef?.connectionId
|
|
140
|
+
? await getConnectionMetadata(
|
|
141
|
+
input.db,
|
|
142
|
+
input.workspaceId,
|
|
143
|
+
integration.connectionRef.connectionId,
|
|
144
|
+
input.grant.subjectId,
|
|
145
|
+
)
|
|
146
|
+
: null;
|
|
147
|
+
const accounts = integrations.map((candidate) => ({
|
|
148
|
+
connectionId: accountId(candidate),
|
|
149
|
+
label: candidate.displayName,
|
|
150
|
+
scope:
|
|
151
|
+
candidate.connectionRef?.subjectScope === "subject"
|
|
152
|
+
? ("personal" as const)
|
|
153
|
+
: candidate.connectionRef
|
|
154
|
+
? ("workspace" as const)
|
|
155
|
+
: ("none" as const),
|
|
156
|
+
instanceKey: candidate.instanceKey,
|
|
157
|
+
}));
|
|
158
|
+
return {
|
|
159
|
+
server,
|
|
160
|
+
connection,
|
|
161
|
+
connectionId: accountId(integration),
|
|
162
|
+
accountLabel: connection
|
|
163
|
+
? connectionAccountIdentityLabel(connection.metadata, integration.displayName)
|
|
164
|
+
: integration.displayName,
|
|
165
|
+
accounts,
|
|
166
|
+
instanceKey: integration.instanceKey,
|
|
167
|
+
listedTools: integration.revision.tools
|
|
168
|
+
.filter((tool) => integration.allowedTools.includes(tool.id))
|
|
169
|
+
.map(
|
|
170
|
+
(tool): ListedTool => ({
|
|
171
|
+
name: tool.id,
|
|
172
|
+
title: tool.name,
|
|
173
|
+
description: tool.description,
|
|
174
|
+
annotations: {
|
|
175
|
+
readOnlyHint: tool.safety === "read",
|
|
176
|
+
destructiveHint: tool.safety === "destructive",
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const catalog = await buildCapabilityCatalog(input);
|
|
183
|
+
const item = catalog.items.find((candidate) => candidate.id === input.capabilityId);
|
|
184
|
+
if (
|
|
185
|
+
!item ||
|
|
186
|
+
!item.enabled ||
|
|
187
|
+
item.kind !== "mcp" ||
|
|
188
|
+
!item.runtime.mcpServerId ||
|
|
189
|
+
item.source === "built_in"
|
|
190
|
+
) {
|
|
191
|
+
throw new HTTPException(404, { message: "Connected MCP connector not found" });
|
|
192
|
+
}
|
|
193
|
+
const enabled = await listEnabledMcpCapabilityServers(input.db, input.workspaceId);
|
|
194
|
+
const settings = settingsWithMcpCapabilityServers(input.settings, enabled);
|
|
195
|
+
const server = settings.mcpServers.find((candidate) => candidate.id === item.runtime.mcpServerId);
|
|
196
|
+
if (!server || server.url !== item.endpointUrl)
|
|
197
|
+
throw new HTTPException(409, { message: "Connector configuration is unavailable" });
|
|
198
|
+
const ref = server.connectionRef;
|
|
199
|
+
if (
|
|
200
|
+
ref?.subjectScope === "subject" &&
|
|
201
|
+
ref.accountSelection !== "all_eligible" &&
|
|
202
|
+
!input.personalOwnerVerified
|
|
203
|
+
)
|
|
204
|
+
throw new HTTPException(403, {
|
|
205
|
+
message: "Only the authenticated connection owner may manage personal tool permissions",
|
|
206
|
+
});
|
|
207
|
+
const source = personalConnectionDelegationSourceForGrant(input.grant);
|
|
208
|
+
const visible = ref
|
|
209
|
+
? await visibleMcpAccountConnections(input.db, {
|
|
210
|
+
accountId: input.grant.accountId,
|
|
211
|
+
workspaceId: input.workspaceId,
|
|
212
|
+
source:
|
|
213
|
+
input.personalOwnerVerified && source.kind === "subject" ? source : { kind: "none" },
|
|
214
|
+
})
|
|
215
|
+
: [];
|
|
216
|
+
const bindings = mcpAccountBindingsFromVisibleConnections({
|
|
217
|
+
accountId: input.grant.accountId,
|
|
218
|
+
workspaceId: input.workspaceId,
|
|
219
|
+
subjectId: input.personalOwnerVerified ? input.grant.subjectId : null,
|
|
220
|
+
servers: [server],
|
|
221
|
+
// Runtime admission can enumerate every eligible account. A permission
|
|
222
|
+
// selector must also preserve the installation's explicit owner scope.
|
|
223
|
+
connections:
|
|
224
|
+
ref?.accountSelection === "all_eligible"
|
|
225
|
+
? visible
|
|
226
|
+
: visible.filter((candidate) =>
|
|
227
|
+
ref?.subjectScope === "subject"
|
|
228
|
+
? candidate.subjectId === input.grant.subjectId
|
|
229
|
+
: candidate.subjectId === null,
|
|
230
|
+
),
|
|
231
|
+
});
|
|
232
|
+
const binding = bindings.find(
|
|
233
|
+
(candidate) => !input.connectionId || candidate.connectionId === input.connectionId,
|
|
234
|
+
);
|
|
235
|
+
const connection = visible.find((candidate) => candidate.id === binding?.connectionId) ?? null;
|
|
236
|
+
if (ref && !connection) {
|
|
237
|
+
throw new HTTPException(409, { message: "Reconnect this connector to manage its tools" });
|
|
238
|
+
}
|
|
239
|
+
if (connection?.subjectId && !input.personalOwnerVerified)
|
|
240
|
+
throw new HTTPException(403, {
|
|
241
|
+
message: "Only the authenticated connection owner may manage personal tool permissions",
|
|
242
|
+
});
|
|
243
|
+
const connectionId = connection?.id ?? sessionMcpApprovalConnectionId(server.id, server.url);
|
|
244
|
+
if (input.connectionId && input.connectionId !== connectionId)
|
|
245
|
+
throw new HTTPException(409, {
|
|
246
|
+
message: "Connector account unavailable. Reload its permissions.",
|
|
247
|
+
});
|
|
248
|
+
return {
|
|
249
|
+
server: binding ? { ...server, connectionRef: binding.connectionRef } : server,
|
|
250
|
+
connection,
|
|
251
|
+
connectionId,
|
|
252
|
+
accountLabel: binding?.accountLabel ?? server.name ?? server.id,
|
|
253
|
+
accounts: bindings.map((candidate) => ({
|
|
254
|
+
connectionId: candidate.connectionId,
|
|
255
|
+
label: candidate.accountLabel,
|
|
256
|
+
scope: candidate.subjectScope === "subject" ? ("personal" as const) : ("workspace" as const),
|
|
257
|
+
})),
|
|
258
|
+
instanceKey: undefined,
|
|
259
|
+
listedTools: null,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Use the same adapter-owned identities as execution for reviewed local catalogs. */
|
|
264
|
+
export function reviewedConnectorToolCatalog(
|
|
265
|
+
server: Pick<Settings["mcpServers"][number], "url" | "connectionRef" | "allowedTools">,
|
|
266
|
+
grantedScopes: readonly string[],
|
|
267
|
+
deployment: Pick<Settings, "gmailWatchTopicName">,
|
|
268
|
+
): ListedTool[] | null {
|
|
269
|
+
const tools = isOfficialGmailMcpConfig(server.url, server.connectionRef)
|
|
270
|
+
? GMAIL_REST_MCP_TOOLS.filter(
|
|
271
|
+
(tool) =>
|
|
272
|
+
gmailToolSupportsScopes(tool.name, grantedScopes) &&
|
|
273
|
+
gmailToolAvailableOnDeployment(tool.name, {
|
|
274
|
+
watchTopicName: deployment.gmailWatchTopicName,
|
|
275
|
+
}),
|
|
276
|
+
)
|
|
277
|
+
: isOfficialSlackMcpConfig(server.url, server.connectionRef)
|
|
278
|
+
? slackRestMcpToolsForScopes(grantedScopes)
|
|
279
|
+
: null;
|
|
280
|
+
return (
|
|
281
|
+
tools?.filter((tool) => !server.allowedTools || server.allowedTools.includes(tool.name)) ?? null
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function listTools(
|
|
286
|
+
input: Input,
|
|
287
|
+
target: Awaited<ReturnType<typeof resolveTarget>>,
|
|
288
|
+
): Promise<ListedTool[]> {
|
|
289
|
+
if (target.listedTools !== null) return target.listedTools;
|
|
290
|
+
const reviewed = reviewedConnectorToolCatalog(
|
|
291
|
+
target.server,
|
|
292
|
+
target.connection?.grantedScopes ?? [],
|
|
293
|
+
input.settings,
|
|
294
|
+
);
|
|
295
|
+
if (reviewed !== null) return reviewed;
|
|
296
|
+
let headers = { ...target.server.headers };
|
|
297
|
+
if (target.server.connectionRef) {
|
|
298
|
+
const result = await buildConnectionTokenResolver(
|
|
299
|
+
input.db,
|
|
300
|
+
input.settings,
|
|
301
|
+
)({
|
|
302
|
+
// The selected account's credentials remain in its origin workspace;
|
|
303
|
+
// approval preferences remain scoped to the workspace being configured.
|
|
304
|
+
workspaceId: target.connection?.workspaceId ?? input.workspaceId,
|
|
305
|
+
...(target.connection?.subjectId ? { subjectId: input.grant.subjectId } : {}),
|
|
306
|
+
serverId: target.server.id,
|
|
307
|
+
toolName: "tools/list",
|
|
308
|
+
connectionRef: connectorToolPermissionReference(
|
|
309
|
+
target.server.connectionRef,
|
|
310
|
+
target.connectionId,
|
|
311
|
+
),
|
|
312
|
+
destinationUrl: target.server.url,
|
|
313
|
+
});
|
|
314
|
+
if (result.status !== "ok") throw new Error("Reconnect this connector to load its tools.");
|
|
315
|
+
headers = { ...headers, ...result.headers };
|
|
316
|
+
}
|
|
317
|
+
const pinned = createPinnedIntegrationTransport({ network: input.settings });
|
|
318
|
+
const deadline = AbortSignal.timeout(15_000);
|
|
319
|
+
const client = new Client(
|
|
320
|
+
{ name: "opengeni-tool-permissions", version: "0.1.0" },
|
|
321
|
+
{ capabilities: {} },
|
|
322
|
+
);
|
|
323
|
+
try {
|
|
324
|
+
const transport = new StreamableHTTPClientTransport(new URL(target.server.url), {
|
|
325
|
+
requestInit: { headers },
|
|
326
|
+
fetch: async (url, init) =>
|
|
327
|
+
boundMcpResponseBody(
|
|
328
|
+
await pinned.fetch(url.toString(), {
|
|
329
|
+
...init,
|
|
330
|
+
signal: init?.signal ? AbortSignal.any([deadline, init.signal]) : deadline,
|
|
331
|
+
}),
|
|
332
|
+
MCP_MAX_RESPONSE_BYTES,
|
|
333
|
+
),
|
|
334
|
+
});
|
|
335
|
+
await client.connect(transport as unknown as Transport, {
|
|
336
|
+
timeout: 15_000,
|
|
337
|
+
maxTotalTimeout: 15_000,
|
|
338
|
+
});
|
|
339
|
+
const tools = await collectConnectorToolPages(client, deadline);
|
|
340
|
+
return tools.filter(
|
|
341
|
+
(tool) => !target.server.allowedTools || target.server.allowedTools.includes(tool.name),
|
|
342
|
+
);
|
|
343
|
+
} finally {
|
|
344
|
+
await client.close().catch(() => undefined);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Collect discovery pages without changing the caller's connection or authority. */
|
|
349
|
+
export async function collectConnectorToolPages(
|
|
350
|
+
client: Pick<Client, "listTools">,
|
|
351
|
+
deadline: AbortSignal,
|
|
352
|
+
): Promise<ListedTool[]> {
|
|
353
|
+
const tools: ListedTool[] = [];
|
|
354
|
+
const cursors = new Set<string>();
|
|
355
|
+
let cursor: string | undefined;
|
|
356
|
+
do {
|
|
357
|
+
const page = await client.listTools(cursor ? { cursor } : undefined, {
|
|
358
|
+
timeout: 15_000,
|
|
359
|
+
maxTotalTimeout: 15_000,
|
|
360
|
+
signal: deadline,
|
|
361
|
+
});
|
|
362
|
+
tools.push(...page.tools);
|
|
363
|
+
assertMcpToolListWithinBounds(tools);
|
|
364
|
+
if (new Set(tools.map((tool) => tool.name)).size !== tools.length)
|
|
365
|
+
throw new Error("The connector returned duplicate tool names.");
|
|
366
|
+
cursor = page.nextCursor;
|
|
367
|
+
if (cursor && cursors.has(cursor))
|
|
368
|
+
throw new Error("The connector returned an invalid or oversized tool catalog.");
|
|
369
|
+
if (cursor) cursors.add(cursor);
|
|
370
|
+
} while (cursor);
|
|
371
|
+
return tools;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function getConnectorToolPermissions(
|
|
375
|
+
input: Input,
|
|
376
|
+
): Promise<ConnectorToolPermissionsResponse> {
|
|
377
|
+
const target = await resolveTarget(input);
|
|
378
|
+
const policies = await listConnectorToolPermissionPolicies(input.db, {
|
|
379
|
+
accountId: input.grant.accountId,
|
|
380
|
+
workspaceId: input.workspaceId,
|
|
381
|
+
connectionId: target.connectionId,
|
|
382
|
+
});
|
|
383
|
+
const changedTools = new Set(
|
|
384
|
+
await listChangedConnectorToolPermissions(input.db, {
|
|
385
|
+
accountId: input.grant.accountId,
|
|
386
|
+
workspaceId: input.workspaceId,
|
|
387
|
+
connectionId: target.connectionId,
|
|
388
|
+
serverId: target.server.id,
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
const defaultPolicy = policies.find(
|
|
392
|
+
(row) => row.serverId === target.server.id && row.toolName === "*" && row.actionName === "*",
|
|
393
|
+
);
|
|
394
|
+
let tools: ListedTool[] = [];
|
|
395
|
+
let discoveryError: string | null = null;
|
|
396
|
+
try {
|
|
397
|
+
tools = await listTools(input, target);
|
|
398
|
+
if (tools.some((tool) => tool.name === "*" || tool.name !== tool.name.trim())) {
|
|
399
|
+
tools = tools.filter((tool) => tool.name !== "*" && tool.name === tool.name.trim());
|
|
400
|
+
discoveryError =
|
|
401
|
+
"The connector exposes a reserved or whitespace-padded tool name. Individual permissions for these names are unsupported, so they are omitted from tool groups. The connector default still applies.";
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
discoveryError =
|
|
405
|
+
"Could not load the connector's tools. Try again or reconnect. Your saved permissions still apply.";
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
connectionId: target.connectionId,
|
|
409
|
+
serverId: target.server.id,
|
|
410
|
+
defaultPermission: defaultPolicy?.policy ?? null,
|
|
411
|
+
tools: tools.map(
|
|
412
|
+
(tool): ConnectorToolPermissionEntry => ({
|
|
413
|
+
name: tool.name,
|
|
414
|
+
...(changedTools.has(tool.name) ? { resetReason: "operation_changed" as const } : {}),
|
|
415
|
+
...((tool.title ?? tool.annotations?.title)
|
|
416
|
+
? { title: tool.title ?? tool.annotations?.title }
|
|
417
|
+
: {}),
|
|
418
|
+
...(tool.description ? { description: tool.description } : {}),
|
|
419
|
+
group: connectorToolGroup(tool),
|
|
420
|
+
...projectConnectorToolPermission(policies, {
|
|
421
|
+
connectionId: target.connectionId,
|
|
422
|
+
serverId: target.server.id,
|
|
423
|
+
toolName: tool.name,
|
|
424
|
+
defaultDecision:
|
|
425
|
+
target.server.requireApproval === true ||
|
|
426
|
+
(Array.isArray(target.server.requireApproval) &&
|
|
427
|
+
target.server.requireApproval.includes(tool.name)) ||
|
|
428
|
+
mcpToolRecommendsApproval(tool)
|
|
429
|
+
? "ask"
|
|
430
|
+
: "allow",
|
|
431
|
+
}),
|
|
432
|
+
}),
|
|
433
|
+
),
|
|
434
|
+
discoveryError,
|
|
435
|
+
canManage: canManageConnectorPermissions(input.grant),
|
|
436
|
+
appliesTo: "next_attempt",
|
|
437
|
+
revision: connectorToolPolicyRevision(policies, target.connectionId, target.server.id),
|
|
438
|
+
accountLabel: target.accountLabel,
|
|
439
|
+
accounts: target.accounts,
|
|
440
|
+
...(target.instanceKey ? { instanceKey: target.instanceKey } : {}),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function canManageConnectorPermissions(grant: AccessGrant): boolean {
|
|
445
|
+
return (
|
|
446
|
+
hasPermission(grant.permissions, "capabilities:manage") &&
|
|
447
|
+
grant.principalKind !== "agent_attempt" &&
|
|
448
|
+
grant.principalKind !== "service" &&
|
|
449
|
+
!grant.serviceInitiator &&
|
|
450
|
+
!grant.serviceInitiatorContext
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function updateConnectorToolPermissions(
|
|
455
|
+
input: Input & { payload: UpdateConnectorToolPermissionsRequest },
|
|
456
|
+
): Promise<void> {
|
|
457
|
+
if (!canManageConnectorPermissions(input.grant))
|
|
458
|
+
throw new HTTPException(403, { message: "Connector management permission required" });
|
|
459
|
+
if (
|
|
460
|
+
input.payload.target === "tools" &&
|
|
461
|
+
input.payload.toolNames.some((name) => name === "*" || name !== name.trim())
|
|
462
|
+
)
|
|
463
|
+
throw new HTTPException(400, {
|
|
464
|
+
message: "Tool names must be exact and cannot use the connector-default wildcard.",
|
|
465
|
+
});
|
|
466
|
+
const target = await resolveTarget({
|
|
467
|
+
...input,
|
|
468
|
+
connectionId: input.payload.connectionId,
|
|
469
|
+
...(input.payload.instanceKey ? { instanceKey: input.payload.instanceKey } : {}),
|
|
470
|
+
});
|
|
471
|
+
// Prevent a reconnect while the sheet is open from applying old choices to a new account.
|
|
472
|
+
if (target.connectionId !== input.payload.connectionId)
|
|
473
|
+
throw new HTTPException(409, {
|
|
474
|
+
message: "The connector account changed. Reload its permissions.",
|
|
475
|
+
});
|
|
476
|
+
try {
|
|
477
|
+
await updateConnectorToolPermissionPolicies(input.db, {
|
|
478
|
+
accountId: input.grant.accountId,
|
|
479
|
+
workspaceId: input.workspaceId,
|
|
480
|
+
subjectId: input.grant.subjectId,
|
|
481
|
+
connectionId: target.connectionId,
|
|
482
|
+
serverId: target.server.id,
|
|
483
|
+
toolNames:
|
|
484
|
+
input.payload.target === "default"
|
|
485
|
+
? ["*"]
|
|
486
|
+
: input.payload.target === "action"
|
|
487
|
+
? [input.payload.toolName]
|
|
488
|
+
: input.payload.toolNames,
|
|
489
|
+
...(input.payload.target === "action" ? { actionName: input.payload.actionName } : {}),
|
|
490
|
+
...(input.payload.expectedRevision
|
|
491
|
+
? { expectedRevision: input.payload.expectedRevision }
|
|
492
|
+
: {}),
|
|
493
|
+
policy: input.payload.permission,
|
|
494
|
+
});
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (error instanceof ConnectorToolPermissionConflictError)
|
|
497
|
+
throw new HTTPException(409, { message: error.message });
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Same secret-free identity the runtime derives for a credential-free session MCP server.
|
|
504
|
+
* Keyed to the exact URL: a different URL string gets different rules.
|
|
505
|
+
*/
|
|
506
|
+
export function sessionMcpToolPermissionConnectionId(serverId: string, url: string): string {
|
|
507
|
+
return sessionMcpApprovalConnectionId(serverId, url);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
type SessionMcpInput = {
|
|
511
|
+
db: Database;
|
|
512
|
+
workspaceId: string;
|
|
513
|
+
grant: AccessGrant;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Saved Allow/Ask/Block choices for a host-attached session MCP server. The
|
|
518
|
+
* host owns the tool catalog; Opengeni owns the rules and enforces them on the
|
|
519
|
+
* next attempt of every session in this workspace that attaches the same
|
|
520
|
+
* server id and exact URL.
|
|
521
|
+
*/
|
|
522
|
+
export async function getSessionMcpToolPermissions(
|
|
523
|
+
input: SessionMcpInput & { query: SessionMcpToolPermissionsQuery },
|
|
524
|
+
): Promise<SessionMcpToolPermissionsResponse> {
|
|
525
|
+
const { serverId, url } = input.query;
|
|
526
|
+
const connectionId = sessionMcpToolPermissionConnectionId(serverId, url);
|
|
527
|
+
const policies = await listConnectorToolPermissionPolicies(input.db, {
|
|
528
|
+
accountId: input.grant.accountId,
|
|
529
|
+
workspaceId: input.workspaceId,
|
|
530
|
+
connectionId,
|
|
531
|
+
});
|
|
532
|
+
const serverPolicies = policies.filter(
|
|
533
|
+
(row) => row.serverId === serverId && row.actionName === "*",
|
|
534
|
+
);
|
|
535
|
+
return {
|
|
536
|
+
connectionId,
|
|
537
|
+
serverId,
|
|
538
|
+
defaultPermission: serverPolicies.find((row) => row.toolName === "*")?.policy ?? null,
|
|
539
|
+
tools: serverPolicies
|
|
540
|
+
.filter((row) => row.toolName !== "*")
|
|
541
|
+
.map((row) => ({ toolName: row.toolName, permission: row.policy }))
|
|
542
|
+
.sort((left, right) => left.toolName.localeCompare(right.toolName)),
|
|
543
|
+
revision: connectorToolPolicyRevision(policies, connectionId, serverId),
|
|
544
|
+
canManage: canManageConnectorPermissions(input.grant),
|
|
545
|
+
appliesTo: "next_attempt",
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Agents and service principals cannot change the permissions that bound them. */
|
|
550
|
+
export async function updateSessionMcpToolPermissions(
|
|
551
|
+
input: SessionMcpInput & { payload: UpdateSessionMcpToolPermissionsRequest },
|
|
552
|
+
): Promise<void> {
|
|
553
|
+
if (!canManageConnectorPermissions(input.grant))
|
|
554
|
+
throw new HTTPException(403, { message: "Connector management permission required" });
|
|
555
|
+
const { payload } = input;
|
|
556
|
+
try {
|
|
557
|
+
await updateConnectorToolPermissionPolicies(input.db, {
|
|
558
|
+
accountId: input.grant.accountId,
|
|
559
|
+
workspaceId: input.workspaceId,
|
|
560
|
+
subjectId: input.grant.subjectId,
|
|
561
|
+
connectionId: sessionMcpToolPermissionConnectionId(payload.serverId, payload.url),
|
|
562
|
+
serverId: payload.serverId,
|
|
563
|
+
toolNames: payload.target === "default" ? ["*"] : payload.toolNames,
|
|
564
|
+
...(payload.expectedRevision ? { expectedRevision: payload.expectedRevision } : {}),
|
|
565
|
+
policy: payload.permission,
|
|
566
|
+
});
|
|
567
|
+
} catch (error) {
|
|
568
|
+
if (error instanceof ConnectorToolPermissionConflictError)
|
|
569
|
+
throw new HTTPException(409, { message: error.message });
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DirectModelProviderMetadata } from "@opengeni/contracts";
|
|
2
|
+
import { HTTPException } from "hono/http-exception";
|
|
3
|
+
|
|
4
|
+
/** Verify the customer's exact route without persisting their key or test response. */
|
|
5
|
+
export async function verifyDirectModelAccess(
|
|
6
|
+
input: DirectModelProviderMetadata,
|
|
7
|
+
apiKey: string,
|
|
8
|
+
fetchImpl: typeof fetch = fetch,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const config = DirectModelProviderMetadata.parse(input);
|
|
11
|
+
const baseUrl = config.provider === "openai" ? "https://api.openai.com/v1" : config.endpoint!;
|
|
12
|
+
let response: Response;
|
|
13
|
+
try {
|
|
14
|
+
response = await fetchImpl(`${baseUrl}/responses`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
redirect: "error",
|
|
17
|
+
signal: AbortSignal.timeout(15_000),
|
|
18
|
+
headers: {
|
|
19
|
+
"content-type": "application/json",
|
|
20
|
+
...(config.provider === "openai"
|
|
21
|
+
? { authorization: `Bearer ${apiKey}` }
|
|
22
|
+
: { "api-key": apiKey }),
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
model: config.model,
|
|
26
|
+
input: "Reply OK.",
|
|
27
|
+
max_output_tokens: 32,
|
|
28
|
+
store: false,
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
throw new HTTPException(502, {
|
|
33
|
+
message: "We couldn’t reach your model provider. Check the endpoint and try again.",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Never reflect provider responses: they can include credentials or request details.
|
|
37
|
+
if (response.status === 401 || response.status === 403) {
|
|
38
|
+
throw new HTTPException(422, {
|
|
39
|
+
message: "Your provider didn’t accept this API key. Check the key and its permissions.",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (response.status === 404 || response.status === 400) {
|
|
43
|
+
throw new HTTPException(422, {
|
|
44
|
+
message:
|
|
45
|
+
config.provider === "azure_openai"
|
|
46
|
+
? "Azure couldn’t use this deployment. Check the endpoint and deployment name."
|
|
47
|
+
: "OpenAI couldn’t use this model. Check the model name and your account’s access.",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (response.status === 429) {
|
|
51
|
+
throw new HTTPException(422, {
|
|
52
|
+
message:
|
|
53
|
+
"Your provider’s quota or rate limit blocked the check. Check your provider billing or try again shortly.",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const body = response.ok ? await response.json().catch(() => null) : null;
|
|
57
|
+
if (!body || !["completed", "incomplete"].includes(body.status)) {
|
|
58
|
+
throw new HTTPException(502, {
|
|
59
|
+
message: "Your provider couldn’t complete the connection check. Try again shortly.",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|