@opengeni/core 0.28.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +1 -1
  2. package/dist/access/external-actor-authority.d.ts +47 -0
  3. package/dist/access/index.d.ts +182 -3
  4. package/dist/access/service-initiator.d.ts +3 -0
  5. package/dist/application/api-integration-servers.d.ts +20 -0
  6. package/dist/application/archived-session-imports.d.ts +6 -0
  7. package/dist/application/composer-submit.d.ts +24 -0
  8. package/dist/application/connect-authority.d.ts +13 -0
  9. package/dist/application/connect-operation.d.ts +31 -0
  10. package/dist/application/external-continuation.d.ts +15 -0
  11. package/dist/application/external-identity-lifecycle.d.ts +20 -0
  12. package/dist/application/external-link-work-admission.d.ts +12 -0
  13. package/dist/application/external-workspace-members.d.ts +34 -0
  14. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  15. package/dist/application/new-session-drafts.d.ts +39 -2
  16. package/dist/application/sandbox-recovery.d.ts +58 -0
  17. package/dist/application/session-commands.d.ts +26 -13
  18. package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
  19. package/dist/application/session-tenancy.d.ts +19 -0
  20. package/dist/application/user-resource-grants.d.ts +20 -0
  21. package/dist/billing/limits.d.ts +11 -0
  22. package/dist/canonical-human-identities.d.ts +7 -2
  23. package/dist/canonical-human-identities.js +17 -4
  24. package/dist/canonical-human-identities.js.map +1 -1
  25. package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
  26. package/dist/chunk-2MKMRGSY.js.map +1 -0
  27. package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
  28. package/dist/chunk-GVO3N37G.js.map +1 -0
  29. package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
  30. package/dist/chunk-LCQXNR62.js.map +1 -0
  31. package/dist/chunk-MM6N7CTO.js +387 -0
  32. package/dist/chunk-MM6N7CTO.js.map +1 -0
  33. package/dist/chunk-QO5GVFFO.js +235 -0
  34. package/dist/chunk-QO5GVFFO.js.map +1 -0
  35. package/dist/codex-model-availability.d.ts +14 -0
  36. package/dist/default-session-model.d.ts +139 -0
  37. package/dist/dependencies.d.ts +87 -4
  38. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  39. package/dist/domain/agent-config-resolution.d.ts +49 -0
  40. package/dist/domain/automations.d.ts +58 -0
  41. package/dist/domain/capabilities.d.ts +22 -4
  42. package/dist/domain/company-profile-agent-admin.d.ts +25 -0
  43. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  44. package/dist/domain/connector-tool-permissions.d.ts +36 -0
  45. package/dist/domain/direct-model-provider.d.ts +3 -0
  46. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  47. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  48. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  49. package/dist/domain/editable-artifacts/service.d.ts +4 -3
  50. package/dist/domain/environments.d.ts +5 -9
  51. package/dist/domain/external-creation-attribution.d.ts +6 -0
  52. package/dist/domain/file-owner.d.ts +8 -0
  53. package/dist/domain/github-action-policies.d.ts +36 -0
  54. package/dist/domain/github-repository-bindings.d.ts +117 -0
  55. package/dist/domain/github-skill-source.d.ts +5 -0
  56. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  57. package/dist/domain/insights-timing.d.ts +16 -0
  58. package/dist/domain/insights-usage.d.ts +27 -0
  59. package/dist/domain/insights.d.ts +28 -1
  60. package/dist/domain/knowledge-files.d.ts +20 -0
  61. package/dist/domain/knowledge-messages.d.ts +39 -0
  62. package/dist/domain/knowledge-messages.test.d.ts +1 -0
  63. package/dist/domain/knowledge-preparation.d.ts +10 -0
  64. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  65. package/dist/domain/knowledge-search.d.ts +15 -0
  66. package/dist/domain/knowledge-search.test.d.ts +1 -0
  67. package/dist/domain/knowledge.d.ts +9 -0
  68. package/dist/domain/mcp-account-bindings.d.ts +19 -0
  69. package/dist/domain/mcp-account-routes.d.ts +21 -0
  70. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  71. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  72. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  73. package/dist/domain/personal-connection-delegations.d.ts +87 -4
  74. package/dist/domain/pr-review.d.ts +249 -0
  75. package/dist/domain/resources.d.ts +24 -3
  76. package/dist/domain/scheduled-task-access.d.ts +247 -0
  77. package/dist/domain/scheduled-tasks.d.ts +103 -6
  78. package/dist/domain/session-connection-accounts.d.ts +16 -0
  79. package/dist/domain/session-file-access.d.ts +4 -0
  80. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  81. package/dist/domain/session-start-timing.d.ts +7 -0
  82. package/dist/domain/session-tool-policy.d.ts +93 -4
  83. package/dist/domain/sessions.d.ts +275 -28
  84. package/dist/domain/skill-imports.d.ts +1 -0
  85. package/dist/domain/skill-search.d.ts +30 -0
  86. package/dist/domain/skills.d.ts +117 -0
  87. package/dist/domain/slack-bot.d.ts +46 -1
  88. package/dist/domain/workspace-members.d.ts +8 -1
  89. package/dist/editable-artifact-live/application.d.ts +12 -3
  90. package/dist/editable-artifact-live/ports.d.ts +0 -1
  91. package/dist/editable-artifact-live/server.d.ts +5 -2
  92. package/dist/editable-artifact-live/ticket.d.ts +2 -2
  93. package/dist/editable-artifact-live/types.d.ts +14 -2
  94. package/dist/editable-artifact-live.js +2 -2
  95. package/dist/editable-artifacts.js +10 -4
  96. package/dist/index.d.ts +49 -3
  97. package/dist/index.js +18812 -6833
  98. package/dist/index.js.map +1 -1
  99. package/dist/managed-auth-session-sets.d.ts +131 -0
  100. package/dist/managed-auth-session-sets.js +51 -0
  101. package/dist/managed-auth-session-sets.js.map +1 -0
  102. package/dist/managed-session.d.ts +98 -11
  103. package/dist/model-catalog.d.ts +117 -0
  104. package/dist/product-usage-metrics.d.ts +30 -0
  105. package/dist/rigs/index.d.ts +22 -5
  106. package/dist/rigs/provider-images.d.ts +1 -1
  107. package/dist/sandbox/fleet.d.ts +59 -10
  108. package/dist/sandbox/routing.d.ts +22 -2
  109. package/dist/sandbox/runtime-settings.d.ts +56 -0
  110. package/dist/session-authorization.d.ts +38 -1
  111. package/dist/site-session-origin.d.ts +9 -0
  112. package/dist/transcription.d.ts +12 -1
  113. package/dist/turn-surface.d.ts +18 -0
  114. package/dist/user-presence.d.ts +32 -0
  115. package/package.json +18 -11
  116. package/src/access/external-actor-authority.ts +94 -0
  117. package/src/access/index.ts +1386 -44
  118. package/src/access/service-initiator.ts +50 -0
  119. package/src/application/api-integration-servers.ts +215 -0
  120. package/src/application/archived-session-imports.ts +161 -0
  121. package/src/application/composer-submit.ts +77 -0
  122. package/src/application/connect-authority.ts +77 -0
  123. package/src/application/connect-operation.ts +80 -0
  124. package/src/application/external-continuation.ts +129 -0
  125. package/src/application/external-identity-lifecycle.ts +48 -0
  126. package/src/application/external-link-work-admission.ts +90 -0
  127. package/src/application/external-workspace-members.ts +232 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +222 -29
  130. package/src/application/sandbox-recovery.ts +78 -0
  131. package/src/application/session-commands.ts +430 -73
  132. package/src/application/session-mcp-credential-rotation.ts +217 -0
  133. package/src/application/session-tenancy.ts +445 -0
  134. package/src/application/user-resource-grants.ts +154 -0
  135. package/src/billing/limits.ts +103 -24
  136. package/src/canonical-human-identities.ts +22 -3
  137. package/src/codex-model-availability.ts +136 -0
  138. package/src/default-session-model.ts +656 -0
  139. package/src/dependencies.ts +97 -5
  140. package/src/developer-setup-credential-ceiling.ts +60 -0
  141. package/src/domain/agent-config-resolution.ts +148 -0
  142. package/src/domain/automations.ts +270 -0
  143. package/src/domain/capabilities.ts +244 -151
  144. package/src/domain/company-profile-agent-admin.ts +56 -0
  145. package/src/domain/connection-account-selection-error.ts +19 -0
  146. package/src/domain/connector-tool-permissions.ts +371 -0
  147. package/src/domain/direct-model-provider.ts +62 -0
  148. package/src/domain/editable-artifacts/agent-application.ts +61 -15
  149. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  150. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  151. package/src/domain/editable-artifacts/ports.ts +11 -0
  152. package/src/domain/editable-artifacts/service.ts +32 -1
  153. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  154. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  155. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  156. package/src/domain/environments.ts +25 -45
  157. package/src/domain/external-creation-attribution.ts +22 -0
  158. package/src/domain/file-owner.ts +67 -0
  159. package/src/domain/github-action-policies.ts +169 -0
  160. package/src/domain/github-repository-bindings.ts +295 -0
  161. package/src/domain/github-skill-source.ts +233 -0
  162. package/src/domain/governed-learning-slack-publication.ts +157 -0
  163. package/src/domain/insights-timing.ts +95 -0
  164. package/src/domain/insights-usage.ts +67 -0
  165. package/src/domain/insights.ts +270 -169
  166. package/src/domain/knowledge-files.ts +46 -0
  167. package/src/domain/knowledge-messages.test.ts +91 -0
  168. package/src/domain/knowledge-messages.ts +106 -0
  169. package/src/domain/knowledge-preparation.test.ts +276 -0
  170. package/src/domain/knowledge-preparation.ts +147 -0
  171. package/src/domain/knowledge-search.test.ts +561 -0
  172. package/src/domain/knowledge-search.ts +256 -0
  173. package/src/domain/knowledge.ts +133 -0
  174. package/src/domain/mcp-account-bindings.ts +171 -0
  175. package/src/domain/mcp-account-routes.ts +121 -0
  176. package/src/domain/native-mcp-connection-admission.ts +14 -0
  177. package/src/domain/organization-integration-catalog.ts +88 -0
  178. package/src/domain/organization-membership-lifecycle.ts +10 -0
  179. package/src/domain/personal-connection-delegations.ts +758 -115
  180. package/src/domain/pr-review.ts +687 -0
  181. package/src/domain/resources.ts +182 -25
  182. package/src/domain/scheduled-task-access.ts +1052 -0
  183. package/src/domain/scheduled-tasks.ts +1952 -215
  184. package/src/domain/session-connection-accounts.ts +67 -0
  185. package/src/domain/session-file-access.ts +47 -0
  186. package/src/domain/session-native-connection-replacement.ts +64 -0
  187. package/src/domain/session-start-timing.ts +55 -0
  188. package/src/domain/session-tool-policy.ts +443 -19
  189. package/src/domain/sessions.ts +3417 -619
  190. package/src/domain/skill-imports.ts +137 -17
  191. package/src/domain/skill-search.ts +221 -0
  192. package/src/domain/skills.ts +104 -0
  193. package/src/domain/slack-bot.ts +128 -3
  194. package/src/domain/workspace-members.ts +34 -2
  195. package/src/editable-artifact-live/application.ts +29 -4
  196. package/src/editable-artifact-live/ports.ts +0 -1
  197. package/src/editable-artifact-live/server.ts +167 -13
  198. package/src/editable-artifact-live/ticket.ts +87 -6
  199. package/src/editable-artifact-live/types.ts +16 -2
  200. package/src/editable-artifact-live/wire.ts +0 -10
  201. package/src/index.ts +69 -4
  202. package/src/managed-auth-session-sets.ts +371 -0
  203. package/src/managed-session.ts +520 -2
  204. package/src/model-catalog.ts +873 -0
  205. package/src/product-usage-metrics.ts +101 -0
  206. package/src/rigs/index.ts +179 -63
  207. package/src/rigs/provider-images.ts +2 -3
  208. package/src/sandbox/fleet.ts +471 -59
  209. package/src/sandbox/routing.ts +160 -33
  210. package/src/sandbox/runtime-settings.ts +281 -0
  211. package/src/session-authorization.ts +262 -78
  212. package/src/site-session-origin.ts +22 -0
  213. package/src/transcription.ts +13 -1
  214. package/src/turn-surface.ts +45 -0
  215. package/src/user-presence.ts +134 -0
  216. package/dist/chunk-6WKPWE5S.js.map +0 -1
  217. package/dist/chunk-IBOEYG6N.js +0 -34
  218. package/dist/chunk-IBOEYG6N.js.map +0 -1
  219. package/dist/chunk-JJU6I5XD.js.map +0 -1
  220. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  221. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  222. package/dist/domain/packs.d.ts +0 -55
  223. package/src/domain/durable-learning-slack-publication.ts +0 -202
  224. package/src/domain/packs.ts +0 -556
