@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,7 +1,59 @@
|
|
|
1
|
-
import { KnowledgeEntryListRequest } from "@opengeni/contracts";
|
|
2
|
-
import
|
|
1
|
+
import { KnowledgeEntryListRequest, type KnowledgeEntryListResponse } from "@opengeni/contracts";
|
|
2
|
+
import type { Settings } from "@opengeni/config";
|
|
3
|
+
import {
|
|
4
|
+
applyCreditDebitAfterUse,
|
|
5
|
+
checkWorkspaceAllowance,
|
|
6
|
+
creditDebitAttributionForTurn,
|
|
7
|
+
getSpendableCreditBalance,
|
|
8
|
+
listKnowledgeEntries,
|
|
9
|
+
recordUsageEvent,
|
|
10
|
+
sumUsageQuantity,
|
|
11
|
+
withKnowledgeQueryAccountLock,
|
|
12
|
+
type Database,
|
|
13
|
+
type KnowledgeContext,
|
|
14
|
+
type CreditDebitAttribution,
|
|
15
|
+
} from "@opengeni/db";
|
|
3
16
|
import type { DocumentEmbedder } from "@opengeni/documents";
|
|
4
17
|
import type { z } from "zod";
|
|
18
|
+
import { documentEmbeddingCostMicros, paidDocumentEmbedding } from "../billing/limits";
|
|
19
|
+
import { workspaceCreditsDisabled } from "./workspace-credits";
|
|
20
|
+
|
|
21
|
+
// Safety ceilings, not a commercial tariff. Paid queries have no durable
|
|
22
|
+
// reusable vector cache, so each bounded request consumes the provider once.
|
|
23
|
+
const MAX_PAID_QUERY_BYTES = 4096;
|
|
24
|
+
const MAX_PAID_QUERY_MICROS = 1000;
|
|
25
|
+
const MAX_PAID_QUERY_BYTES_PER_MINUTE = 64 * 1024;
|
|
26
|
+
const MAX_PAID_QUERY_BYTES_PER_MONTH = 8 * 1024 * 1024;
|
|
27
|
+
const MAX_PAID_QUERY_MICROS_PER_MINUTE = 10_000;
|
|
28
|
+
const MAX_PAID_QUERY_MICROS_PER_MONTH = 1_000_000;
|
|
29
|
+
|
|
30
|
+
export class KnowledgeVectorFundingError extends Error {
|
|
31
|
+
readonly code = "knowledge_vector_funding_required";
|
|
32
|
+
constructor() {
|
|
33
|
+
super("Knowledge vector search needs Opengeni credits; keyword search remains available.");
|
|
34
|
+
this.name = "KnowledgeVectorFundingError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class KnowledgeVectorCreditsDisabledError extends Error {
|
|
39
|
+
readonly code = "knowledge_vector_credits_disabled";
|
|
40
|
+
constructor() {
|
|
41
|
+
super(
|
|
42
|
+
"Knowledge vector search uses Opengeni credits, which are turned off in this workspace; keyword search remains available.",
|
|
43
|
+
);
|
|
44
|
+
this.name = "KnowledgeVectorCreditsDisabledError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class KnowledgeVectorQueryRejectedError extends Error {
|
|
49
|
+
constructor(
|
|
50
|
+
public readonly code: "pagination_unavailable" | "query_limit" | "quota",
|
|
51
|
+
message: string,
|
|
52
|
+
) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "KnowledgeVectorQueryRejectedError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
5
57
|
|
|
6
58
|
/** Shared search path for HTTP and both first-party retrieval servers. */
|
|
7
59
|
export async function searchKnowledgeEntries(
|
|
@@ -9,8 +61,18 @@ export async function searchKnowledgeEntries(
|
|
|
9
61
|
context: KnowledgeContext,
|
|
10
62
|
input: z.input<typeof KnowledgeEntryListRequest>,
|
|
11
63
|
embedder: () => DocumentEmbedder,
|
|
12
|
-
|
|
64
|
+
settings?: Settings,
|
|
65
|
+
): Promise<KnowledgeEntryListResponse> {
|
|
13
66
|
const request = KnowledgeEntryListRequest.parse(input);
|
|
67
|
+
// Copy trusted actor fields before any provider/lock await. Request lifetime
|
|
68
|
+
// mutation cannot replace the initiating identity between admission/debit.
|
|
69
|
+
const billingActor = { ...context.actor };
|
|
70
|
+
let paidAttribution: CreditDebitAttribution =
|
|
71
|
+
billingActor.kind === "human"
|
|
72
|
+
? { kind: "human", initiatingHumanSubjectId: billingActor.subjectId }
|
|
73
|
+
: billingActor.kind === "service"
|
|
74
|
+
? { kind: "service" }
|
|
75
|
+
: { kind: "unknown" };
|
|
14
76
|
if (!request.query || request.mode === "keyword") {
|
|
15
77
|
return {
|
|
16
78
|
...(await listKnowledgeEntries(db, context, request)),
|
|
@@ -27,29 +89,184 @@ export async function searchKnowledgeEntries(
|
|
|
27
89
|
const lexicalRequest = KnowledgeEntryListRequest.safeParse({ ...request, query: lexicalQuery });
|
|
28
90
|
const retrievalRequest =
|
|
29
91
|
request.mode === "hybrid" && lexicalRequest.success ? lexicalRequest.data : request;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
values.length !== provider.dimensions ||
|
|
36
|
-
values.some((value) => !Number.isFinite(value)) ||
|
|
37
|
-
!values.some((value) => value !== 0)
|
|
38
|
-
) {
|
|
39
|
-
throw new Error("Knowledge query embedding is unavailable");
|
|
40
|
-
}
|
|
41
|
-
embedding = { model: provider.model, values };
|
|
42
|
-
} catch (error) {
|
|
43
|
-
// Exact vector requests cannot silently change meaning. Hybrid search can
|
|
44
|
-
// continue using the transactional keyword index while a provider is down.
|
|
92
|
+
const keywordFallback = async (
|
|
93
|
+
queryDb: Database,
|
|
94
|
+
error: unknown,
|
|
95
|
+
fallbackReason: NonNullable<KnowledgeEntryListResponse["fallbackReason"]>,
|
|
96
|
+
) => {
|
|
45
97
|
if (request.mode === "vector") throw error;
|
|
46
98
|
return {
|
|
47
|
-
...(await listKnowledgeEntries(
|
|
99
|
+
...(await listKnowledgeEntries(queryDb, context, { ...retrievalRequest, mode: "keyword" })),
|
|
48
100
|
searchMode: "keyword" as const,
|
|
101
|
+
fallbackReason,
|
|
49
102
|
};
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
...(await listKnowledgeEntries(db, context, retrievalRequest, embedding)),
|
|
53
|
-
searchMode: request.mode,
|
|
54
103
|
};
|
|
104
|
+
const bytes = Buffer.byteLength(request.query, "utf8");
|
|
105
|
+
const paidSettings = settings && paidDocumentEmbedding(settings) ? settings : undefined;
|
|
106
|
+
// A workspace that turned Opengeni credits off is never debited; it keeps
|
|
107
|
+
// keyword search (and keyword pagination) instead of paid semantic
|
|
108
|
+
// retrieval. Nothing is charged here, so no account lock is needed.
|
|
109
|
+
if (paidSettings && (await workspaceCreditsDisabled(db, context.workspaceId)))
|
|
110
|
+
return keywordFallback(db, new KnowledgeVectorCreditsDisabledError(), "credits_disabled");
|
|
111
|
+
// Paid cursors would re-embed and re-charge on each page. Until a durable
|
|
112
|
+
// vector cache exists, require explicit keyword mode to paginate instead.
|
|
113
|
+
if (paidSettings && request.cursor)
|
|
114
|
+
throw new KnowledgeVectorQueryRejectedError(
|
|
115
|
+
"pagination_unavailable",
|
|
116
|
+
"Paid semantic Knowledge pagination is unavailable; use keyword mode",
|
|
117
|
+
);
|
|
118
|
+
const cost = paidSettings ? documentEmbeddingCostMicros(paidSettings, bytes) : 0;
|
|
119
|
+
if (paidSettings && (bytes > MAX_PAID_QUERY_BYTES || cost > MAX_PAID_QUERY_MICROS))
|
|
120
|
+
return keywordFallback(
|
|
121
|
+
db,
|
|
122
|
+
new KnowledgeVectorQueryRejectedError(
|
|
123
|
+
"query_limit",
|
|
124
|
+
"Paid Knowledge query exceeds the per-request limit",
|
|
125
|
+
),
|
|
126
|
+
"query_limit",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const retrieveThenSettle = async (queryDb: Database) => {
|
|
130
|
+
let embedding: { model: string; values: number[] };
|
|
131
|
+
try {
|
|
132
|
+
const provider = embedder();
|
|
133
|
+
const values = await provider.embedQuery(request.query!);
|
|
134
|
+
if (
|
|
135
|
+
values.length !== provider.dimensions ||
|
|
136
|
+
values.some((value) => !Number.isFinite(value)) ||
|
|
137
|
+
!values.some((value) => value !== 0)
|
|
138
|
+
)
|
|
139
|
+
throw new Error("Knowledge query embedding is unavailable");
|
|
140
|
+
embedding = { model: provider.model, values };
|
|
141
|
+
} catch (error) {
|
|
142
|
+
// Only provider failure may fall back. Retrieval and settlement errors
|
|
143
|
+
// after use surface; they never masquerade as a successful keyword hit.
|
|
144
|
+
return keywordFallback(queryDb, error, "provider_unavailable");
|
|
145
|
+
}
|
|
146
|
+
// Read before billing, in the same transaction for the paid mode. Failure
|
|
147
|
+
// here leaves no customer charge and no vector result to return.
|
|
148
|
+
const found = await listKnowledgeEntries(queryDb, context, retrievalRequest, embedding);
|
|
149
|
+
if (settings) {
|
|
150
|
+
const usageId = crypto.randomUUID();
|
|
151
|
+
await recordUsageEvent(queryDb, {
|
|
152
|
+
accountId: context.accountId,
|
|
153
|
+
workspaceId: context.workspaceId,
|
|
154
|
+
eventType: "document.query_embedding_bytes",
|
|
155
|
+
quantity: bytes,
|
|
156
|
+
unit: "byte",
|
|
157
|
+
sourceResourceType: "knowledge_query",
|
|
158
|
+
sourceResourceId: usageId,
|
|
159
|
+
idempotencyKey: `knowledge.query_bytes:${usageId}`,
|
|
160
|
+
});
|
|
161
|
+
if (paidSettings && cost > 0)
|
|
162
|
+
await recordUsageEvent(queryDb, {
|
|
163
|
+
accountId: context.accountId,
|
|
164
|
+
workspaceId: context.workspaceId,
|
|
165
|
+
eventType: "document.query_embedding_cost",
|
|
166
|
+
quantity: cost,
|
|
167
|
+
unit: "micro_usd",
|
|
168
|
+
sourceResourceType: "knowledge_query",
|
|
169
|
+
sourceResourceId: usageId,
|
|
170
|
+
idempotencyKey: `knowledge.query_cost:${usageId}`,
|
|
171
|
+
// Durable receipt inserted before the debit in this same transaction.
|
|
172
|
+
// The service is the writer, not an inferred causal human.
|
|
173
|
+
initiator: { kind: "service", subjectId: "worker:knowledge-query" },
|
|
174
|
+
initiatorContext: { creditDebitAttribution: paidAttribution },
|
|
175
|
+
});
|
|
176
|
+
if (paidSettings && cost > 0)
|
|
177
|
+
await applyCreditDebitAfterUse(queryDb, {
|
|
178
|
+
accountId: context.accountId,
|
|
179
|
+
workspaceId: context.workspaceId,
|
|
180
|
+
type: "document_embedding_debit",
|
|
181
|
+
amountMicros: cost,
|
|
182
|
+
sourceType: "knowledge_query",
|
|
183
|
+
sourceId: usageId,
|
|
184
|
+
idempotencyKey: `knowledge.query_embedding:${usageId}`,
|
|
185
|
+
metadata: {
|
|
186
|
+
model: embedding.model,
|
|
187
|
+
bytes,
|
|
188
|
+
// This context is constructed by the trusted HTTP/MCP host, never
|
|
189
|
+
// from request input. The ledger resolves the immutable exact turn;
|
|
190
|
+
// an API key's subject (or a session creator) is not a human.
|
|
191
|
+
...(billingActor.kind === "agent"
|
|
192
|
+
? { turnId: billingActor.turnId }
|
|
193
|
+
: billingActor.kind === "human"
|
|
194
|
+
? { initiatingHumanSubjectId: billingActor.subjectId }
|
|
195
|
+
: {}),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return { ...found, searchMode: request.mode };
|
|
200
|
+
};
|
|
201
|
+
if (!paidSettings) return retrieveThenSettle(db);
|
|
202
|
+
// Serialize paid queries across all workspaces of the account before
|
|
203
|
+
// checking balance. This is an execution fence, not a credit reservation.
|
|
204
|
+
return withKnowledgeQueryAccountLock(
|
|
205
|
+
db,
|
|
206
|
+
context.accountId,
|
|
207
|
+
context.workspaceId,
|
|
208
|
+
async (lockedDb) => {
|
|
209
|
+
const balance = await getSpendableCreditBalance(lockedDb, context.accountId);
|
|
210
|
+
if (balance.balanceMicros <= 0)
|
|
211
|
+
return keywordFallback(lockedDb, new KnowledgeVectorFundingError(), "awaiting_funding");
|
|
212
|
+
const attribution =
|
|
213
|
+
billingActor.kind === "agent"
|
|
214
|
+
? await creditDebitAttributionForTurn(lockedDb, {
|
|
215
|
+
accountId: context.accountId,
|
|
216
|
+
workspaceId: context.workspaceId,
|
|
217
|
+
turnId: billingActor.turnId,
|
|
218
|
+
})
|
|
219
|
+
: null;
|
|
220
|
+
if (attribution) paidAttribution = attribution;
|
|
221
|
+
const refusal = await checkWorkspaceAllowance(lockedDb, {
|
|
222
|
+
accountId: context.accountId,
|
|
223
|
+
workspaceId: context.workspaceId,
|
|
224
|
+
subjectId:
|
|
225
|
+
billingActor.kind === "human"
|
|
226
|
+
? billingActor.subjectId
|
|
227
|
+
: attribution?.kind === "turn"
|
|
228
|
+
? attribution.initiatingHumanSubjectId
|
|
229
|
+
: null,
|
|
230
|
+
});
|
|
231
|
+
if (refusal)
|
|
232
|
+
return keywordFallback(
|
|
233
|
+
lockedDb,
|
|
234
|
+
Object.assign(new Error(refusal.message), refusal),
|
|
235
|
+
"quota",
|
|
236
|
+
);
|
|
237
|
+
const now = new Date();
|
|
238
|
+
const minuteBytes = await sumUsageQuantity(lockedDb, {
|
|
239
|
+
accountId: context.accountId,
|
|
240
|
+
eventType: "document.query_embedding_bytes",
|
|
241
|
+
since: new Date(now.getTime() - 60_000),
|
|
242
|
+
});
|
|
243
|
+
const monthBytes = await sumUsageQuantity(lockedDb, {
|
|
244
|
+
accountId: context.accountId,
|
|
245
|
+
eventType: "document.query_embedding_bytes",
|
|
246
|
+
since: new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)),
|
|
247
|
+
});
|
|
248
|
+
const minuteMicros = await sumUsageQuantity(lockedDb, {
|
|
249
|
+
accountId: context.accountId,
|
|
250
|
+
eventType: "document.query_embedding_cost",
|
|
251
|
+
since: new Date(now.getTime() - 60_000),
|
|
252
|
+
});
|
|
253
|
+
const monthMicros = await sumUsageQuantity(lockedDb, {
|
|
254
|
+
accountId: context.accountId,
|
|
255
|
+
eventType: "document.query_embedding_cost",
|
|
256
|
+
since: new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)),
|
|
257
|
+
});
|
|
258
|
+
if (
|
|
259
|
+
minuteBytes + bytes > MAX_PAID_QUERY_BYTES_PER_MINUTE ||
|
|
260
|
+
monthBytes + bytes > MAX_PAID_QUERY_BYTES_PER_MONTH ||
|
|
261
|
+
minuteMicros + cost > MAX_PAID_QUERY_MICROS_PER_MINUTE ||
|
|
262
|
+
monthMicros + cost > MAX_PAID_QUERY_MICROS_PER_MONTH
|
|
263
|
+
)
|
|
264
|
+
return keywordFallback(
|
|
265
|
+
lockedDb,
|
|
266
|
+
new KnowledgeVectorQueryRejectedError("quota", "Paid Knowledge query rate limit reached"),
|
|
267
|
+
"quota",
|
|
268
|
+
);
|
|
269
|
+
return retrieveThenSettle(lockedDb);
|
|
270
|
+
},
|
|
271
|
+
);
|
|
55
272
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { connectionAccountIdentityLabel } from "@opengeni/contracts/connection-account-label";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import type { McpServerConfig } from "@opengeni/config";
|
|
4
|
+
import {
|
|
5
|
+
McpConnectionAccountBindings,
|
|
6
|
+
type ConnectionMetadata,
|
|
7
|
+
type McpConnectionAccountBinding,
|
|
8
|
+
type McpConnectionAccountSelection,
|
|
9
|
+
type McpPersonalConnectionDelegation,
|
|
10
|
+
} from "@opengeni/contracts";
|
|
11
|
+
import { ConnectionAccountSelectionError } from "./connection-account-selection-error";
|
|
12
|
+
|
|
13
|
+
/** Account routes are stable across retries, but are never parsed to discover
|
|
14
|
+
* authority. The canonical server is retained separately in the binding. */
|
|
15
|
+
export function mcpAccountRouteId(serverId: string, connectionId: string): string {
|
|
16
|
+
return `account-${createHash("sha256")
|
|
17
|
+
.update(JSON.stringify([serverId, connectionId]))
|
|
18
|
+
.digest("hex")}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function accountLabel(connection: ConnectionMetadata): string {
|
|
22
|
+
const identity = connectionAccountIdentityLabel(
|
|
23
|
+
connection.metadata,
|
|
24
|
+
`${connection.providerDomain} account ${connection.id.slice(0, 8)}`,
|
|
25
|
+
);
|
|
26
|
+
const scope = connection.subjectId === null ? "This workspace" : "Only me";
|
|
27
|
+
return `${identity} · ${scope}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function canonicalResource(value: string): string {
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(value);
|
|
33
|
+
url.hash = "";
|
|
34
|
+
url.pathname = url.pathname.replace(/\/+$/u, "") || "/";
|
|
35
|
+
return url.toString();
|
|
36
|
+
} catch {
|
|
37
|
+
return value.trim();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Pure admission projection over an already authorized inventory. Explicit
|
|
42
|
+
* pairs narrow one connector; omission attaches all eligible accounts. */
|
|
43
|
+
export function mcpAccountBindingsFromVisibleConnections(input: {
|
|
44
|
+
accountId: string;
|
|
45
|
+
workspaceId: string;
|
|
46
|
+
subjectId: string | null;
|
|
47
|
+
servers: McpServerConfig[];
|
|
48
|
+
connections: ConnectionMetadata[];
|
|
49
|
+
selections?: McpConnectionAccountSelection[];
|
|
50
|
+
}): McpConnectionAccountBinding[] {
|
|
51
|
+
const remaining = new Set(
|
|
52
|
+
(input.selections ?? []).map((selection) =>
|
|
53
|
+
JSON.stringify([selection.serverId, selection.connectionId]),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
const bindings: McpConnectionAccountBinding[] = [];
|
|
57
|
+
for (const server of input.servers) {
|
|
58
|
+
const ref = server.connectionRef;
|
|
59
|
+
if (!ref || ref.authoritySource === "host") continue;
|
|
60
|
+
// A selector becomes an exact reference at acceptance; do not persist a
|
|
61
|
+
// selection directive beside the frozen connection UUID.
|
|
62
|
+
const { accountSelection: _accountSelection, ...exactRef } = ref;
|
|
63
|
+
const selections = (input.selections ?? []).filter(
|
|
64
|
+
(selection) => selection.serverId === server.id,
|
|
65
|
+
);
|
|
66
|
+
const selectedIds = new Set(selections.map((selection) => selection.connectionId));
|
|
67
|
+
for (const connection of input.connections) {
|
|
68
|
+
if (
|
|
69
|
+
connection.accountId !== input.accountId ||
|
|
70
|
+
connection.status !== "active" ||
|
|
71
|
+
connection.providerDomain.toLowerCase() !== ref.providerDomain.toLowerCase() ||
|
|
72
|
+
(ref.kind && connection.kind !== ref.kind) ||
|
|
73
|
+
(ref.connectionId !== undefined && ref.connectionId !== connection.id) ||
|
|
74
|
+
(ref.resource &&
|
|
75
|
+
(typeof connection.metadata.resource !== "string" ||
|
|
76
|
+
canonicalResource(connection.metadata.resource) !== canonicalResource(ref.resource))) ||
|
|
77
|
+
(selectedIds.size > 0 && !selectedIds.has(connection.id))
|
|
78
|
+
)
|
|
79
|
+
continue;
|
|
80
|
+
const personal = connection.subjectId !== null;
|
|
81
|
+
if (
|
|
82
|
+
personal
|
|
83
|
+
? !input.subjectId || connection.subjectId !== input.subjectId || !connection.authorityId
|
|
84
|
+
: connection.workspaceId !== input.workspaceId
|
|
85
|
+
)
|
|
86
|
+
continue;
|
|
87
|
+
remaining.delete(JSON.stringify([server.id, connection.id]));
|
|
88
|
+
bindings.push({
|
|
89
|
+
serverId: mcpAccountRouteId(server.id, connection.id),
|
|
90
|
+
canonicalServerId: server.id,
|
|
91
|
+
connectionId: connection.id,
|
|
92
|
+
originWorkspaceId: connection.workspaceId,
|
|
93
|
+
subjectScope: personal ? "subject" : "workspace",
|
|
94
|
+
ownerSubjectId: connection.subjectId,
|
|
95
|
+
accountLabel: accountLabel(connection),
|
|
96
|
+
providerDomain: connection.providerDomain,
|
|
97
|
+
kind: connection.kind,
|
|
98
|
+
connectionRef: {
|
|
99
|
+
...exactRef,
|
|
100
|
+
connectionId: connection.id,
|
|
101
|
+
providerDomain: connection.providerDomain,
|
|
102
|
+
kind: connection.kind,
|
|
103
|
+
subjectScope: personal ? "subject" : "workspace",
|
|
104
|
+
},
|
|
105
|
+
...(connection.connectionAuthorityGeneration
|
|
106
|
+
? { connectionAuthorityGeneration: connection.connectionAuthorityGeneration }
|
|
107
|
+
: {}),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (remaining.size > 0) {
|
|
112
|
+
throw new ConnectionAccountSelectionError(
|
|
113
|
+
"An attached connector account is unavailable. Review its connection settings.",
|
|
114
|
+
{
|
|
115
|
+
version: 1,
|
|
116
|
+
reason: "selected_account_unavailable",
|
|
117
|
+
accounts: (input.selections ?? [])
|
|
118
|
+
.filter((selection) =>
|
|
119
|
+
remaining.has(JSON.stringify([selection.serverId, selection.connectionId])),
|
|
120
|
+
)
|
|
121
|
+
.map((selection) => {
|
|
122
|
+
const server = input.servers.find((candidate) => candidate.id === selection.serverId);
|
|
123
|
+
// Only inspect the caller's already-authorized inventory. Never look
|
|
124
|
+
// up an unavailable account globally merely to improve an error.
|
|
125
|
+
const connection = input.connections.find(
|
|
126
|
+
(candidate) => candidate.id === selection.connectionId,
|
|
127
|
+
);
|
|
128
|
+
return {
|
|
129
|
+
serverId: selection.serverId,
|
|
130
|
+
connectionId: selection.connectionId,
|
|
131
|
+
reason:
|
|
132
|
+
!server?.connectionRef || server.connectionRef.authoritySource === "host"
|
|
133
|
+
? ("connector_unavailable" as const)
|
|
134
|
+
: !connection
|
|
135
|
+
? ("account_not_visible" as const)
|
|
136
|
+
: connection.status !== "active"
|
|
137
|
+
? ("account_inactive" as const)
|
|
138
|
+
: ("account_mismatch" as const),
|
|
139
|
+
};
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return McpConnectionAccountBindings.parse(
|
|
145
|
+
bindings.sort((left, right) => left.serverId.localeCompare(right.serverId)),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Workspace accounts never acquire a fabricated personal owner/delegation. */
|
|
150
|
+
export function personalDelegationsForAccountBindings(
|
|
151
|
+
bindings: McpConnectionAccountBinding[],
|
|
152
|
+
): McpPersonalConnectionDelegation[] {
|
|
153
|
+
return bindings.flatMap((binding) =>
|
|
154
|
+
binding.subjectScope === "subject" && binding.ownerSubjectId
|
|
155
|
+
? [
|
|
156
|
+
{
|
|
157
|
+
serverId: binding.serverId,
|
|
158
|
+
canonicalServerId: binding.canonicalServerId,
|
|
159
|
+
connectionId: binding.connectionId,
|
|
160
|
+
originWorkspaceId: binding.originWorkspaceId,
|
|
161
|
+
ownerSubjectId: binding.ownerSubjectId,
|
|
162
|
+
providerDomain: binding.providerDomain,
|
|
163
|
+
kind: binding.kind,
|
|
164
|
+
connectionType: "mcp" as const,
|
|
165
|
+
},
|
|
166
|
+
]
|
|
167
|
+
: [],
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import type {
|
|
3
|
+
McpAccountRouteLabel,
|
|
4
|
+
McpConnectionAccountBinding,
|
|
5
|
+
ToolRef,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import type { ApiIntegrationRuntime } from "@opengeni/db";
|
|
8
|
+
|
|
9
|
+
const ACCOUNT_SCOPE_SUFFIX = / · (?:Only me|This workspace)$/u;
|
|
10
|
+
|
|
11
|
+
/** "alice@example.com · Only me" → "alice@example.com". */
|
|
12
|
+
function accountIdentity(binding: McpConnectionAccountBinding): string {
|
|
13
|
+
return binding.accountLabel.replace(ACCOUNT_SCOPE_SUFFIX, "") || binding.accountLabel;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Expand only after canonical tool-policy admission. Aliases are execution
|
|
17
|
+
* identities, never an alternate way to gain admission to a connector. */
|
|
18
|
+
export function expandMcpAccountRoutes(input: {
|
|
19
|
+
settings: Settings;
|
|
20
|
+
tools: ToolRef[];
|
|
21
|
+
bindings: readonly McpConnectionAccountBinding[] | null | undefined;
|
|
22
|
+
}): {
|
|
23
|
+
settings: Settings;
|
|
24
|
+
tools: ToolRef[];
|
|
25
|
+
accountLabels: ReadonlyMap<string, McpAccountRouteLabel>;
|
|
26
|
+
} {
|
|
27
|
+
// Only historical work has no binding snapshot. A present empty snapshot is
|
|
28
|
+
// an explicit lack of account authority, never a request for current defaults.
|
|
29
|
+
if (input.bindings == null) {
|
|
30
|
+
return { settings: input.settings, tools: input.tools, accountLabels: new Map() };
|
|
31
|
+
}
|
|
32
|
+
const configured = new Map(input.settings.mcpServers.map((server) => [server.id, server]));
|
|
33
|
+
const byCanonical = new Map<string, McpConnectionAccountBinding[]>();
|
|
34
|
+
const routes = new Set<string>();
|
|
35
|
+
const accounts = new Set<string>();
|
|
36
|
+
for (const binding of input.bindings) {
|
|
37
|
+
const account = JSON.stringify([binding.canonicalServerId, binding.connectionId]);
|
|
38
|
+
if (
|
|
39
|
+
routes.has(binding.serverId) ||
|
|
40
|
+
accounts.has(account) ||
|
|
41
|
+
configured.has(binding.serverId) ||
|
|
42
|
+
(binding.subjectScope === "subject") !== (binding.ownerSubjectId !== null) ||
|
|
43
|
+
binding.connectionRef.authoritySource === "host" ||
|
|
44
|
+
binding.connectionRef.connectionId !== binding.connectionId ||
|
|
45
|
+
binding.connectionRef.subjectScope !== binding.subjectScope ||
|
|
46
|
+
binding.connectionRef.providerDomain !== binding.providerDomain ||
|
|
47
|
+
binding.connectionRef.kind !== binding.kind
|
|
48
|
+
) {
|
|
49
|
+
throw new Error("Invalid accepted MCP account route");
|
|
50
|
+
}
|
|
51
|
+
routes.add(binding.serverId);
|
|
52
|
+
accounts.add(account);
|
|
53
|
+
const siblings = byCanonical.get(binding.canonicalServerId) ?? [];
|
|
54
|
+
siblings.push(binding);
|
|
55
|
+
byCanonical.set(binding.canonicalServerId, siblings);
|
|
56
|
+
}
|
|
57
|
+
const servers: Settings["mcpServers"] = [];
|
|
58
|
+
const tools: ToolRef[] = [];
|
|
59
|
+
const accountLabels = new Map<string, McpAccountRouteLabel>();
|
|
60
|
+
for (const tool of input.tools) {
|
|
61
|
+
const bindings = byCanonical.get(tool.id);
|
|
62
|
+
const canonical = configured.get(tool.id);
|
|
63
|
+
// A connector reconfigured after acceptance (removed, made public, or moved
|
|
64
|
+
// to another provider) drops its frozen account routes. The route is
|
|
65
|
+
// never rebound to the new configuration, and the rest of the turn keeps
|
|
66
|
+
// its other tools instead of failing on one stale connector.
|
|
67
|
+
if (!bindings || !canonical?.connectionRef) {
|
|
68
|
+
if (!canonical?.connectionRef) tools.push(tool);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const usable = bindings.filter(
|
|
72
|
+
(binding) =>
|
|
73
|
+
canonical.connectionRef!.authoritySource !== "host" &&
|
|
74
|
+
canonical.connectionRef!.providerDomain === binding.providerDomain &&
|
|
75
|
+
(!canonical.connectionRef!.kind || canonical.connectionRef!.kind === binding.kind),
|
|
76
|
+
);
|
|
77
|
+
const connector = canonical.name ?? canonical.id;
|
|
78
|
+
// People only need the account to tell several apart. Two accounts with the
|
|
79
|
+
// same identity (personal and workspace) keep their scope to stay distinct.
|
|
80
|
+
const identities = usable.map(accountIdentity);
|
|
81
|
+
for (const [index, binding] of usable.entries()) {
|
|
82
|
+
// Static headers can belong to the formerly selected account. Credentials
|
|
83
|
+
// for account-qualified routes come exclusively from the native resolver.
|
|
84
|
+
const { headers: _headers, ...config } = canonical;
|
|
85
|
+
const label = `${connector} — ${binding.subjectScope === "subject" ? "Personal" : "Workspace"}: ${binding.accountLabel}`;
|
|
86
|
+
const identity = identities[index]!;
|
|
87
|
+
const account =
|
|
88
|
+
usable.length < 2
|
|
89
|
+
? undefined
|
|
90
|
+
: identities.indexOf(identity) === identities.lastIndexOf(identity)
|
|
91
|
+
? identity
|
|
92
|
+
: binding.accountLabel;
|
|
93
|
+
servers.push({
|
|
94
|
+
...config,
|
|
95
|
+
id: binding.serverId,
|
|
96
|
+
name: label,
|
|
97
|
+
connectionRef: structuredClone(binding.connectionRef),
|
|
98
|
+
});
|
|
99
|
+
tools.push({ ...tool, id: binding.serverId });
|
|
100
|
+
accountLabels.set(binding.serverId, {
|
|
101
|
+
model: label,
|
|
102
|
+
connector,
|
|
103
|
+
providerDomain: binding.providerDomain,
|
|
104
|
+
...(account ? { account } : {}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
settings: {
|
|
110
|
+
...input.settings,
|
|
111
|
+
mcpServers: [
|
|
112
|
+
...input.settings.mcpServers.filter((server) => !server.connectionRef),
|
|
113
|
+
...servers,
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
tools,
|
|
117
|
+
accountLabels,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Each adapter closes over its own immutable credential ref and authority
|
|
122
|
+
* generation. Sharing a provider transport never shares a credential resolver. */
|
|
123
|
+
export function expandApiIntegrationAccountRoutes(input: {
|
|
124
|
+
integrations: readonly ApiIntegrationRuntime[];
|
|
125
|
+
bindings: readonly McpConnectionAccountBinding[] | null | undefined;
|
|
126
|
+
tools: readonly ToolRef[];
|
|
127
|
+
}): ApiIntegrationRuntime[] {
|
|
128
|
+
const selected = new Set(input.tools.map((tool) => tool.id));
|
|
129
|
+
if (input.bindings == null)
|
|
130
|
+
return input.integrations.filter((item) => selected.has(item.serverId));
|
|
131
|
+
const acceptedBindings = input.bindings;
|
|
132
|
+
return input.integrations.flatMap((integration) => {
|
|
133
|
+
const bindings = acceptedBindings.filter(
|
|
134
|
+
(binding) => binding.canonicalServerId === integration.serverId,
|
|
135
|
+
);
|
|
136
|
+
if (bindings.length === 0)
|
|
137
|
+
return !integration.connectionRef && selected.has(integration.serverId) ? [integration] : [];
|
|
138
|
+
return bindings
|
|
139
|
+
.filter((binding) => selected.has(binding.serverId))
|
|
140
|
+
.map((binding) => {
|
|
141
|
+
if (binding.connectionAuthorityGeneration === undefined) {
|
|
142
|
+
throw new Error("Accepted API integration account route has no authority generation");
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
...integration,
|
|
146
|
+
serverId: binding.serverId,
|
|
147
|
+
connectionRef: structuredClone(binding.connectionRef),
|
|
148
|
+
connectionAuthorityGeneration: binding.connectionAuthorityGeneration,
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { McpServerConnectionRef } from "@opengeni/contracts";
|
|
2
|
+
import { HTTPException } from "hono/http-exception";
|
|
3
|
+
|
|
4
|
+
/** Reject retired authority sources instead of aliasing them to native IDs. */
|
|
5
|
+
export function assertNativeMcpConnectionRef(
|
|
6
|
+
connectionRef: McpServerConnectionRef | null | undefined,
|
|
7
|
+
): void {
|
|
8
|
+
if (connectionRef?.authoritySource === "host") {
|
|
9
|
+
throw new HTTPException(422, {
|
|
10
|
+
message:
|
|
11
|
+
"host-owned MCP connection refs are no longer supported; select an ordinary native connection",
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|