@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,56 @@
1
+ import {
2
+ CompanyProfileAgentAttempt,
3
+ CompanyProfileAgentConfirmRequest,
4
+ CompanyProfileAgentProposalRequest,
5
+ type CompanyProfileAgentConfirmationReceipt,
6
+ type CompanyProfileAgentProposalReceipt,
7
+ } from "@opengeni/contracts";
8
+ import {
9
+ confirmCompanyProfileForAgent,
10
+ proposeCompanyProfileForAgent,
11
+ type Database,
12
+ } from "@opengeni/db";
13
+ export { CompanyProfileAgentAdminError } from "@opengeni/db";
14
+
15
+ export type CompanyProfileAgentAdminRouterOptions = {
16
+ db: Database;
17
+ propose?: typeof proposeCompanyProfileForAgent;
18
+ confirm?: typeof confirmCompanyProfileForAgent;
19
+ };
20
+
21
+ /**
22
+ * Explicit organization administration is intentionally separate from derived
23
+ * workspace learning. The database capabilities own exact-attempt admission,
24
+ * current organization-owner authority, the separate organization policy,
25
+ * canonical review when required, tenant isolation, CAS, and immutable
26
+ * receipts.
27
+ */
28
+ export function createCompanyProfileAgentAdminRouter(
29
+ options: CompanyProfileAgentAdminRouterOptions,
30
+ ): {
31
+ propose: (input: {
32
+ attempt: unknown;
33
+ request: unknown;
34
+ }) => Promise<CompanyProfileAgentProposalReceipt>;
35
+ confirm: (input: {
36
+ attempt: unknown;
37
+ request: unknown;
38
+ }) => Promise<CompanyProfileAgentConfirmationReceipt>;
39
+ } {
40
+ const propose = options.propose ?? proposeCompanyProfileForAgent;
41
+ const confirm = options.confirm ?? confirmCompanyProfileForAgent;
42
+ return {
43
+ async propose(input) {
44
+ return await propose(options.db, {
45
+ attempt: CompanyProfileAgentAttempt.parse(input.attempt),
46
+ request: CompanyProfileAgentProposalRequest.parse(input.request),
47
+ });
48
+ },
49
+ async confirm(input) {
50
+ return await confirm(options.db, {
51
+ attempt: CompanyProfileAgentAttempt.parse(input.attempt),
52
+ request: CompanyProfileAgentConfirmRequest.parse(input.request),
53
+ });
54
+ },
55
+ };
56
+ }
@@ -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,371 @@
1
+ import { slackRestMcpToolsForScopes } from "@opengeni/contracts/slack-rest-mcp";
2
+ import { createHash } from "node:crypto";
3
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
6
+ import {
7
+ boundMcpResponseBody,
8
+ MCP_MAX_RESPONSE_BYTES,
9
+ assertMcpToolListWithinBounds,
10
+ } from "@opengeni/runtime/mcp-network";
11
+ import { createPinnedIntegrationTransport } from "@opengeni/capabilities";
12
+ import type { Settings } from "@opengeni/config";
13
+ import type {
14
+ AccessGrant,
15
+ ConnectorToolPermission,
16
+ ConnectorToolPermissionEntry,
17
+ ConnectorToolPermissionsResponse,
18
+ UpdateConnectorToolPermissionsRequest,
19
+ ConnectionMetadata,
20
+ McpServerConnectionRef,
21
+ } from "@opengeni/contracts";
22
+ import {
23
+ buildConnectionTokenResolver,
24
+ getConnectionMetadata,
25
+ listConnectionsMetadata,
26
+ listEnabledMcpCapabilityServers,
27
+ listConnectorToolPermissionPolicies,
28
+ updateConnectorToolPermissionPolicies,
29
+ resolveConnectorActionPolicy,
30
+ type Database,
31
+ } from "@opengeni/db";
32
+ import { HTTPException } from "hono/http-exception";
33
+ import {
34
+ GMAIL_REST_MCP_TOOLS,
35
+ gmailToolSupportsScopes,
36
+ isOfficialGmailMcpConfig,
37
+ isOfficialSlackMcpConfig,
38
+ } from "@opengeni/runtime";
39
+ import { hasPermission } from "../access";
40
+ import { buildCapabilityCatalog, settingsWithMcpCapabilityServers } from "./capabilities";
41
+
42
+ type Input = {
43
+ db: Database;
44
+ settings: Settings;
45
+ workspaceId: string;
46
+ grant: AccessGrant;
47
+ capabilityId: string;
48
+ personalOwnerVerified: boolean;
49
+ };
50
+ type ListedTool = {
51
+ name: string;
52
+ title?: string | undefined;
53
+ description?: string | undefined;
54
+ annotations?:
55
+ | {
56
+ readOnlyHint?: boolean | undefined;
57
+ destructiveHint?: boolean | undefined;
58
+ title?: string | undefined;
59
+ }
60
+ | undefined;
61
+ };
62
+
63
+ export function connectorToolGroup(tool: ListedTool): "read" | "write" | "other" {
64
+ if (tool.annotations?.destructiveHint === true || tool.annotations?.readOnlyHint === false)
65
+ return "write";
66
+ if (tool.annotations?.readOnlyHint === true) return "read";
67
+ return "other";
68
+ }
69
+
70
+ /** A selector can supply discovery credentials only when one visible account
71
+ * matches. Never apply one account's approval choices to an arbitrary sibling. */
72
+ export function unpinnedConnectorToolPermissionAccount<T extends ConnectionMetadata>(
73
+ ref: McpServerConnectionRef,
74
+ visible: T[],
75
+ subjectId: string,
76
+ ): T | null {
77
+ const matches = visible.filter(
78
+ (candidate) =>
79
+ candidate.subjectId === (ref.subjectScope === "subject" ? subjectId : null) &&
80
+ candidate.providerDomain === ref.providerDomain &&
81
+ (!ref.kind || candidate.kind === ref.kind) &&
82
+ candidate.status === "active",
83
+ );
84
+ if (matches.length > 1)
85
+ throw new HTTPException(409, {
86
+ message: "Choose one account to manage its tool permissions",
87
+ });
88
+ return matches[0] ?? null;
89
+ }
90
+
91
+ export function connectorToolPermissionReference(
92
+ ref: McpServerConnectionRef,
93
+ connectionId: string,
94
+ ): McpServerConnectionRef {
95
+ return { ...ref, accountSelection: undefined, connectionId };
96
+ }
97
+
98
+ async function resolveTarget(input: Input) {
99
+ if (input.capabilityId.startsWith("api:")) {
100
+ // API Integrations are not MCP connectors: their per-tool approval is part
101
+ // of the reviewed installation, not this connector policy surface.
102
+ throw new HTTPException(404, {
103
+ message:
104
+ "Connected MCP connector not found; API Integration tool approval is set with installApiIntegration autoApprovedTools",
105
+ });
106
+ }
107
+ const catalog = await buildCapabilityCatalog(input);
108
+ const item = catalog.items.find((candidate) => candidate.id === input.capabilityId);
109
+ if (
110
+ !item ||
111
+ !item.enabled ||
112
+ item.kind !== "mcp" ||
113
+ !item.runtime.mcpServerId ||
114
+ item.source === "built_in"
115
+ ) {
116
+ throw new HTTPException(404, { message: "Connected MCP connector not found" });
117
+ }
118
+ const enabled = await listEnabledMcpCapabilityServers(input.db, input.workspaceId);
119
+ const settings = settingsWithMcpCapabilityServers(input.settings, enabled);
120
+ const server = settings.mcpServers.find((candidate) => candidate.id === item.runtime.mcpServerId);
121
+ if (!server || server.url !== item.endpointUrl)
122
+ throw new HTTPException(409, { message: "Connector configuration is unavailable" });
123
+ const ref = server.connectionRef;
124
+ if (ref?.authoritySource === "host")
125
+ throw new HTTPException(409, {
126
+ message: "Tool permissions for this connection are managed by its host",
127
+ });
128
+ let connection = ref?.connectionId
129
+ ? await getConnectionMetadata(
130
+ input.db,
131
+ input.workspaceId,
132
+ ref.connectionId,
133
+ input.grant.subjectId,
134
+ )
135
+ : null;
136
+ if (
137
+ ref &&
138
+ !ref.connectionId &&
139
+ (ref.subjectScope === "subject" || ref.accountSelection === "all_eligible")
140
+ ) {
141
+ const visible = await listConnectionsMetadata(
142
+ input.db,
143
+ input.workspaceId,
144
+ input.grant.subjectId,
145
+ );
146
+ connection = unpinnedConnectorToolPermissionAccount(ref, visible, input.grant.subjectId);
147
+ }
148
+ if (
149
+ ref &&
150
+ (!connection ||
151
+ connection.providerDomain !== ref.providerDomain ||
152
+ (ref.kind && connection.kind !== ref.kind) ||
153
+ (ref.subjectScope === "subject"
154
+ ? connection.subjectId !== input.grant.subjectId
155
+ : connection.subjectId !== null))
156
+ ) {
157
+ throw new HTTPException(409, { message: "Reconnect this connector to manage its tools" });
158
+ }
159
+ if (connection?.subjectId && !input.personalOwnerVerified)
160
+ throw new HTTPException(403, {
161
+ message: "Only the authenticated connection owner may manage personal tool permissions",
162
+ });
163
+ // Same secret-free target identity used by runtime sessionMcpApprovalConnectionId.
164
+ const connectionId =
165
+ connection?.id ??
166
+ `session-mcp:${server.id}:${createHash("sha256").update(server.url, "utf8").digest("hex")}`;
167
+ return { server, connection, connectionId };
168
+ }
169
+
170
+ /** Use the same adapter-owned identities as execution for reviewed local catalogs. */
171
+ export function reviewedConnectorToolCatalog(
172
+ server: Pick<Settings["mcpServers"][number], "url" | "connectionRef" | "allowedTools">,
173
+ grantedScopes: readonly string[],
174
+ ): ListedTool[] | null {
175
+ const tools = isOfficialGmailMcpConfig(server.url, server.connectionRef)
176
+ ? GMAIL_REST_MCP_TOOLS.filter((tool) => gmailToolSupportsScopes(tool.name, grantedScopes))
177
+ : isOfficialSlackMcpConfig(server.url, server.connectionRef)
178
+ ? slackRestMcpToolsForScopes(grantedScopes)
179
+ : null;
180
+ return (
181
+ tools?.filter((tool) => !server.allowedTools || server.allowedTools.includes(tool.name)) ?? null
182
+ );
183
+ }
184
+
185
+ async function listTools(
186
+ input: Input,
187
+ target: Awaited<ReturnType<typeof resolveTarget>>,
188
+ ): Promise<ListedTool[]> {
189
+ const reviewed = reviewedConnectorToolCatalog(
190
+ target.server,
191
+ target.connection?.grantedScopes ?? [],
192
+ );
193
+ if (reviewed !== null) return reviewed;
194
+ let headers = { ...target.server.headers };
195
+ if (target.server.connectionRef) {
196
+ const result = await buildConnectionTokenResolver(
197
+ input.db,
198
+ input.settings,
199
+ )({
200
+ workspaceId: input.workspaceId,
201
+ ...(target.connection?.subjectId ? { subjectId: input.grant.subjectId } : {}),
202
+ serverId: target.server.id,
203
+ toolName: "tools/list",
204
+ connectionRef: connectorToolPermissionReference(
205
+ target.server.connectionRef,
206
+ target.connectionId,
207
+ ),
208
+ destinationUrl: target.server.url,
209
+ });
210
+ if (result.status !== "ok") throw new Error("Reconnect this connector to load its tools.");
211
+ headers = { ...headers, ...result.headers };
212
+ }
213
+ const pinned = createPinnedIntegrationTransport({ network: input.settings });
214
+ const deadline = AbortSignal.timeout(15_000);
215
+ const client = new Client(
216
+ { name: "opengeni-tool-permissions", version: "0.1.0" },
217
+ { capabilities: {} },
218
+ );
219
+ try {
220
+ const transport = new StreamableHTTPClientTransport(new URL(target.server.url), {
221
+ requestInit: { headers },
222
+ fetch: async (url, init) =>
223
+ boundMcpResponseBody(
224
+ await pinned.fetch(url.toString(), {
225
+ ...init,
226
+ signal: init?.signal ? AbortSignal.any([deadline, init.signal]) : deadline,
227
+ }),
228
+ MCP_MAX_RESPONSE_BYTES,
229
+ ),
230
+ });
231
+ await client.connect(transport as unknown as Transport, {
232
+ timeout: 15_000,
233
+ maxTotalTimeout: 15_000,
234
+ });
235
+ const tools = await collectConnectorToolPages(client, deadline);
236
+ return tools.filter(
237
+ (tool) => !target.server.allowedTools || target.server.allowedTools.includes(tool.name),
238
+ );
239
+ } finally {
240
+ await client.close().catch(() => undefined);
241
+ }
242
+ }
243
+
244
+ /** Collect discovery pages without changing the caller's connection or authority. */
245
+ export async function collectConnectorToolPages(
246
+ client: Pick<Client, "listTools">,
247
+ deadline: AbortSignal,
248
+ ): Promise<ListedTool[]> {
249
+ const tools: ListedTool[] = [];
250
+ const cursors = new Set<string>();
251
+ let cursor: string | undefined;
252
+ do {
253
+ const page = await client.listTools(cursor ? { cursor } : undefined, {
254
+ timeout: 15_000,
255
+ maxTotalTimeout: 15_000,
256
+ signal: deadline,
257
+ });
258
+ tools.push(...page.tools);
259
+ assertMcpToolListWithinBounds(tools);
260
+ if (new Set(tools.map((tool) => tool.name)).size !== tools.length)
261
+ throw new Error("The connector returned duplicate tool names.");
262
+ cursor = page.nextCursor;
263
+ if (cursor && cursors.has(cursor))
264
+ throw new Error("The connector returned an invalid or oversized tool catalog.");
265
+ if (cursor) cursors.add(cursor);
266
+ } while (cursor);
267
+ return tools;
268
+ }
269
+
270
+ export async function getConnectorToolPermissions(
271
+ input: Input,
272
+ ): Promise<ConnectorToolPermissionsResponse> {
273
+ const target = await resolveTarget(input);
274
+ const policies = await listConnectorToolPermissionPolicies(input.db, {
275
+ accountId: input.grant.accountId,
276
+ workspaceId: input.workspaceId,
277
+ connectionId: target.connectionId,
278
+ });
279
+ const permission = (
280
+ name: string,
281
+ ): { permission: ConnectorToolPermission; inherited: boolean } => {
282
+ const resolved = resolveConnectorActionPolicy(policies, {
283
+ connectionId: target.connectionId,
284
+ serverId: target.server.id,
285
+ toolName: name,
286
+ actionName: "*",
287
+ });
288
+ if (!resolved.managed) return { permission: "allow", inherited: true };
289
+ return {
290
+ permission: resolved.entry?.policy ?? "block",
291
+ inherited: resolved.entry?.toolName !== name,
292
+ };
293
+ };
294
+ const defaultPolicy = policies.find(
295
+ (row) => row.serverId === target.server.id && row.toolName === "*" && row.actionName === "*",
296
+ );
297
+ let tools: ListedTool[] = [];
298
+ let discoveryError: string | null = null;
299
+ try {
300
+ tools = await listTools(input, target);
301
+ if (tools.some((tool) => tool.name === "*" || tool.name !== tool.name.trim())) {
302
+ tools = tools.filter((tool) => tool.name !== "*" && tool.name === tool.name.trim());
303
+ discoveryError =
304
+ "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.";
305
+ }
306
+ } catch {
307
+ discoveryError =
308
+ "Could not load the connector's tools. Try again or reconnect. Your saved permissions still apply.";
309
+ }
310
+ return {
311
+ connectionId: target.connectionId,
312
+ serverId: target.server.id,
313
+ defaultPermission: defaultPolicy?.policy ?? null,
314
+ tools: tools.map(
315
+ (tool): ConnectorToolPermissionEntry => ({
316
+ name: tool.name,
317
+ ...((tool.title ?? tool.annotations?.title)
318
+ ? { title: tool.title ?? tool.annotations?.title }
319
+ : {}),
320
+ ...(tool.description ? { description: tool.description } : {}),
321
+ group: connectorToolGroup(tool),
322
+ ...permission(tool.name),
323
+ approvalRequired:
324
+ target.server.requireApproval === true ||
325
+ (Array.isArray(target.server.requireApproval) &&
326
+ target.server.requireApproval.includes(tool.name)),
327
+ }),
328
+ ),
329
+ discoveryError,
330
+ canManage: canManageConnectorPermissions(input.grant),
331
+ };
332
+ }
333
+
334
+ function canManageConnectorPermissions(grant: AccessGrant): boolean {
335
+ return (
336
+ hasPermission(grant.permissions, "capabilities:manage") &&
337
+ grant.principalKind !== "agent_attempt" &&
338
+ grant.principalKind !== "service" &&
339
+ !grant.serviceInitiator &&
340
+ !grant.serviceInitiatorContext
341
+ );
342
+ }
343
+
344
+ export async function updateConnectorToolPermissions(
345
+ input: Input & { payload: UpdateConnectorToolPermissionsRequest },
346
+ ): Promise<void> {
347
+ if (!canManageConnectorPermissions(input.grant))
348
+ throw new HTTPException(403, { message: "Connector management permission required" });
349
+ if (
350
+ input.payload.target === "tools" &&
351
+ input.payload.toolNames.some((name) => name === "*" || name !== name.trim())
352
+ )
353
+ throw new HTTPException(400, {
354
+ message: "Tool names must be exact and cannot use the connector-default wildcard.",
355
+ });
356
+ const target = await resolveTarget(input);
357
+ // Prevent a reconnect while the sheet is open from applying old choices to a new account.
358
+ if (target.connectionId !== input.payload.connectionId)
359
+ throw new HTTPException(409, {
360
+ message: "The connector account changed. Reload its permissions.",
361
+ });
362
+ await updateConnectorToolPermissionPolicies(input.db, {
363
+ accountId: input.grant.accountId,
364
+ workspaceId: input.workspaceId,
365
+ subjectId: input.grant.subjectId,
366
+ connectionId: target.connectionId,
367
+ serverId: target.server.id,
368
+ toolNames: input.payload.target === "default" ? ["*"] : input.payload.toolNames,
369
+ policy: input.payload.permission,
370
+ });
371
+ }
@@ -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
+ }
@@ -1,14 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
- import {
4
- COMMITTED_TRANSACTION_PROTOCOL_VERSION,
5
- EDITABLE_ARTIFACT_MODEL_SCHEMA_VERSION,
6
- } from "@opengeni/contracts/editable-artifact-versions";
7
3
  import {
8
4
  DOCUMENT_ARTIFACT_COMMAND_VERSION,
5
+ EDITABLE_ARTIFACT_INTENT_PROTOCOL_VERSION,
9
6
  EDITABLE_ARTIFACT_INTENT_VERSION,
10
7
  PRESENTATION_ARTIFACT_COMMAND_VERSION,
11
8
  SPREADSHEET_ARTIFACT_COMMAND_VERSION,
9
+ currentEditableArtifactCompatibility,
12
10
  decodeEditableArtifactMutationIntent,
13
11
  decodeDocumentArtifactQueryResponse,
14
12
  decodePresentationArtifactQueryResponse,
@@ -28,10 +26,10 @@ import {
28
26
  type SpreadsheetArtifactKernelQuery,
29
27
  } from "@opengeni/contracts/editable-artifacts";
30
28
 
31
- import type {
32
- EditableArtifactDurableExportService,
33
- EditableArtifactMaterializationFormat,
34
- EditableArtifactMaterializationJob,
29
+ import {
30
+ type EditableArtifactDurableExportService,
31
+ type EditableArtifactMaterializationFormat,
32
+ type EditableArtifactMaterializationJob,
35
33
  } from "./durable-export";
36
34
  import {
37
35
  EditableArtifactDomainError,
@@ -78,6 +76,7 @@ export type EditableArtifactAgentContext = Readonly<{
78
76
 
79
77
  export type EditableArtifactAgentMetadata = Readonly<{
80
78
  id: string;
79
+ artifactReference: string;
81
80
  modality: EditableArtifactModality;
82
81
  title: string;
83
82
  lifecycle: EditableArtifact["lifecycle"];
@@ -176,6 +175,14 @@ export type EditableArtifactAgentApplicationDependencies = Readonly<{
176
175
  exports: EditableArtifactDurableExportService;
177
176
  associations: EditableArtifactAgentAssociationPort;
178
177
  inspector: EditableArtifactAgentInspectionKernelPort;
178
+ /** Production persists proof only after a successful native query. */
179
+ recordInspection?: (
180
+ input: EditableArtifactAgentContext & {
181
+ artifact: EditableArtifactAgentMetadata;
182
+ queryHash: string;
183
+ queryKind: string;
184
+ },
185
+ ) => Promise<string>;
179
186
  officeImports: EditableArtifactOfficeImportPort;
180
187
  workspaceFiles: EditableArtifactAgentWorkspaceFilePort;
181
188
  }>;
@@ -185,6 +192,9 @@ export type EditableArtifactAgentApplicationDependencies = Readonly<{
185
192
  * not another artifact engine: every mutation terminates at EditableArtifactService.
186
193
  */
187
194
  export class EditableArtifactAgentApplication {
195
+ describeExportFormats(): string {
196
+ return this.dependencies.exports.describeFormats();
197
+ }
188
198
  constructor(private readonly dependencies: EditableArtifactAgentApplicationDependencies) {}
189
199
 
190
200
  async list(
@@ -293,7 +303,13 @@ export class EditableArtifactAgentApplication {
293
303
  async inspect(
294
304
  input: EditableArtifactAgentContext &
295
305
  Readonly<{ artifactId: EditableArtifactId; request: EditableArtifactAgentQuery }>,
296
- ): Promise<Readonly<{ artifact: EditableArtifactAgentMetadata; projection: unknown }>> {
306
+ ): Promise<
307
+ Readonly<{
308
+ artifact: EditableArtifactAgentMetadata;
309
+ projection: unknown;
310
+ inspectionReceiptId?: string;
311
+ }>
312
+ > {
297
313
  const context = agentContext(input);
298
314
  const artifactId = editableArtifactId(input.artifactId);
299
315
  const state = await this.dependencies.domain.readCurrentKernelState({
@@ -306,8 +322,21 @@ export class EditableArtifactAgentApplication {
306
322
  const queryBytes = encodeQuery(input.request);
307
323
  const responseBytes = await this.dependencies.inspector.query({ state, queryBytes });
308
324
  const projection = decodeQueryResponse(input.request.modality, responseBytes);
325
+ const inspectionReceiptId =
326
+ state.modality === "document"
327
+ ? await this.dependencies.recordInspection?.({
328
+ ...context,
329
+ artifact: projectArtifact(state.artifact),
330
+ queryHash: createHash("sha256").update(queryBytes).digest("hex"),
331
+ queryKind: input.request.query.kind,
332
+ })
333
+ : undefined;
309
334
  await this.touch(context, artifactId);
310
- return Object.freeze({ artifact: projectArtifact(state.artifact), projection });
335
+ return Object.freeze({
336
+ artifact: projectArtifact(state.artifact),
337
+ projection,
338
+ ...(inspectionReceiptId ? { inspectionReceiptId } : {}),
339
+ });
311
340
  }
312
341
 
313
342
  async apply(
@@ -364,8 +393,9 @@ export class EditableArtifactAgentApplication {
364
393
  }
365
394
  const authored = hashEditableArtifactMutationIntent({
366
395
  envelopeVersion: EDITABLE_ARTIFACT_INTENT_VERSION,
367
- protocolVersion: COMMITTED_TRANSACTION_PROTOCOL_VERSION,
368
- modelSchemaVersion: EDITABLE_ARTIFACT_MODEL_SCHEMA_VERSION,
396
+ protocolVersion: EDITABLE_ARTIFACT_INTENT_PROTOCOL_VERSION,
397
+ modelSchemaVersion: currentEditableArtifactCompatibility(input.batch.modality)
398
+ .modelSchemaVersion,
369
399
  commandProtocolVersion: commandProtocolVersion(input.batch.modality),
370
400
  artifactId,
371
401
  clientTransactionId,
@@ -446,6 +476,12 @@ export class EditableArtifactAgentApplication {
446
476
  actor: context.actor,
447
477
  artifactId,
448
478
  });
479
+ // Refuse before pinning a version: the agent learns what does exist.
480
+ await this.dependencies.exports.preflight({
481
+ modality: artifact.modality,
482
+ format: input.format,
483
+ ...(input.options ? { options: input.options } : {}),
484
+ });
449
485
  const version = await this.dependencies.exports.pinVersion({
450
486
  scope: context.scope,
451
487
  actor: context.actor,
@@ -564,12 +600,21 @@ function agentContext(input: EditableArtifactAgentContext): EditableArtifactAgen
564
600
 
565
601
  function encodeCommands(batch: EditableArtifactAgentCommandBatch): Uint8Array {
566
602
  if (batch.modality === "spreadsheet") {
567
- return encodeSpreadsheetArtifactCommandBatch({ version: 1, commands: batch.commands });
603
+ return encodeSpreadsheetArtifactCommandBatch({
604
+ version: SPREADSHEET_ARTIFACT_COMMAND_VERSION,
605
+ commands: batch.commands,
606
+ });
568
607
  }
569
608
  if (batch.modality === "document") {
570
- return encodeDocumentArtifactCommandBatch({ version: 1, commands: batch.commands });
609
+ return encodeDocumentArtifactCommandBatch({
610
+ version: DOCUMENT_ARTIFACT_COMMAND_VERSION,
611
+ commands: batch.commands,
612
+ });
571
613
  }
572
- return encodePresentationArtifactCommandBatch({ version: 1, commands: batch.commands });
614
+ return encodePresentationArtifactCommandBatch({
615
+ version: PRESENTATION_ARTIFACT_COMMAND_VERSION,
616
+ commands: batch.commands,
617
+ });
573
618
  }
574
619
 
575
620
  function encodeQuery(request: EditableArtifactAgentQuery): Uint8Array {
@@ -595,6 +640,7 @@ function commandProtocolVersion(modality: EditableArtifactModality): number {
595
640
  function projectArtifact(artifact: EditableArtifact): EditableArtifactAgentMetadata {
596
641
  return Object.freeze({
597
642
  id: artifact.id,
643
+ artifactReference: `[Open report](/workspaces/${artifact.scope.workspaceId}/artifacts/editable/${artifact.id})`,
598
644
  modality: artifact.modality,
599
645
  title: artifact.title,
600
646
  lifecycle: artifact.lifecycle,