@@ -0,0 +1,256 @@
1
+ import { KnowledgeEntryListRequest, type KnowledgeEntryListResponse } from "@opengeni/contracts";
2
+ import type { Settings } from "@opengeni/config";
3
+ import {
4
+ applyCreditDebitAfterUse,
5
+ checkWorkspaceAllowance,
6
+ creditDebitAttributionForTurn,
7
+ getBillingBalance,
8
+ listKnowledgeEntries,
9
+ recordUsageEvent,
10
+ sumUsageQuantity,
11
+ withKnowledgeQueryAccountLock,
12
+ type Database,
13
+ type KnowledgeContext,
14
+ type CreditDebitAttribution,
15
+ } from "@opengeni/db";
16
+ import type { DocumentEmbedder } from "@opengeni/documents";
17
+ import type { z } from "zod";
18
+ import { documentEmbeddingCostMicros, paidDocumentEmbedding } from "../billing/limits";
19
+
20
+ // Safety ceilings, not a commercial tariff. Paid queries have no durable
21
+ // reusable vector cache, so each bounded request consumes the provider once.
22
+ const MAX_PAID_QUERY_BYTES = 4096;
23
+ const MAX_PAID_QUERY_MICROS = 1000;
24
+ const MAX_PAID_QUERY_BYTES_PER_MINUTE = 64 * 1024;
25
+ const MAX_PAID_QUERY_BYTES_PER_MONTH = 8 * 1024 * 1024;
26
+ const MAX_PAID_QUERY_MICROS_PER_MINUTE = 10_000;
27
+ const MAX_PAID_QUERY_MICROS_PER_MONTH = 1_000_000;
28
+
29
+ export class KnowledgeVectorFundingError extends Error {
30
+ readonly code = "knowledge_vector_funding_required";
31
+ constructor() {
32
+ super("Knowledge vector search needs Opengeni credits; keyword search remains available.");
33
+ this.name = "KnowledgeVectorFundingError";
34
+ }
35
+ }
36
+
37
+ export class KnowledgeVectorQueryRejectedError extends Error {
38
+ constructor(
39
+ public readonly code: "pagination_unavailable" | "query_limit" | "quota",
40
+ message: string,
41
+ ) {
42
+ super(message);
43
+ this.name = "KnowledgeVectorQueryRejectedError";
44
+ }
45
+ }
46
+
47
+ /** Shared search path for HTTP and both first-party retrieval servers. */
48
+ export async function searchKnowledgeEntries(
49
+ db: Database,
50
+ context: KnowledgeContext,
51
+ input: z.input<typeof KnowledgeEntryListRequest>,
52
+ embedder: () => DocumentEmbedder,
53
+ settings?: Settings,
54
+ ): Promise<KnowledgeEntryListResponse> {
55
+ const request = KnowledgeEntryListRequest.parse(input);
56
+ // Copy trusted actor fields before any provider/lock await. Request lifetime
57
+ // mutation cannot replace the initiating identity between admission/debit.
58
+ const billingActor = { ...context.actor };
59
+ let paidAttribution: CreditDebitAttribution =
60
+ billingActor.kind === "human"
61
+ ? { kind: "human", initiatingHumanSubjectId: billingActor.subjectId }
62
+ : billingActor.kind === "service"
63
+ ? { kind: "service" }
64
+ : { kind: "unknown" };
65
+ if (!request.query || request.mode === "keyword") {
66
+ return {
67
+ ...(await listKnowledgeEntries(db, context, request)),
68
+ searchMode: "keyword" as const,
69
+ };
70
+ }
71
+ // Hybrid queries are natural language, not a conjunction of every synonym.
72
+ // Keep lexical recall useful when embeddings or vector indexing are unavailable.
73
+ // Explicit keyword syntax and keyword-mode queries retain their exact semantics.
74
+ const lexicalQuery =
75
+ /^[\p{L}\p{N}\s]+$/u.test(request.query) && !/\bOR\b/i.test(request.query)
76
+ ? [...new Set(request.query.trim().split(/\s+/u))].map((term) => `"${term}"`).join(" OR ")
77
+ : request.query;
78
+ const lexicalRequest = KnowledgeEntryListRequest.safeParse({ ...request, query: lexicalQuery });
79
+ const retrievalRequest =
80
+ request.mode === "hybrid" && lexicalRequest.success ? lexicalRequest.data : request;
81
+ const keywordFallback = async (
82
+ queryDb: Database,
83
+ error: unknown,
84
+ fallbackReason: "awaiting_funding" | "quota" | "provider_unavailable" | "query_limit",
85
+ ) => {
86
+ if (request.mode === "vector") throw error;
87
+ return {
88
+ ...(await listKnowledgeEntries(queryDb, context, { ...retrievalRequest, mode: "keyword" })),
89
+ searchMode: "keyword" as const,
90
+ fallbackReason,
91
+ };
92
+ };
93
+ const bytes = Buffer.byteLength(request.query, "utf8");
94
+ const paidSettings = settings && paidDocumentEmbedding(settings) ? settings : undefined;
95
+ // Paid cursors would re-embed and re-charge on each page. Until a durable
96
+ // vector cache exists, require explicit keyword mode to paginate instead.
97
+ if (paidSettings && request.cursor)
98
+ throw new KnowledgeVectorQueryRejectedError(
99
+ "pagination_unavailable",
100
+ "Paid semantic Knowledge pagination is unavailable; use keyword mode",
101
+ );
102
+ const cost = paidSettings ? documentEmbeddingCostMicros(paidSettings, bytes) : 0;
103
+ if (paidSettings && (bytes > MAX_PAID_QUERY_BYTES || cost > MAX_PAID_QUERY_MICROS))
104
+ return keywordFallback(
105
+ db,
106
+ new KnowledgeVectorQueryRejectedError(
107
+ "query_limit",
108
+ "Paid Knowledge query exceeds the per-request limit",
109
+ ),
110
+ "query_limit",
111
+ );
112
+
113
+ const retrieveThenSettle = async (queryDb: Database) => {
114
+ let embedding: { model: string; values: number[] };
115
+ try {
116
+ const provider = embedder();
117
+ const values = await provider.embedQuery(request.query!);
118
+ if (
119
+ values.length !== provider.dimensions ||
120
+ values.some((value) => !Number.isFinite(value)) ||
121
+ !values.some((value) => value !== 0)
122
+ )
123
+ throw new Error("Knowledge query embedding is unavailable");
124
+ embedding = { model: provider.model, values };
125
+ } catch (error) {
126
+ // Only provider failure may fall back. Retrieval and settlement errors
127
+ // after use surface; they never masquerade as a successful keyword hit.
128
+ return keywordFallback(queryDb, error, "provider_unavailable");
129
+ }
130
+ // Read before billing, in the same transaction for the paid mode. Failure
131
+ // here leaves no customer charge and no vector result to return.
132
+ const found = await listKnowledgeEntries(queryDb, context, retrievalRequest, embedding);
133
+ if (settings) {
134
+ const usageId = crypto.randomUUID();
135
+ await recordUsageEvent(queryDb, {
136
+ accountId: context.accountId,
137
+ workspaceId: context.workspaceId,
138
+ eventType: "document.query_embedding_bytes",
139
+ quantity: bytes,
140
+ unit: "byte",
141
+ sourceResourceType: "knowledge_query",
142
+ sourceResourceId: usageId,
143
+ idempotencyKey: `knowledge.query_bytes:${usageId}`,
144
+ });
145
+ if (paidSettings && cost > 0)
146
+ await recordUsageEvent(queryDb, {
147
+ accountId: context.accountId,
148
+ workspaceId: context.workspaceId,
149
+ eventType: "document.query_embedding_cost",
150
+ quantity: cost,
151
+ unit: "micro_usd",
152
+ sourceResourceType: "knowledge_query",
153
+ sourceResourceId: usageId,
154
+ idempotencyKey: `knowledge.query_cost:${usageId}`,
155
+ // Durable receipt inserted before the debit in this same transaction.
156
+ // The service is the writer, not an inferred causal human.
157
+ initiator: { kind: "service", subjectId: "worker:knowledge-query" },
158
+ initiatorContext: { creditDebitAttribution: paidAttribution },
159
+ });
160
+ if (paidSettings && cost > 0)
161
+ await applyCreditDebitAfterUse(queryDb, {
162
+ accountId: context.accountId,
163
+ workspaceId: context.workspaceId,
164
+ type: "document_embedding_debit",
165
+ amountMicros: cost,
166
+ sourceType: "knowledge_query",
167
+ sourceId: usageId,
168
+ idempotencyKey: `knowledge.query_embedding:${usageId}`,
169
+ metadata: {
170
+ model: embedding.model,
171
+ bytes,
172
+ // This context is constructed by the trusted HTTP/MCP host, never
173
+ // from request input. The ledger resolves the immutable exact turn;
174
+ // an API key's subject (or a session creator) is not a human.
175
+ ...(billingActor.kind === "agent"
176
+ ? { turnId: billingActor.turnId }
177
+ : billingActor.kind === "human"
178
+ ? { initiatingHumanSubjectId: billingActor.subjectId }
179
+ : {}),
180
+ },
181
+ });
182
+ }
183
+ return { ...found, searchMode: request.mode };
184
+ };
185
+ if (!paidSettings) return retrieveThenSettle(db);
186
+ // Serialize paid queries across all workspaces of the account before
187
+ // checking balance. This is an execution fence, not a credit reservation.
188
+ return withKnowledgeQueryAccountLock(
189
+ db,
190
+ context.accountId,
191
+ context.workspaceId,
192
+ async (lockedDb) => {
193
+ const balance = await getBillingBalance(lockedDb, context.accountId);
194
+ if (balance.balanceMicros <= 0)
195
+ return keywordFallback(lockedDb, new KnowledgeVectorFundingError(), "awaiting_funding");
196
+ const attribution =
197
+ billingActor.kind === "agent"
198
+ ? await creditDebitAttributionForTurn(lockedDb, {
199
+ accountId: context.accountId,
200
+ workspaceId: context.workspaceId,
201
+ turnId: billingActor.turnId,
202
+ })
203
+ : null;
204
+ if (attribution) paidAttribution = attribution;
205
+ const refusal = await checkWorkspaceAllowance(lockedDb, {
206
+ accountId: context.accountId,
207
+ workspaceId: context.workspaceId,
208
+ subjectId:
209
+ billingActor.kind === "human"
210
+ ? billingActor.subjectId
211
+ : attribution?.kind === "turn"
212
+ ? attribution.initiatingHumanSubjectId
213
+ : null,
214
+ });
215
+ if (refusal)
216
+ return keywordFallback(
217
+ lockedDb,
218
+ Object.assign(new Error(refusal.message), refusal),
219
+ "quota",
220
+ );
221
+ const now = new Date();
222
+ const minuteBytes = await sumUsageQuantity(lockedDb, {
223
+ accountId: context.accountId,
224
+ eventType: "document.query_embedding_bytes",
225
+ since: new Date(now.getTime() - 60_000),
226
+ });
227
+ const monthBytes = await sumUsageQuantity(lockedDb, {
228
+ accountId: context.accountId,
229
+ eventType: "document.query_embedding_bytes",
230
+ since: new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)),
231
+ });
232
+ const minuteMicros = await sumUsageQuantity(lockedDb, {
233
+ accountId: context.accountId,
234
+ eventType: "document.query_embedding_cost",
235
+ since: new Date(now.getTime() - 60_000),
236
+ });
237
+ const monthMicros = await sumUsageQuantity(lockedDb, {
238
+ accountId: context.accountId,
239
+ eventType: "document.query_embedding_cost",
240
+ since: new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)),
241
+ });
242
+ if (
243
+ minuteBytes + bytes > MAX_PAID_QUERY_BYTES_PER_MINUTE ||
244
+ monthBytes + bytes > MAX_PAID_QUERY_BYTES_PER_MONTH ||
245
+ minuteMicros + cost > MAX_PAID_QUERY_MICROS_PER_MINUTE ||
246
+ monthMicros + cost > MAX_PAID_QUERY_MICROS_PER_MONTH
247
+ )
248
+ return keywordFallback(
249
+ lockedDb,
250
+ new KnowledgeVectorQueryRejectedError("quota", "Paid Knowledge query rate limit reached"),
251
+ "quota",
252
+ );
253
+ return retrieveThenSettle(lockedDb);
254
+ },
255
+ );
256
+ }
@@ -0,0 +1,133 @@
1
+ import type { AccessGrant, KnowledgeEntryScope, Permission } from "@opengeni/contracts";
2
+ import type { KnowledgeContext } from "@opengeni/db";
3
+ import { hasPermission, type AccessGrantAuthorization } from "../access";
4
+ import { requireLiveAgentAttemptAuthorization } from "../session-authorization";
5
+ import type { ApiRouteDeps } from "../dependencies";
6
+ import { HTTPException } from "hono/http-exception";
7
+
8
+ /** A delegated MCP gateway can retrieve shared Knowledge. Its subject label
9
+ * does not establish personal ownership or human review authority. */
10
+ export async function knowledgeContextForGateway(
11
+ deps: Pick<ApiRouteDeps, "db">,
12
+ grant: AccessGrant,
13
+ ): Promise<KnowledgeContext> {
14
+ if (!hasPermission(grant.permissions, "documents:search"))
15
+ throw new HTTPException(403, { message: "Missing permission: documents:search" });
16
+ if (grant.principalKind === "agent_attempt") {
17
+ const sessionId = grant.metadata?.sessionId;
18
+ if (typeof sessionId !== "string")
19
+ throw new HTTPException(403, { message: "Knowledge attempt unavailable" });
20
+ const attempt = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
21
+ return {
22
+ accountId: grant.accountId,
23
+ workspaceId: grant.workspaceId,
24
+ actor: {
25
+ kind: "agent",
26
+ sessionId: attempt.callerSessionId,
27
+ turnId: attempt.turnId,
28
+ attemptId: attempt.attemptId,
29
+ executionGeneration: attempt.executionGeneration,
30
+ },
31
+ };
32
+ }
33
+ return {
34
+ accountId: grant.accountId,
35
+ workspaceId: grant.workspaceId,
36
+ actor: {
37
+ kind: "service",
38
+ principalKind: "mcp_gateway",
39
+ subjectId: grant.subjectId,
40
+ writeScopes: [],
41
+ review: false,
42
+ settingsScopes: [],
43
+ },
44
+ };
45
+ }
46
+
47
+ /** One host boundary for HTTP, SDK-through-HTTP and first-party Knowledge tools. */
48
+ export async function knowledgeContextForAccess(
49
+ deps: Pick<ApiRouteDeps, "db">,
50
+ access: AccessGrantAuthorization,
51
+ permission: Permission,
52
+ ): Promise<KnowledgeContext> {
53
+ const { grant } = access;
54
+ if (!hasPermission(grant.permissions, permission)) {
55
+ throw new HTTPException(403, { message: `Missing permission: ${permission}` });
56
+ }
57
+ if (grant.principalKind === "agent_attempt") {
58
+ const sessionId = grant.metadata?.sessionId;
59
+ if (typeof sessionId !== "string")
60
+ throw new HTTPException(403, { message: "Knowledge attempt unavailable" });
61
+ const attempt = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
62
+ return {
63
+ accountId: grant.accountId,
64
+ workspaceId: grant.workspaceId,
65
+ actor: {
66
+ kind: "agent",
67
+ sessionId: attempt.callerSessionId,
68
+ turnId: attempt.turnId,
69
+ attemptId: attempt.attemptId,
70
+ executionGeneration: attempt.executionGeneration,
71
+ },
72
+ };
73
+ }
74
+ if (
75
+ grant.principalKind === "service" ||
76
+ grant.principalKind === "api_key" ||
77
+ grant.principalKind === "configured_key"
78
+ ) {
79
+ const writeScopes: Array<"workspace" | "organization"> = hasPermission(
80
+ grant.permissions,
81
+ "documents:manage",
82
+ )
83
+ ? ["workspace"]
84
+ : [];
85
+ if (writeScopes.length && access.accountGrant?.permissions.includes("account:admin"))
86
+ writeScopes.push("organization");
87
+ return {
88
+ accountId: grant.accountId,
89
+ workspaceId: grant.workspaceId,
90
+ actor: {
91
+ kind: "service",
92
+ principalKind: grant.principalKind,
93
+ subjectId: grant.subjectId,
94
+ writeScopes,
95
+ review: false,
96
+ settingsScopes: [],
97
+ },
98
+ };
99
+ }
100
+ if (
101
+ grant.principalKind !== "human_session" ||
102
+ !access.contextIntegrity ||
103
+ access.authenticatedSubjectId !== grant.subjectId ||
104
+ grant.serviceInitiator ||
105
+ grant.serviceInitiatorContext ||
106
+ grant.metadata?.attemptId !== undefined ||
107
+ grant.metadata?.turnId !== undefined
108
+ ) {
109
+ throw new HTTPException(403, {
110
+ message: "Knowledge management requires an authenticated human or agent attempt",
111
+ });
112
+ }
113
+ const canWrite = hasPermission(grant.permissions, "documents:manage");
114
+ const writeScopes: KnowledgeEntryScope[] = canWrite ? ["workspace", "personal"] : [];
115
+ // Organization authority does not follow from the workspace-admin wildcard.
116
+ if (canWrite && access.accountGrant?.permissions.includes("account:admin"))
117
+ writeScopes.push("organization");
118
+ return {
119
+ accountId: grant.accountId,
120
+ workspaceId: grant.workspaceId,
121
+ actor: {
122
+ kind: "human",
123
+ principalKind: "human_session",
124
+ subjectId: access.authenticatedSubjectId,
125
+ writeScopes,
126
+ review: canWrite,
127
+ settingsScopes: [
128
+ "personal",
129
+ ...(hasPermission(grant.permissions, "workspace:admin") ? ["workspace" as const] : []),
130
+ ],
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,171 @@
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
+ /** The complete accepted set, including an explicitly empty set. */
51
+ selectionsFrozen?: boolean;
52
+ }): McpConnectionAccountBinding[] {
53
+ const remaining = new Set(
54
+ (input.selections ?? []).map((selection) =>
55
+ JSON.stringify([selection.serverId, selection.connectionId]),
56
+ ),
57
+ );
58
+ const bindings: McpConnectionAccountBinding[] = [];
59
+ for (const server of input.servers) {
60
+ const ref = server.connectionRef;
61
+ if (!ref || ref.authoritySource === "host") continue;
62
+ // A selector becomes an exact reference at acceptance; do not persist a
63
+ // selection directive beside the frozen connection UUID.
64
+ const { accountSelection: _accountSelection, ...exactRef } = ref;
65
+ const selections = (input.selections ?? []).filter(
66
+ (selection) => selection.serverId === server.id,
67
+ );
68
+ const selectedIds = new Set(selections.map((selection) => selection.connectionId));
69
+ for (const connection of input.connections) {
70
+ if (
71
+ connection.accountId !== input.accountId ||
72
+ connection.status !== "active" ||
73
+ connection.providerDomain.toLowerCase() !== ref.providerDomain.toLowerCase() ||
74
+ (ref.kind && connection.kind !== ref.kind) ||
75
+ (ref.connectionId !== undefined && ref.connectionId !== connection.id) ||
76
+ (ref.resource &&
77
+ (typeof connection.metadata.resource !== "string" ||
78
+ canonicalResource(connection.metadata.resource) !== canonicalResource(ref.resource))) ||
79
+ ((input.selectionsFrozen || selectedIds.size > 0) && !selectedIds.has(connection.id))
80
+ )
81
+ continue;
82
+ const personal = connection.subjectId !== null;
83
+ if (
84
+ personal
85
+ ? !input.subjectId || connection.subjectId !== input.subjectId || !connection.authorityId
86
+ : connection.workspaceId !== input.workspaceId
87
+ )
88
+ continue;
89
+ remaining.delete(JSON.stringify([server.id, connection.id]));
90
+ bindings.push({
91
+ serverId: mcpAccountRouteId(server.id, connection.id),
92
+ canonicalServerId: server.id,
93
+ connectionId: connection.id,
94
+ originWorkspaceId: connection.workspaceId,
95
+ subjectScope: personal ? "subject" : "workspace",
96
+ ownerSubjectId: connection.subjectId,
97
+ accountLabel: accountLabel(connection),
98
+ providerDomain: connection.providerDomain,
99
+ kind: connection.kind,
100
+ connectionRef: {
101
+ ...exactRef,
102
+ connectionId: connection.id,
103
+ providerDomain: connection.providerDomain,
104
+ kind: connection.kind,
105
+ subjectScope: personal ? "subject" : "workspace",
106
+ },
107
+ ...(connection.connectionAuthorityGeneration
108
+ ? { connectionAuthorityGeneration: connection.connectionAuthorityGeneration }
109
+ : {}),
110
+ });
111
+ }
112
+ }
113
+ if (remaining.size > 0) {
114
+ throw new ConnectionAccountSelectionError(
115
+ "An attached connector account is unavailable. Review its connection settings.",
116
+ {
117
+ version: 1,
118
+ reason: "selected_account_unavailable",
119
+ accounts: (input.selections ?? [])
120
+ .filter((selection) =>
121
+ remaining.has(JSON.stringify([selection.serverId, selection.connectionId])),
122
+ )
123
+ .map((selection) => {
124
+ const server = input.servers.find((candidate) => candidate.id === selection.serverId);
125
+ // Only inspect the caller's already-authorized inventory. Never look
126
+ // up an unavailable account globally merely to improve an error.
127
+ const connection = input.connections.find(
128
+ (candidate) => candidate.id === selection.connectionId,
129
+ );
130
+ return {
131
+ serverId: selection.serverId,
132
+ connectionId: selection.connectionId,
133
+ reason:
134
+ !server?.connectionRef || server.connectionRef.authoritySource === "host"
135
+ ? ("connector_unavailable" as const)
136
+ : !connection
137
+ ? ("account_not_visible" as const)
138
+ : connection.status !== "active"
139
+ ? ("account_inactive" as const)
140
+ : ("account_mismatch" as const),
141
+ };
142
+ }),
143
+ },
144
+ );
145
+ }
146
+ return McpConnectionAccountBindings.parse(
147
+ bindings.sort((left, right) => left.serverId.localeCompare(right.serverId)),
148
+ );
149
+ }
150
+
151
+ /** Workspace accounts never acquire a fabricated personal owner/delegation. */
152
+ export function personalDelegationsForAccountBindings(
153
+ bindings: McpConnectionAccountBinding[],
154
+ ): McpPersonalConnectionDelegation[] {
155
+ return bindings.flatMap((binding) =>
156
+ binding.subjectScope === "subject" && binding.ownerSubjectId
157
+ ? [
158
+ {
159
+ serverId: binding.serverId,
160
+ canonicalServerId: binding.canonicalServerId,
161
+ connectionId: binding.connectionId,
162
+ originWorkspaceId: binding.originWorkspaceId,
163
+ ownerSubjectId: binding.ownerSubjectId,
164
+ providerDomain: binding.providerDomain,
165
+ kind: binding.kind,
166
+ connectionType: "mcp" as const,
167
+ },
168
+ ]
169
+ : [],
170
+ );
171
+ }
@@ -0,0 +1,121 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import type { McpConnectionAccountBinding, ToolRef } from "@opengeni/contracts";
3
+ import type { ApiIntegrationRuntime } from "@opengeni/db";
4
+
5
+ /** Expand only after canonical tool-policy admission. Aliases are execution
6
+ * identities, never an alternate way to gain admission to a connector. */
7
+ export function expandMcpAccountRoutes(input: {
8
+ settings: Settings;
9
+ tools: ToolRef[];
10
+ bindings: readonly McpConnectionAccountBinding[] | null | undefined;
11
+ }): { settings: Settings; tools: ToolRef[]; accountLabels: ReadonlyMap<string, string> } {
12
+ // Only historical work has no binding snapshot. A present empty snapshot is
13
+ // an explicit lack of account authority, never a request for current defaults.
14
+ if (input.bindings == null) {
15
+ return { settings: input.settings, tools: input.tools, accountLabels: new Map() };
16
+ }
17
+ const configured = new Map(input.settings.mcpServers.map((server) => [server.id, server]));
18
+ const byCanonical = new Map<string, McpConnectionAccountBinding[]>();
19
+ const routes = new Set<string>();
20
+ const accounts = new Set<string>();
21
+ for (const binding of input.bindings) {
22
+ const account = JSON.stringify([binding.canonicalServerId, binding.connectionId]);
23
+ if (
24
+ routes.has(binding.serverId) ||
25
+ accounts.has(account) ||
26
+ configured.has(binding.serverId) ||
27
+ (binding.subjectScope === "subject") !== (binding.ownerSubjectId !== null) ||
28
+ binding.connectionRef.authoritySource === "host" ||
29
+ binding.connectionRef.connectionId !== binding.connectionId ||
30
+ binding.connectionRef.subjectScope !== binding.subjectScope ||
31
+ binding.connectionRef.providerDomain !== binding.providerDomain ||
32
+ binding.connectionRef.kind !== binding.kind
33
+ ) {
34
+ throw new Error("Invalid accepted MCP account route");
35
+ }
36
+ routes.add(binding.serverId);
37
+ accounts.add(account);
38
+ const siblings = byCanonical.get(binding.canonicalServerId) ?? [];
39
+ siblings.push(binding);
40
+ byCanonical.set(binding.canonicalServerId, siblings);
41
+ }
42
+ const servers: Settings["mcpServers"] = [];
43
+ const tools: ToolRef[] = [];
44
+ const accountLabels = new Map<string, string>();
45
+ for (const tool of input.tools) {
46
+ const bindings = byCanonical.get(tool.id);
47
+ if (!bindings) {
48
+ if (!configured.get(tool.id)?.connectionRef) tools.push(tool);
49
+ continue;
50
+ }
51
+ const canonical = configured.get(tool.id);
52
+ if (!canonical?.connectionRef) {
53
+ throw new Error("Accepted MCP account route has no canonical connection configuration");
54
+ }
55
+ for (const binding of bindings) {
56
+ if (
57
+ canonical.connectionRef.authoritySource === "host" ||
58
+ canonical.connectionRef.providerDomain !== binding.providerDomain ||
59
+ (canonical.connectionRef.kind && canonical.connectionRef.kind !== binding.kind)
60
+ ) {
61
+ throw new Error("Accepted MCP account route does not match canonical provider");
62
+ }
63
+ // Static headers can belong to the formerly selected account. Credentials
64
+ // for account-qualified routes come exclusively from the native resolver.
65
+ const { headers: _headers, ...config } = canonical;
66
+ const label = `${canonical.name ?? canonical.id} — ${binding.subjectScope === "subject" ? "Personal" : "Workspace"}: ${binding.accountLabel}`;
67
+ servers.push({
68
+ ...config,
69
+ id: binding.serverId,
70
+ name: label,
71
+ connectionRef: structuredClone(binding.connectionRef),
72
+ });
73
+ tools.push({ ...tool, id: binding.serverId });
74
+ accountLabels.set(binding.serverId, label);
75
+ }
76
+ }
77
+ return {
78
+ settings: {
79
+ ...input.settings,
80
+ mcpServers: [
81
+ ...input.settings.mcpServers.filter((server) => !server.connectionRef),
82
+ ...servers,
83
+ ],
84
+ },
85
+ tools,
86
+ accountLabels,
87
+ };
88
+ }
89
+
90
+ /** Each adapter closes over its own immutable credential ref and authority
91
+ * generation. Sharing a provider transport never shares a credential resolver. */
92
+ export function expandApiIntegrationAccountRoutes(input: {
93
+ integrations: readonly ApiIntegrationRuntime[];
94
+ bindings: readonly McpConnectionAccountBinding[] | null | undefined;
95
+ tools: readonly ToolRef[];
96
+ }): ApiIntegrationRuntime[] {
97
+ const selected = new Set(input.tools.map((tool) => tool.id));
98
+ if (input.bindings == null)
99
+ return input.integrations.filter((item) => selected.has(item.serverId));
100
+ const acceptedBindings = input.bindings;
101
+ return input.integrations.flatMap((integration) => {
102
+ const bindings = acceptedBindings.filter(
103
+ (binding) => binding.canonicalServerId === integration.serverId,
104
+ );
105
+ if (bindings.length === 0)
106
+ return !integration.connectionRef && selected.has(integration.serverId) ? [integration] : [];
107
+ return bindings
108
+ .filter((binding) => selected.has(binding.serverId))
109
+ .map((binding) => {
110
+ if (binding.connectionAuthorityGeneration === undefined) {
111
+ throw new Error("Accepted API integration account route has no authority generation");
112
+ }
113
+ return {
114
+ ...integration,
115
+ serverId: binding.serverId,
116
+ connectionRef: structuredClone(binding.connectionRef),
117
+ connectionAuthorityGeneration: binding.connectionAuthorityGeneration,
118
+ };
119
+ });
120
+ });
121
+ }