@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
@@ -3,10 +3,14 @@ import {
3
3
  OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
4
4
  OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
5
5
  OpenGeniSlackBotConnectionMetadata,
6
+ SCHEDULED_SLACK_BOT_POSTING_TOOLS,
6
7
  areOpenGeniSlackBotScopesAccepted,
7
8
  type AccessGrant,
8
9
  type ConnectionMetadata,
10
+ type FirstPartyMcpToolName,
9
11
  type OpenGeniSlackBotConnectionMetadata as OpenGeniSlackBotMetadata,
12
+ type ScheduledTask,
13
+ type ScheduledTaskAgentConfig,
10
14
  type Session,
11
15
  } from "@opengeni/contracts";
12
16
  import {
@@ -15,7 +19,8 @@ import {
15
19
  type Database,
16
20
  } from "@opengeni/db";
17
21
  import { HTTPException } from "hono/http-exception";
18
- import { requirePermission } from "../access";
22
+ import { requirePermission, type AccessGrantAuthorization } from "../access";
23
+ import { grantHasAgentAttemptAuthority } from "../session-authorization";
19
24
 
20
25
  export function openGeniSlackBotMetadata(
21
26
  metadata: Record<string, unknown>,
@@ -72,12 +77,12 @@ export async function requireOpenGeniSlackBotConnection(
72
77
  const connection = await getConnectionMetadata(db, workspaceId, connectionId, null);
73
78
  if (!connection || !isOpenGeniSlackBotConnection(connection)) {
74
79
  throw new HTTPException(422, {
75
- message: "slackBotConnectionId must reference an OpenGeni Slack bot connection",
80
+ message: "slackBotConnectionId must reference an Opengeni Slack bot connection",
76
81
  });
77
82
  }
78
83
  if (connection.status !== "active") {
79
84
  throw new HTTPException(422, {
80
- message: `OpenGeni Slack bot connection is not active (${connection.status})`,
85
+ message: `Opengeni Slack bot connection is not active (${connection.status})`,
81
86
  });
82
87
  }
83
88
  return connection;
@@ -126,3 +131,123 @@ export function isTrustedScheduledSlackBotSession(
126
131
  function zUuid(value: string): boolean {
127
132
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
128
133
  }
134
+
135
+ /**
136
+ * Proves, with the bot's own token, that it may post in the chosen channel:
137
+ * the bot is a member and the channel is active and not shared with another
138
+ * organization. Supplied only by the HTTP routes a person uses to edit tasks.
139
+ */
140
+ export type ScheduledTaskSlackChannelVerifier = (input: {
141
+ connectionId: string;
142
+ channelId: string;
143
+ }) => Promise<void>;
144
+
145
+ /**
146
+ * Whether this request is a signed-in person acting for themselves: never an
147
+ * agent attempt, a service, an API key, or a request carrying service
148
+ * provenance. The same rule gates Agent learning settings.
149
+ */
150
+ export function isAuthenticatedPersonAuthorization(
151
+ authorization: AccessGrantAuthorization | undefined,
152
+ ): boolean {
153
+ if (!authorization) return false;
154
+ const { grant } = authorization;
155
+ return (
156
+ grant.principalKind === "human_session" &&
157
+ authorization.contextIntegrity &&
158
+ authorization.authenticatedSubjectId === grant.subjectId &&
159
+ !grant.serviceInitiator &&
160
+ !grant.serviceInitiatorContext &&
161
+ grant.metadata?.attemptId === undefined &&
162
+ grant.metadata?.turnId === undefined &&
163
+ grant.metadata?.executionGeneration === undefined &&
164
+ !grantHasAgentAttemptAuthority(grant)
165
+ );
166
+ }
167
+
168
+ /**
169
+ * A scheduled task may post as the OpenGeni workspace bot only to the one
170
+ * Slack channel a person chose on the task. Choosing or changing that channel
171
+ * therefore needs a signed-in person with `connections:write`, and the bot's
172
+ * membership is verified at that moment. Keeping or clearing the existing
173
+ * channel needs neither, so an agent editing other task fields cannot redirect
174
+ * posts but also does not have to re-prove the person's choice. Clearing is the
175
+ * safe direction and is never refused, even for a task that continues one chat.
176
+ */
177
+ export async function validateScheduledTaskSlackChannel(input: {
178
+ grant: AccessGrant;
179
+ authorization?: AccessGrantAuthorization | undefined;
180
+ previous: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId"> | null;
181
+ next: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId">;
182
+ runMode: ScheduledTask["runMode"];
183
+ /**
184
+ * Null unless the task already created the reusable chat its runs continue.
185
+ * Otherwise reports whether that chat was created with the posting tools,
186
+ * which are fixed at creation.
187
+ */
188
+ reusableSessionCanPost: (() => Promise<boolean>) | null;
189
+ verifySlackChannel?: ScheduledTaskSlackChannelVerifier | undefined;
190
+ }): Promise<void> {
191
+ const previousChannel = input.previous?.slackBotChannelId ?? null;
192
+ const nextChannel = input.next.slackBotChannelId ?? null;
193
+ // Clearing takes effect at once: the tools re-read the task and refuse.
194
+ if (!nextChannel) return;
195
+ if (!input.next.slackBotConnectionId) {
196
+ throw new HTTPException(422, {
197
+ message: "slackBotChannelId requires slackBotConnectionId",
198
+ });
199
+ }
200
+ if (input.runMode === "existing_session") {
201
+ throw new HTTPException(422, {
202
+ message: "Slack channel posting needs a task that starts its own chats",
203
+ });
204
+ }
205
+ if (
206
+ previousChannel === nextChannel &&
207
+ (input.previous?.slackBotConnectionId ?? null) === input.next.slackBotConnectionId
208
+ ) {
209
+ return;
210
+ }
211
+ if (!isAuthenticatedPersonAuthorization(input.authorization)) {
212
+ throw new HTTPException(403, {
213
+ message: "Only a person can choose the Slack channel a scheduled task posts to",
214
+ });
215
+ }
216
+ requirePermission(input.grant, "connections:write");
217
+ // A chat created without the posting tools keeps that tool set, so choosing
218
+ // a channel for it would silently post nothing.
219
+ if (input.reusableSessionCanPost && !(await input.reusableSessionCanPost())) {
220
+ throw new HTTPException(409, {
221
+ message:
222
+ "this task continues a chat that was created without Slack posting; recreate the task to post from it",
223
+ });
224
+ }
225
+ if (!input.verifySlackChannel) {
226
+ throw new HTTPException(422, {
227
+ message: "Choose the Slack channel in the schedule editor",
228
+ });
229
+ }
230
+ await input.verifySlackChannel({
231
+ connectionId: input.next.slackBotConnectionId,
232
+ channelId: nextChannel,
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Add the bot posting tools to a generated scheduled run when, and only when,
238
+ * a person chose the task's Slack channel. The deployment ceiling still
239
+ * applies: an operator who disallowed these tools keeps them off.
240
+ */
241
+ export function withScheduledSlackBotPostingTools(
242
+ tools: readonly FirstPartyMcpToolName[],
243
+ agentConfig: Pick<ScheduledTaskAgentConfig, "slackBotConnectionId" | "slackBotChannelId">,
244
+ allowedTools: readonly FirstPartyMcpToolName[],
245
+ ): FirstPartyMcpToolName[] {
246
+ if (!agentConfig.slackBotConnectionId || !agentConfig.slackBotChannelId) return [...tools];
247
+ const allowed = new Set(allowedTools);
248
+ const next = [...tools];
249
+ for (const tool of SCHEDULED_SLACK_BOT_POSTING_TOOLS) {
250
+ if (allowed.has(tool) && !next.includes(tool)) next.push(tool);
251
+ }
252
+ return next;
253
+ }
@@ -44,7 +44,9 @@ export function assertWorkspaceMemberRemovable(input: {
44
44
  }): void {
45
45
  const { members, subjectId, callerSubjectId } = input;
46
46
  if (subjectId === callerSubjectId) {
47
- throw new HTTPException(409, { message: "you cannot remove your own membership" });
47
+ throw new HTTPException(409, {
48
+ message: "you cannot remove your own membership",
49
+ });
48
50
  }
49
51
  const target = members.find((member) => member.subjectId === subjectId);
50
52
  if (!target) {
@@ -62,6 +64,34 @@ export function assertWorkspaceMemberRemovable(input: {
62
64
  }
63
65
  }
64
66
 
67
+ /** Keep scoped member edits from orphaning the workspace or changing the caller's own grant. */
68
+ export function assertWorkspaceMemberUpdateAllowed(input: {
69
+ members: WorkspaceMember[];
70
+ subjectId: string;
71
+ callerSubjectId: string;
72
+ nextPermissions: Permission[];
73
+ }): void {
74
+ const { members, subjectId, callerSubjectId, nextPermissions } = input;
75
+ if (subjectId === callerSubjectId) {
76
+ throw new HTTPException(409, {
77
+ message: "you cannot change your own workspace access",
78
+ });
79
+ }
80
+ const target = members.find((member) => member.subjectId === subjectId);
81
+ if (!target) {
82
+ throw new HTTPException(404, { message: "member not found" });
83
+ }
84
+ if (
85
+ memberCanAdminister(target) &&
86
+ !memberCanAdminister({ permissions: nextPermissions }) &&
87
+ !members.some((member) => member.subjectId !== subjectId && memberCanAdminister(member))
88
+ ) {
89
+ throw new HTTPException(409, {
90
+ message: "the workspace must keep at least one administrator",
91
+ });
92
+ }
93
+ }
94
+
65
95
  /**
66
96
  * Guard the workspace-delete path before any external/DB mutation. Refuses
67
97
  * (409) to delete the account's last workspace, and refuses while any session
@@ -74,7 +104,9 @@ export function assertWorkspaceDeletable(input: {
74
104
  activeSessionCount: number;
75
105
  }): void {
76
106
  if (input.workspaceCountForAccount <= 1) {
77
- throw new HTTPException(409, { message: "cannot delete the account's only workspace" });
107
+ throw new HTTPException(409, {
108
+ message: "cannot delete the account's only workspace",
109
+ });
78
110
  }
79
111
  if (input.activeSessionCount > 0) {
80
112
  throw new HTTPException(409, {
@@ -19,13 +19,20 @@ import type {
19
19
  EditableArtifactLiveServer,
20
20
  OpenEditableArtifactLiveInput,
21
21
  } from "./server";
22
- import type { EditableArtifactLiveTicket } from "./types";
22
+ import type {
23
+ EditableArtifactLiveSourceSessionAuthority,
24
+ EditableArtifactLiveTicket,
25
+ } from "./types";
23
26
 
24
27
  export type EditableArtifactLiveCompatibilityRequest = Readonly<{
25
28
  artifact: EditableArtifact;
26
- protocolVersion: number;
29
+ modality: EditableArtifactModality;
30
+ liveProtocolVersion: number;
27
31
  kernelVersion: string;
28
32
  modelSchemaVersion: number;
33
+ snapshotVersion: number;
34
+ commandProtocolVersion: number;
35
+ committedTransactionProtocolVersion: number;
29
36
  }>;
30
37
 
31
38
  /** A client/runtime protocol tuple cannot be served by the loaded kernel. */
@@ -71,10 +78,15 @@ export type ReadEditableArtifactApplicationInput = Readonly<{
71
78
 
72
79
  export type MintEditableArtifactApplicationTicketInput = ReadEditableArtifactApplicationInput &
73
80
  Readonly<{
74
- protocolVersion: number;
81
+ modality: EditableArtifactModality;
82
+ liveProtocolVersion: number;
75
83
  kernelVersion: string;
76
84
  modelSchemaVersion: number;
85
+ snapshotVersion: number;
86
+ commandProtocolVersion: number;
87
+ committedTransactionProtocolVersion: number;
77
88
  allowEdit: boolean;
89
+ sourceSessionAuthority?: EditableArtifactLiveSourceSessionAuthority;
78
90
  }>;
79
91
 
80
92
  /** Exact application seam used by standalone API and embedding hosts. */
@@ -148,9 +160,19 @@ export class EditableArtifactApplication implements EditableArtifactApplicationP
148
160
  const artifact = await this.readArtifact(input);
149
161
  await this.dependencies.compatibility.assertCompatible({
150
162
  artifact,
151
- protocolVersion: positiveInteger(input.protocolVersion, "protocolVersion"),
163
+ modality: input.modality,
164
+ liveProtocolVersion: positiveInteger(input.liveProtocolVersion, "liveProtocolVersion"),
152
165
  kernelVersion: boundedVersion(input.kernelVersion, "kernelVersion"),
153
166
  modelSchemaVersion: positiveInteger(input.modelSchemaVersion, "modelSchemaVersion"),
167
+ snapshotVersion: positiveInteger(input.snapshotVersion, "snapshotVersion"),
168
+ commandProtocolVersion: positiveInteger(
169
+ input.commandProtocolVersion,
170
+ "commandProtocolVersion",
171
+ ),
172
+ committedTransactionProtocolVersion: positiveInteger(
173
+ input.committedTransactionProtocolVersion,
174
+ "committedTransactionProtocolVersion",
175
+ ),
154
176
  });
155
177
  return await this.dependencies.live.mintTicket({
156
178
  scope: artifact.scope,
@@ -158,6 +180,9 @@ export class EditableArtifactApplication implements EditableArtifactApplicationP
158
180
  modality: artifact.modality,
159
181
  actor: input.actor,
160
182
  allowEdit: input.allowEdit,
183
+ ...(input.sourceSessionAuthority
184
+ ? { sourceSessionAuthority: input.sourceSessionAuthority }
185
+ : {}),
161
186
  });
162
187
  }
163
188
 
@@ -40,7 +40,6 @@ export type EditableArtifactLiveBootstrapRead = Readonly<{
40
40
  scope: EditableArtifactScope;
41
41
  artifactId: EditableArtifactId;
42
42
  resume: EditableArtifactLiveResume;
43
- protocolVersion: number;
44
43
  }>;
45
44
 
46
45
  export type EditableArtifactLiveTransactionPage = Readonly<{
@@ -1,5 +1,6 @@
1
1
  import type { EditableArtifactAuthorizationPort } from "../domain/editable-artifacts/ports";
2
2
  import type { EditableArtifactService } from "../domain/editable-artifacts/service";
3
+ import type { Database } from "@opengeni/db";
3
4
  import {
4
5
  EDITABLE_ARTIFACT_COMMAND_MAX_BYTES,
5
6
  EDITABLE_ARTIFACT_INTENT_MAX_BYTES,
@@ -54,9 +55,13 @@ import {
54
55
  type EditableArtifactLiveSnapshot,
55
56
  type EditableArtifactLiveTicket,
56
57
  type EditableArtifactLiveTicketRecord,
58
+ type EditableArtifactLiveSourceSessionAuthority,
57
59
  } from "./types";
58
60
 
59
61
  const textEncoder = new TextEncoder();
62
+ // Source-bound sockets must renew through the product proxy's live host check.
63
+ // Unbound native console sockets retain their existing artifact-only lifecycle.
64
+ const SOURCE_SESSION_LIVE_LEASE_MS = 15_000;
60
65
  const strictDecoder = new TextDecoder("utf-8", { fatal: true });
61
66
 
62
67
  // Shared OGATX is bounded at 5 MiB and whole OGACO at 8 MiB. Transport
@@ -81,7 +86,6 @@ const MAX_IDENTIFIER_BYTES = 512;
81
86
  const MAX_CAUSAL_ACTORS = 1_024;
82
87
 
83
88
  export type EditableArtifactLiveServerOptions = Readonly<{
84
- protocolVersion?: number;
85
89
  ticketTtlMs?: number;
86
90
  maxClientFrameBytes?: number;
87
91
  maxOutboundFrameBytes?: number;
@@ -101,7 +105,10 @@ export type EditableArtifactLiveServerOptions = Readonly<{
101
105
  }>;
102
106
 
103
107
  type NormalizedOptions = Required<Omit<EditableArtifactLiveServerOptions, "ticketTtlMs">> &
104
- Readonly<{ ticketTtlMs?: number }>;
108
+ Readonly<{
109
+ protocolVersion: typeof EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION;
110
+ ticketTtlMs?: number;
111
+ }>;
105
112
 
106
113
  export type OpenEditableArtifactLiveInput = Readonly<{
107
114
  token: string;
@@ -110,6 +117,13 @@ export type OpenEditableArtifactLiveInput = Readonly<{
110
117
  resume: EditableArtifactLiveResume;
111
118
  sink: EditableArtifactLiveSinkPort;
112
119
  signal?: AbortSignal;
120
+ /** API-owned canonical source session authorization, including live revocation. */
121
+ authorizeSourceSession?: (
122
+ ticket: EditableArtifactLiveTicketRecord,
123
+ permission: "read" | "edit",
124
+ transaction?: Database,
125
+ signal?: AbortSignal,
126
+ ) => Promise<boolean>;
113
127
  }>;
114
128
 
115
129
  export interface EditableArtifactLiveSession {
@@ -147,7 +161,6 @@ export class EditableArtifactLiveServer {
147
161
  tickets: dependencies.tickets,
148
162
  tokens: dependencies.tokens,
149
163
  clock: dependencies.clock,
150
- protocolVersion: this.options.protocolVersion,
151
164
  ...(this.options.ticketTtlMs === undefined ? {} : { ttlMs: this.options.ticketTtlMs }),
152
165
  });
153
166
  }
@@ -159,6 +172,7 @@ export class EditableArtifactLiveServer {
159
172
  modality: EditableArtifactModality;
160
173
  actor: EditableArtifactActor;
161
174
  allowEdit: boolean;
175
+ sourceSessionAuthority?: EditableArtifactLiveSourceSessionAuthority;
162
176
  }>,
163
177
  ): Promise<EditableArtifactLiveTicket> {
164
178
  return this.ticketAuthority.mint(input);
@@ -182,6 +196,9 @@ export class EditableArtifactLiveServer {
182
196
  resume: input.resume,
183
197
  sink: input.sink,
184
198
  ...(input.signal === undefined ? {} : { parentSignal: input.signal }),
199
+ ...(input.authorizeSourceSession
200
+ ? { authorizeSourceSession: input.authorizeSourceSession }
201
+ : {}),
185
202
  });
186
203
  await session.start();
187
204
  return session;
@@ -196,6 +213,7 @@ type SessionConstructor = Readonly<{
196
213
  resume: EditableArtifactLiveResume;
197
214
  sink: EditableArtifactLiveSinkPort;
198
215
  parentSignal?: AbortSignal;
216
+ authorizeSourceSession?: OpenEditableArtifactLiveInput["authorizeSourceSession"];
199
217
  }>;
200
218
 
201
219
  type InFlight = Readonly<{
@@ -213,6 +231,8 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
213
231
  private readonly dependencies: EditableArtifactLiveServerDependencies;
214
232
  private readonly options: NormalizedOptions;
215
233
  private readonly ticket: EditableArtifactLiveTicketRecord;
234
+ private readonly authorizeSourceSession: OpenEditableArtifactLiveInput["authorizeSourceSession"];
235
+ private readonly sourceSessionLeaseExpiresAt: number | null;
216
236
  private readonly resume: EditableArtifactLiveResume;
217
237
  private readonly sink: EditableArtifactLiveSinkPort;
218
238
  private readonly abort = new AbortController();
@@ -246,6 +266,10 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
246
266
  this.dependencies = input.dependencies;
247
267
  this.options = input.options;
248
268
  this.ticket = input.ticket;
269
+ this.authorizeSourceSession = input.authorizeSourceSession;
270
+ this.sourceSessionLeaseExpiresAt = input.ticket.sourceSessionAuthority
271
+ ? Date.parse(input.ticket.issuedAt) + SOURCE_SESSION_LIVE_LEASE_MS
272
+ : null;
249
273
  this.artifactId = input.ticket.artifactId;
250
274
  this.modality = input.ticket.modality;
251
275
  this.streamEpoch = input.streamEpoch;
@@ -267,6 +291,13 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
267
291
  async start(): Promise<void> {
268
292
  try {
269
293
  this.assertOpen();
294
+ // Deadline enforcement owns no work-queue slot and starts before any
295
+ // bootstrap/authorization await. A hung callback cannot retain a socket.
296
+ if (this.sourceSessionLeaseExpiresAt !== null) {
297
+ void this.runSourceSessionLease().catch((error: unknown) => {
298
+ if (!this.abort.signal.aborted) void this.fail(error);
299
+ });
300
+ }
270
301
  this.authorizationRevision = await this.requireRead();
271
302
  this.assertOpen();
272
303
 
@@ -312,7 +343,6 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
312
343
  scope: this.ticket.scope,
313
344
  artifactId: this.artifactId,
314
345
  resume: this.resume,
315
- protocolVersion: this.options.protocolVersion,
316
346
  });
317
347
  this.assertOpen();
318
348
  validateBootstrap(bootstrap, this.artifactId, this.modality, this.resume, this.options);
@@ -527,6 +557,7 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
527
557
  }
528
558
 
529
559
  private assertOpen(): void {
560
+ this.assertSourceSessionLease();
530
561
  if (this.closedValue) {
531
562
  throw new EditableArtifactLiveError("closed", "Live session is closed");
532
563
  }
@@ -706,7 +737,13 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
706
737
  } as const;
707
738
  await this.send(
708
739
  snapshot.modality === "spreadsheet"
709
- ? { ...common, modality: snapshot.modality, causalFrontier: causalFrontier! }
740
+ ? {
741
+ ...common,
742
+ modality: snapshot.modality,
743
+ causalFrontier: causalFrontier!,
744
+ operationProtocolVersion: snapshot.operationProtocolVersion,
745
+ snapshotVersion: snapshot.snapshotVersion,
746
+ }
710
747
  : {
711
748
  ...common,
712
749
  modality: snapshot.modality,
@@ -919,6 +956,25 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
919
956
  scope: this.ticket.scope,
920
957
  artifactId: this.artifactId,
921
958
  actor: this.ticket.actor,
959
+ ...(this.ticket.sourceSessionAuthority
960
+ ? {
961
+ authorizeCommit: async (transaction) => {
962
+ this.assertOpen();
963
+ if (
964
+ !this.authorizeSourceSession ||
965
+ !(await this.withBoundedAuthorization((signal) =>
966
+ this.authorizeSourceSession!(this.ticket, "edit", transaction, signal),
967
+ ))
968
+ ) {
969
+ throw new EditableArtifactLiveError(
970
+ "permission_changed",
971
+ "Editable artifact source session permission changed",
972
+ );
973
+ }
974
+ this.assertOpen();
975
+ },
976
+ }
977
+ : {}),
922
978
  request: {
923
979
  intentBytes: frame.intentBytes.slice(),
924
980
  requestHash: frame.requestHash,
@@ -1029,11 +1085,22 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
1029
1085
  private async checkPermission(
1030
1086
  permission: "read" | "edit",
1031
1087
  ): Promise<Awaited<ReturnType<EditableArtifactAuthorizationPort["authorize"]>>> {
1032
- const decision = await this.dependencies.authorization.authorize({
1033
- scope: this.ticket.scope,
1034
- artifactId: this.artifactId,
1035
- actor: this.ticket.actor,
1036
- permission,
1088
+ const decision = await this.withBoundedAuthorization(async (signal) => {
1089
+ if (this.ticket.sourceSessionAuthority) {
1090
+ if (
1091
+ !this.authorizeSourceSession ||
1092
+ !(await this.authorizeSourceSession(this.ticket, permission, undefined, signal))
1093
+ ) {
1094
+ return Object.freeze({ allowed: false, revision: this.authorizationRevision });
1095
+ }
1096
+ }
1097
+ signal.throwIfAborted();
1098
+ return await this.dependencies.authorization.authorize({
1099
+ scope: this.ticket.scope,
1100
+ artifactId: this.artifactId,
1101
+ actor: this.ticket.actor,
1102
+ permission,
1103
+ });
1037
1104
  });
1038
1105
  if (permission === "edit" && !this.ticket.allowEdit) {
1039
1106
  return Object.freeze({ ...decision, allowed: false });
@@ -1041,6 +1108,57 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
1041
1108
  return decision;
1042
1109
  }
1043
1110
 
1111
+ private async withBoundedAuthorization<T>(
1112
+ operation: (signal: AbortSignal) => Promise<T>,
1113
+ ): Promise<T> {
1114
+ this.assertSourceSessionLease();
1115
+ const timeout = new AbortController();
1116
+ const signal = AbortSignal.any([this.abort.signal, timeout.signal]);
1117
+ // Native artifact-only sockets retain their authorization timing. Only
1118
+ // source-bound sockets have a renewable product-session lease.
1119
+ if (this.sourceSessionLeaseExpiresAt !== null) {
1120
+ const budget = this.sourceSessionLeaseExpiresAt - this.dependencies.clock.now().getTime();
1121
+ void this.dependencies.scheduler.sleep(budget, signal).then(
1122
+ () =>
1123
+ timeout.abort(
1124
+ new EditableArtifactLiveError("ticket_expired", "Live authorization lease expired", {
1125
+ retryable: true,
1126
+ }),
1127
+ ),
1128
+ () => undefined,
1129
+ );
1130
+ }
1131
+ try {
1132
+ const result = await awaitLiveAuthorization(operation(signal), signal);
1133
+ this.assertOpen();
1134
+ return result;
1135
+ } finally {
1136
+ timeout.abort();
1137
+ }
1138
+ }
1139
+
1140
+ private async runSourceSessionLease(): Promise<void> {
1141
+ while (!this.abort.signal.aborted && this.sourceSessionLeaseExpiresAt !== null) {
1142
+ const remaining = this.sourceSessionLeaseExpiresAt - this.dependencies.clock.now().getTime();
1143
+ if (remaining <= 0) {
1144
+ await this.close("ticket_expired");
1145
+ return;
1146
+ }
1147
+ await this.dependencies.scheduler.sleep(remaining, this.abort.signal);
1148
+ }
1149
+ }
1150
+ private assertSourceSessionLease(): void {
1151
+ if (
1152
+ this.sourceSessionLeaseExpiresAt !== null &&
1153
+ this.dependencies.clock.now().getTime() >= this.sourceSessionLeaseExpiresAt
1154
+ ) {
1155
+ void this.close("ticket_expired");
1156
+ throw new EditableArtifactLiveError("ticket_expired", "Source session lease expired", {
1157
+ retryable: true,
1158
+ });
1159
+ }
1160
+ }
1161
+
1044
1162
  private async sendBarrier(): Promise<void> {
1045
1163
  if (!this.sentStateHash)
1046
1164
  throw new EditableArtifactLiveError("durable_gap", "Bootstrap has no state boundary");
@@ -1056,6 +1174,7 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
1056
1174
 
1057
1175
  private async send(frame: EditableArtifactLiveServerFrame): Promise<void> {
1058
1176
  if (this.closedValue) return;
1177
+ this.assertSourceSessionLease();
1059
1178
  const bytes = estimateFrameBytes(frame);
1060
1179
  if (bytes > this.options.maxOutboundFrameBytes) {
1061
1180
  throw new EditableArtifactLiveError(
@@ -1125,9 +1244,21 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
1125
1244
  ),
1126
1245
  );
1127
1246
  while (!this.abort.signal.aborted) {
1128
- await this.dependencies.scheduler.sleep(tick, this.abort.signal);
1247
+ const leaseRemaining =
1248
+ this.sourceSessionLeaseExpiresAt === null
1249
+ ? tick
1250
+ : this.sourceSessionLeaseExpiresAt - this.dependencies.clock.now().getTime();
1251
+ if (leaseRemaining <= 0) {
1252
+ await this.close("ticket_expired");
1253
+ return;
1254
+ }
1255
+ await this.dependencies.scheduler.sleep(Math.min(tick, leaseRemaining), this.abort.signal);
1129
1256
  if (this.abort.signal.aborted || this.closedValue) return;
1130
1257
  const now = this.dependencies.clock.now().getTime();
1258
+ if (this.sourceSessionLeaseExpiresAt !== null && now >= this.sourceSessionLeaseExpiresAt) {
1259
+ await this.close("ticket_expired");
1260
+ return;
1261
+ }
1131
1262
  if (this.oldestInFlightAt !== 0 && now - this.oldestInFlightAt >= this.options.ackTimeoutMs) {
1132
1263
  await this.enqueue(() => this.requireResync("slow_client", this.targetHead));
1133
1264
  return;
@@ -1171,6 +1302,29 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
1171
1302
  }
1172
1303
  }
1173
1304
 
1305
+ /** Settle callers on cancellation even when an embedding callback ignores it. */
1306
+ function awaitLiveAuthorization<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
1307
+ return new Promise<T>((resolve, reject) => {
1308
+ const abort = () => {
1309
+ signal.removeEventListener("abort", abort);
1310
+ reject(signal.reason);
1311
+ };
1312
+ operation.then(
1313
+ (value) => {
1314
+ signal.removeEventListener("abort", abort);
1315
+ if (signal.aborted) reject(signal.reason);
1316
+ else resolve(value);
1317
+ },
1318
+ (error: unknown) => {
1319
+ signal.removeEventListener("abort", abort);
1320
+ reject(error);
1321
+ },
1322
+ );
1323
+ if (signal.aborted) abort();
1324
+ else signal.addEventListener("abort", abort, { once: true });
1325
+ });
1326
+ }
1327
+
1174
1328
  function decodeClientFrame(
1175
1329
  bytes: Uint8Array,
1176
1330
  artifactId: EditableArtifactId,
@@ -1264,7 +1418,7 @@ function validateResume(
1264
1418
  resume: EditableArtifactLiveResume,
1265
1419
  durableModality: EditableArtifactModality,
1266
1420
  ): void {
1267
- const resumeModality = resume.modality ?? "spreadsheet";
1421
+ const resumeModality = resume.modality;
1268
1422
  if (resumeModality !== durableModality) {
1269
1423
  throw new EditableArtifactLiveError(
1270
1424
  "invalid_frame",
@@ -1608,7 +1762,7 @@ function boundedString(value: string, label: string, maxBytes: number): void {
1608
1762
 
1609
1763
  function normalizeOptions(options: EditableArtifactLiveServerOptions): NormalizedOptions {
1610
1764
  const normalized = {
1611
- protocolVersion: options.protocolVersion ?? EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION,
1765
+ protocolVersion: EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION,
1612
1766
  maxClientFrameBytes: options.maxClientFrameBytes ?? DEFAULT_MAX_CLIENT_FRAME_BYTES,
1613
1767
  maxOutboundFrameBytes: options.maxOutboundFrameBytes ?? DEFAULT_MAX_OUTBOUND_FRAME_BYTES,
1614
1768
  maxSnapshotBytes: options.maxSnapshotBytes ?? DEFAULT_MAX_SNAPSHOT_BYTES,