@opengeni/core 3.0.1 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +4 -4
  2. package/dist/access/index.d.ts +131 -7
  3. package/dist/access/service-initiator.d.ts +3 -0
  4. package/dist/application/archived-session-imports.d.ts +6 -0
  5. package/dist/application/composer-submit.d.ts +1 -1
  6. package/dist/application/connect-operation.d.ts +6 -0
  7. package/dist/application/external-workspace-members.d.ts +13 -1
  8. package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
  9. package/dist/application/modal-native-original-configuration.d.ts +79 -0
  10. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  11. package/dist/application/new-session-drafts.d.ts +29 -2
  12. package/dist/application/organization-privacy.d.ts +57 -0
  13. package/dist/application/prepared-mcp-connection.d.ts +31 -0
  14. package/dist/application/sandbox-recovery.d.ts +58 -0
  15. package/dist/application/session-commands.d.ts +3 -0
  16. package/dist/application/session-tenancy.d.ts +13 -4
  17. package/dist/billing/agent-run-admission.d.ts +21 -0
  18. package/dist/billing/limits.d.ts +27 -1
  19. package/dist/billing/model-call-admission.d.ts +34 -0
  20. package/dist/billing/model-usage-settlement.d.ts +112 -0
  21. package/dist/canonical-human-identities.js +1 -1
  22. package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
  23. package/dist/chunk-7T2SZ7BI.js.map +1 -0
  24. package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
  25. package/dist/chunk-OMJFIIWK.js.map +1 -0
  26. package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
  27. package/dist/chunk-QI2M2B2K.js.map +1 -0
  28. package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
  29. package/dist/chunk-TTWTXQB2.js.map +1 -0
  30. package/dist/codex-model-availability.d.ts +55 -0
  31. package/dist/default-session-model.d.ts +192 -0
  32. package/dist/dependencies.d.ts +41 -7
  33. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  34. package/dist/domain/agent-config-resolution.d.ts +49 -0
  35. package/dist/domain/automations.d.ts +8 -1
  36. package/dist/domain/capabilities.d.ts +112 -8
  37. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  38. package/dist/domain/connector-tool-permissions.d.ts +62 -0
  39. package/dist/domain/direct-model-provider.d.ts +3 -0
  40. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  41. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  42. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  43. package/dist/domain/editable-artifacts/service.d.ts +3 -1
  44. package/dist/domain/editable-artifacts/types.d.ts +1 -1
  45. package/dist/domain/encryption-key.d.ts +12 -0
  46. package/dist/domain/insights-timing.d.ts +16 -0
  47. package/dist/domain/insights-usage.d.ts +27 -0
  48. package/dist/domain/insights.d.ts +21 -3
  49. package/dist/domain/knowledge-files.d.ts +1 -1
  50. package/dist/domain/knowledge-preparation.d.ts +10 -0
  51. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  52. package/dist/domain/knowledge-search.d.ts +15 -76
  53. package/dist/domain/mcp-account-bindings.d.ts +17 -0
  54. package/dist/domain/mcp-account-routes.d.ts +21 -0
  55. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  56. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  57. package/dist/domain/personal-connection-delegations.d.ts +73 -12
  58. package/dist/domain/pr-review.d.ts +121 -6
  59. package/dist/domain/realtime-voice-billing.d.ts +103 -0
  60. package/dist/domain/resources.d.ts +17 -1
  61. package/dist/domain/scheduled-task-access.d.ts +265 -0
  62. package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
  63. package/dist/domain/scheduled-task-audience.d.ts +70 -0
  64. package/dist/domain/scheduled-tasks.d.ts +61 -18
  65. package/dist/domain/session-connection-accounts.d.ts +53 -0
  66. package/dist/domain/session-file-access.d.ts +4 -0
  67. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  68. package/dist/domain/session-start-timing.d.ts +7 -0
  69. package/dist/domain/session-tool-policy.d.ts +102 -4
  70. package/dist/domain/sessions.d.ts +179 -15
  71. package/dist/domain/site-audience.d.ts +52 -0
  72. package/dist/domain/skills.d.ts +29 -1
  73. package/dist/domain/slack-bot.d.ts +46 -1
  74. package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
  75. package/dist/domain/vaults/index.d.ts +7 -0
  76. package/dist/domain/vaults/mapping.d.ts +25 -0
  77. package/dist/domain/vaults/provider-registry.d.ts +72 -0
  78. package/dist/domain/vaults/provider-sync.d.ts +176 -0
  79. package/dist/domain/vaults/provider-writes.d.ts +57 -0
  80. package/dist/domain/vaults/secret-resolver.d.ts +34 -0
  81. package/dist/domain/vaults/types.d.ts +24 -0
  82. package/dist/domain/vaults/vault-service.d.ts +35 -0
  83. package/dist/domain/voice-input-billing.d.ts +77 -0
  84. package/dist/domain/web-search-billing.d.ts +51 -0
  85. package/dist/domain/workspace-credits.d.ts +7 -0
  86. package/dist/editable-artifact-live/application.d.ts +2 -1
  87. package/dist/editable-artifact-live/server.d.ts +5 -1
  88. package/dist/editable-artifact-live/ticket.d.ts +2 -1
  89. package/dist/editable-artifact-live/types.d.ts +9 -0
  90. package/dist/editable-artifact-live.js +2 -2
  91. package/dist/editable-artifacts.js +10 -2
  92. package/dist/goal-admission.d.ts +627 -0
  93. package/dist/host-session-usage-limit.d.ts +15 -0
  94. package/dist/index.d.ts +43 -8
  95. package/dist/index.js +22682 -11743
  96. package/dist/index.js.map +1 -1
  97. package/dist/managed-session.d.ts +110 -7
  98. package/dist/model-call.d.ts +58 -0
  99. package/dist/model-catalog.d.ts +29 -3
  100. package/dist/model-provider-credentials.d.ts +43 -0
  101. package/dist/product-usage-metrics.d.ts +30 -0
  102. package/dist/rigs/index.d.ts +1 -13
  103. package/dist/sandbox/fleet.d.ts +58 -4
  104. package/dist/sandbox/routing.d.ts +42 -2
  105. package/dist/sandbox/runtime-settings.d.ts +27 -17
  106. package/dist/session-authorization.d.ts +10 -0
  107. package/dist/session-usage-limit-metrics.d.ts +60 -0
  108. package/dist/session-usage-limits.d.ts +89 -0
  109. package/dist/site-agent-clamp.d.ts +71 -0
  110. package/dist/site-session-origin.d.ts +39 -2
  111. package/dist/transcription.d.ts +120 -3
  112. package/dist/turn-surface.d.ts +20 -0
  113. package/dist/user-presence.d.ts +32 -0
  114. package/dist/workflow-wake-contract.d.ts +13 -1
  115. package/package.json +15 -22
  116. package/src/access/index.ts +961 -38
  117. package/src/access/service-initiator.ts +50 -0
  118. package/src/application/api-integration-servers.ts +9 -0
  119. package/src/application/archived-session-imports.ts +161 -0
  120. package/src/application/composer-submit.ts +3 -2
  121. package/src/application/connect-authority.ts +3 -3
  122. package/src/application/connect-operation.ts +37 -3
  123. package/src/application/external-continuation.ts +20 -3
  124. package/src/application/external-link-work-admission.ts +3 -0
  125. package/src/application/external-workspace-members.ts +51 -3
  126. package/src/application/modal-native-live-declaration-join.ts +196 -0
  127. package/src/application/modal-native-original-configuration.ts +261 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +172 -55
  130. package/src/application/organization-privacy.ts +161 -0
  131. package/src/application/prepared-mcp-connection.ts +133 -0
  132. package/src/application/sandbox-recovery.ts +78 -0
  133. package/src/application/session-commands.ts +73 -1
  134. package/src/application/session-mcp-credential-rotation.ts +61 -18
  135. package/src/application/session-tenancy.ts +262 -44
  136. package/src/application/user-resource-grants.ts +1 -29
  137. package/src/billing/agent-run-admission.ts +117 -0
  138. package/src/billing/limits.ts +138 -14
  139. package/src/billing/model-call-admission.ts +118 -0
  140. package/src/billing/model-usage-settlement.ts +472 -0
  141. package/src/codex-model-availability.ts +264 -0
  142. package/src/default-session-model.ts +692 -0
  143. package/src/dependencies.ts +48 -6
  144. package/src/developer-setup-credential-ceiling.ts +60 -0
  145. package/src/domain/agent-config-resolution.ts +148 -0
  146. package/src/domain/automations.ts +76 -2
  147. package/src/domain/capabilities.ts +830 -181
  148. package/src/domain/connection-account-selection-error.ts +19 -0
  149. package/src/domain/connector-tool-permissions.ts +572 -0
  150. package/src/domain/direct-model-provider.ts +62 -0
  151. package/src/domain/editable-artifacts/agent-application.ts +44 -6
  152. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  153. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  154. package/src/domain/editable-artifacts/ports.ts +11 -0
  155. package/src/domain/editable-artifacts/service.ts +20 -0
  156. package/src/domain/editable-artifacts/types.ts +1 -1
  157. package/src/domain/encryption-key.ts +26 -0
  158. package/src/domain/file-owner.ts +16 -1
  159. package/src/domain/github-action-policies.ts +1 -1
  160. package/src/domain/github-skill-source.ts +3 -1
  161. package/src/domain/insights-timing.ts +95 -0
  162. package/src/domain/insights-usage.ts +67 -0
  163. package/src/domain/insights.ts +167 -43
  164. package/src/domain/knowledge-files.ts +10 -3
  165. package/src/domain/knowledge-messages.ts +1 -0
  166. package/src/domain/knowledge-preparation.test.ts +276 -0
  167. package/src/domain/knowledge-preparation.ts +147 -0
  168. package/src/domain/knowledge-search.test.ts +617 -5
  169. package/src/domain/knowledge-search.ts +240 -23
  170. package/src/domain/mcp-account-bindings.ts +169 -0
  171. package/src/domain/mcp-account-routes.ts +152 -0
  172. package/src/domain/native-mcp-connection-admission.ts +14 -0
  173. package/src/domain/organization-integration-catalog.ts +88 -0
  174. package/src/domain/personal-connection-delegations.ts +399 -257
  175. package/src/domain/pr-review.ts +24 -8
  176. package/src/domain/realtime-voice-billing.ts +425 -0
  177. package/src/domain/resources.ts +50 -4
  178. package/src/domain/scheduled-task-access.ts +1089 -0
  179. package/src/domain/scheduled-task-account-audit.ts +90 -0
  180. package/src/domain/scheduled-task-audience.ts +249 -0
  181. package/src/domain/scheduled-tasks.ts +1313 -374
  182. package/src/domain/session-connection-accounts.ts +239 -0
  183. package/src/domain/session-file-access.ts +47 -0
  184. package/src/domain/session-native-connection-replacement.ts +64 -0
  185. package/src/domain/session-start-timing.ts +60 -0
  186. package/src/domain/session-tool-policy.ts +489 -14
  187. package/src/domain/sessions.ts +2254 -717
  188. package/src/domain/site-audience.ts +130 -0
  189. package/src/domain/skills.ts +9 -0
  190. package/src/domain/slack-bot.ts +141 -4
  191. package/src/domain/slack-publication-secret-safety.ts +1 -1
  192. package/src/domain/vaults/index.ts +19 -0
  193. package/src/domain/vaults/mapping.ts +190 -0
  194. package/src/domain/vaults/provider-registry.ts +201 -0
  195. package/src/domain/vaults/provider-sync.ts +531 -0
  196. package/src/domain/vaults/provider-writes.ts +288 -0
  197. package/src/domain/vaults/secret-resolver.ts +464 -0
  198. package/src/domain/vaults/types.ts +26 -0
  199. package/src/domain/vaults/vault-service.ts +1316 -0
  200. package/src/domain/voice-input-billing.ts +330 -0
  201. package/src/domain/web-search-billing.ts +169 -0
  202. package/src/domain/workspace-credits.ts +13 -0
  203. package/src/editable-artifact-live/application.ts +8 -1
  204. package/src/editable-artifact-live/server.ts +154 -6
  205. package/src/editable-artifact-live/ticket.ts +86 -1
  206. package/src/editable-artifact-live/types.ts +10 -0
  207. package/src/goal-admission.ts +220 -0
  208. package/src/host-session-usage-limit.ts +25 -0
  209. package/src/index.ts +75 -11
  210. package/src/managed-session.ts +137 -6
  211. package/src/model-call.ts +79 -0
  212. package/src/model-catalog.ts +345 -91
  213. package/src/model-provider-credentials.ts +394 -0
  214. package/src/product-usage-metrics.ts +101 -0
  215. package/src/rigs/index.ts +23 -111
  216. package/src/sandbox/fleet.ts +253 -28
  217. package/src/sandbox/routing.ts +204 -43
  218. package/src/sandbox/runtime-settings.ts +159 -85
  219. package/src/session-authorization.ts +172 -23
  220. package/src/session-usage-limit-metrics.ts +95 -0
  221. package/src/session-usage-limits.ts +415 -0
  222. package/src/site-agent-clamp.ts +226 -0
  223. package/src/site-session-origin.ts +84 -5
  224. package/src/transcription.ts +123 -4
  225. package/src/turn-surface.ts +47 -0
  226. package/src/user-presence.ts +134 -0
  227. package/src/workflow-wake-contract.ts +14 -1
  228. package/dist/application/host-mcp-owner.d.ts +0 -6
  229. package/dist/application/host-mcp-resolvers.d.ts +0 -13
  230. package/dist/chunk-5WZYFL45.js +0 -266
  231. package/dist/chunk-5WZYFL45.js.map +0 -1
  232. package/dist/chunk-H7X52RI2.js.map +0 -1
  233. package/dist/chunk-OF65T3PM.js.map +0 -1
  234. package/dist/chunk-VCZFFEPB.js.map +0 -1
  235. package/dist/chunk-ZYCNLYGH.js.map +0 -1
  236. package/dist/domain/connection-authority.d.ts +0 -97
  237. package/dist/domain/connection-authority.js +0 -169
  238. package/dist/domain/connection-authority.js.map +0 -1
  239. package/dist/domain/environments.d.ts +0 -33
  240. package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
  241. package/dist/domain/host-mcp-task-admission.d.ts +0 -18
  242. package/dist/domain/packs.d.ts +0 -59
  243. package/dist/domain/product-integration-pack.d.ts +0 -44
  244. package/dist/domain/product-integration-skill.gen.d.ts +0 -5
  245. package/dist/remote-mcp-credentials.d.ts +0 -23
  246. package/dist/remote-mcp-credentials.js +0 -11
  247. package/dist/remote-mcp-credentials.js.map +0 -1
  248. package/src/application/host-mcp-owner.ts +0 -41
  249. package/src/application/host-mcp-resolvers.ts +0 -84
  250. package/src/domain/connection-authority.ts +0 -329
  251. package/src/domain/environments.ts +0 -120
  252. package/src/domain/host-mcp-authority-source-admission.ts +0 -25
  253. package/src/domain/host-mcp-task-admission.ts +0 -121
  254. package/src/domain/packs.ts +0 -699
  255. package/src/domain/product-integration-pack.ts +0 -42
  256. package/src/domain/product-integration-skill.gen.ts +0 -52
  257. package/src/remote-mcp-credentials.ts +0 -361
@@ -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
@@ -112,6 +117,13 @@ export type OpenEditableArtifactLiveInput = Readonly<{
112
117
  resume: EditableArtifactLiveResume;
113
118
  sink: EditableArtifactLiveSinkPort;
114
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>;
115
127
  }>;
116
128
 
117
129
  export interface EditableArtifactLiveSession {
@@ -160,6 +172,7 @@ export class EditableArtifactLiveServer {
160
172
  modality: EditableArtifactModality;
161
173
  actor: EditableArtifactActor;
162
174
  allowEdit: boolean;
175
+ sourceSessionAuthority?: EditableArtifactLiveSourceSessionAuthority;
163
176
  }>,
164
177
  ): Promise<EditableArtifactLiveTicket> {
165
178
  return this.ticketAuthority.mint(input);
@@ -183,6 +196,9 @@ export class EditableArtifactLiveServer {
183
196
  resume: input.resume,
184
197
  sink: input.sink,
185
198
  ...(input.signal === undefined ? {} : { parentSignal: input.signal }),
199
+ ...(input.authorizeSourceSession
200
+ ? { authorizeSourceSession: input.authorizeSourceSession }
201
+ : {}),
186
202
  });
187
203
  await session.start();
188
204
  return session;
@@ -197,6 +213,7 @@ type SessionConstructor = Readonly<{
197
213
  resume: EditableArtifactLiveResume;
198
214
  sink: EditableArtifactLiveSinkPort;
199
215
  parentSignal?: AbortSignal;
216
+ authorizeSourceSession?: OpenEditableArtifactLiveInput["authorizeSourceSession"];
200
217
  }>;
201
218
 
202
219
  type InFlight = Readonly<{
@@ -214,6 +231,8 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
214
231
  private readonly dependencies: EditableArtifactLiveServerDependencies;
215
232
  private readonly options: NormalizedOptions;
216
233
  private readonly ticket: EditableArtifactLiveTicketRecord;
234
+ private readonly authorizeSourceSession: OpenEditableArtifactLiveInput["authorizeSourceSession"];
235
+ private readonly sourceSessionLeaseExpiresAt: number | null;
217
236
  private readonly resume: EditableArtifactLiveResume;
218
237
  private readonly sink: EditableArtifactLiveSinkPort;
219
238
  private readonly abort = new AbortController();
@@ -247,6 +266,10 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
247
266
  this.dependencies = input.dependencies;
248
267
  this.options = input.options;
249
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;
250
273
  this.artifactId = input.ticket.artifactId;
251
274
  this.modality = input.ticket.modality;
252
275
  this.streamEpoch = input.streamEpoch;
@@ -268,6 +291,13 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
268
291
  async start(): Promise<void> {
269
292
  try {
270
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
+ }
271
301
  this.authorizationRevision = await this.requireRead();
272
302
  this.assertOpen();
273
303
 
@@ -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
  }
@@ -925,6 +956,25 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
925
956
  scope: this.ticket.scope,
926
957
  artifactId: this.artifactId,
927
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
+ : {}),
928
978
  request: {
929
979
  intentBytes: frame.intentBytes.slice(),
930
980
  requestHash: frame.requestHash,
@@ -1035,11 +1085,22 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
1035
1085
  private async checkPermission(
1036
1086
  permission: "read" | "edit",
1037
1087
  ): Promise<Awaited<ReturnType<EditableArtifactAuthorizationPort["authorize"]>>> {
1038
- const decision = await this.dependencies.authorization.authorize({
1039
- scope: this.ticket.scope,
1040
- artifactId: this.artifactId,
1041
- actor: this.ticket.actor,
1042
- 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
+ });
1043
1104
  });
1044
1105
  if (permission === "edit" && !this.ticket.allowEdit) {
1045
1106
  return Object.freeze({ ...decision, allowed: false });
@@ -1047,6 +1108,57 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
1047
1108
  return decision;
1048
1109
  }
1049
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
+
1050
1162
  private async sendBarrier(): Promise<void> {
1051
1163
  if (!this.sentStateHash)
1052
1164
  throw new EditableArtifactLiveError("durable_gap", "Bootstrap has no state boundary");
@@ -1062,6 +1174,7 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
1062
1174
 
1063
1175
  private async send(frame: EditableArtifactLiveServerFrame): Promise<void> {
1064
1176
  if (this.closedValue) return;
1177
+ this.assertSourceSessionLease();
1065
1178
  const bytes = estimateFrameBytes(frame);
1066
1179
  if (bytes > this.options.maxOutboundFrameBytes) {
1067
1180
  throw new EditableArtifactLiveError(
@@ -1131,9 +1244,21 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
1131
1244
  ),
1132
1245
  );
1133
1246
  while (!this.abort.signal.aborted) {
1134
- 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);
1135
1256
  if (this.abort.signal.aborted || this.closedValue) return;
1136
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
+ }
1137
1262
  if (this.oldestInFlightAt !== 0 && now - this.oldestInFlightAt >= this.options.ackTimeoutMs) {
1138
1263
  await this.enqueue(() => this.requireResync("slow_client", this.targetHead));
1139
1264
  return;
@@ -1177,6 +1302,29 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
1177
1302
  }
1178
1303
  }
1179
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
+
1180
1328
  function decodeClientFrame(
1181
1329
  bytes: Uint8Array,
1182
1330
  artifactId: EditableArtifactId,
@@ -1,3 +1,7 @@
1
+ import { AccessGrant } from "@opengeni/contracts";
2
+ import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
3
+ import { z } from "zod";
4
+ import { deflateRawSync, inflateRawSync } from "node:zlib";
1
5
  import type { EditableArtifactAuthorizationPort } from "../domain/editable-artifacts/ports";
2
6
  import {
3
7
  assertIsoTimestamp,
@@ -19,11 +23,22 @@ import {
19
23
  EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION,
20
24
  type EditableArtifactLiveTicket,
21
25
  type EditableArtifactLiveTicketRecord,
26
+ type EditableArtifactLiveSourceSessionAuthority,
22
27
  } from "./types";
23
28
 
24
29
  const DEFAULT_TTL_MS = 30_000;
25
30
  const MAX_TTL_MS = 60_000;
26
31
  const MAX_TOKEN_BYTES = 4_096;
32
+ const SOURCE_AUTHORITY_MARKER = ".ogs1.";
33
+ const COMPRESSED_SOURCE_AUTHORITY_MARKER = ".ogs2.";
34
+ const MAX_SOURCE_AUTHORITY_BYTES = 16 * 1024;
35
+ const SourceSessionAuthority = z
36
+ .object({
37
+ sessionId: z.string().uuid(),
38
+ grant: AccessGrant,
39
+ externalContinuation: ExternalActorContinuation.optional(),
40
+ })
41
+ .strict();
27
42
 
28
43
  export type EditableArtifactLiveTicketAuthorityDependencies = Readonly<{
29
44
  authorization: EditableArtifactAuthorizationPort;
@@ -52,6 +67,7 @@ export class EditableArtifactLiveTicketAuthority {
52
67
  modality: EditableArtifactModality;
53
68
  actor: EditableArtifactActor;
54
69
  allowEdit: boolean;
70
+ sourceSessionAuthority?: EditableArtifactLiveSourceSessionAuthority;
55
71
  }>,
56
72
  ): Promise<EditableArtifactLiveTicket> {
57
73
  const scope = editableArtifactScope(input.scope);
@@ -74,7 +90,28 @@ export class EditableArtifactLiveTicketAuthority {
74
90
  "Editable artifact read permission denied",
75
91
  );
76
92
  }
77
- const token = this.dependencies.tokens.randomOpaqueToken();
93
+ const sourceSessionAuthority = input.sourceSessionAuthority
94
+ ? SourceSessionAuthority.parse(input.sourceSessionAuthority)
95
+ : undefined;
96
+ if (sourceSessionAuthority) {
97
+ assertSourceSessionAuthority(sourceSessionAuthority, scope, actor);
98
+ }
99
+ const sourceBytes = sourceSessionAuthority
100
+ ? Buffer.from(JSON.stringify(sourceSessionAuthority))
101
+ : null;
102
+ if (sourceBytes && sourceBytes.byteLength > MAX_SOURCE_AUTHORITY_BYTES) {
103
+ throw new EditableArtifactLiveError(
104
+ "invalid_ticket",
105
+ "Live ticket source authority is too large",
106
+ );
107
+ }
108
+ const nonce = this.dependencies.tokens.randomOpaqueToken();
109
+ // The execute-only PG store persists the digest of this COMPLETE token.
110
+ // Changing/removing the suffix cannot consume its record. Decode only after
111
+ // atomic consume authenticates it, keeping the existing fixed SQL DTO intact.
112
+ const token = sourceBytes
113
+ ? `${nonce}${COMPRESSED_SOURCE_AUTHORITY_MARKER}${deflateRawSync(sourceBytes).toString("base64url")}`
114
+ : nonce;
78
115
  assertToken(token);
79
116
  const tokenDigest = await this.dependencies.tokens.digestOpaqueToken(token);
80
117
  assertDigest(tokenDigest);
@@ -124,6 +161,10 @@ export class EditableArtifactLiveTicketAuthority {
124
161
  );
125
162
  }
126
163
  validateTicketRecord(record);
164
+ const sourceSessionAuthority = decodeSourceSessionAuthority(input.token);
165
+ if (sourceSessionAuthority) {
166
+ assertSourceSessionAuthority(sourceSessionAuthority, record.scope, record.actor);
167
+ }
127
168
  if (Date.parse(record.expiresAt) <= this.dependencies.clock.now().getTime()) {
128
169
  throw new EditableArtifactLiveError("ticket_expired", "Live ticket has expired", {
129
170
  retryable: true,
@@ -145,6 +186,7 @@ export class EditableArtifactLiveTicketAuthority {
145
186
  ...record,
146
187
  scope: editableArtifactScope(record.scope),
147
188
  actor: Object.freeze({ ...record.actor }) as EditableArtifactActor,
189
+ ...(sourceSessionAuthority ? { sourceSessionAuthority } : {}),
148
190
  });
149
191
  }
150
192
  }
@@ -194,6 +236,49 @@ function validateTicketRecord(record: EditableArtifactLiveTicketRecord): void {
194
236
  assertIsoTimestamp(record.expiresAt, "ticket expiresAt");
195
237
  }
196
238
 
239
+ function decodeSourceSessionAuthority(
240
+ token: string,
241
+ ): EditableArtifactLiveSourceSessionAuthority | undefined {
242
+ const compressed = token.includes(COMPRESSED_SOURCE_AUTHORITY_MARKER);
243
+ const marker = compressed ? COMPRESSED_SOURCE_AUTHORITY_MARKER : SOURCE_AUTHORITY_MARKER;
244
+ const separator = token.lastIndexOf(marker);
245
+ if (separator === -1) return undefined;
246
+ try {
247
+ return SourceSessionAuthority.parse(
248
+ JSON.parse(
249
+ (compressed
250
+ ? inflateRawSync(Buffer.from(token.slice(separator + marker.length), "base64url"), {
251
+ maxOutputLength: MAX_SOURCE_AUTHORITY_BYTES,
252
+ })
253
+ : Buffer.from(token.slice(separator + marker.length), "base64url")
254
+ ).toString("utf8"),
255
+ ),
256
+ );
257
+ } catch {
258
+ throw new EditableArtifactLiveError(
259
+ "invalid_ticket",
260
+ "Live ticket source authority is invalid",
261
+ );
262
+ }
263
+ }
264
+
265
+ function assertSourceSessionAuthority(
266
+ authority: EditableArtifactLiveSourceSessionAuthority,
267
+ scope: EditableArtifactScope,
268
+ actor: EditableArtifactActor,
269
+ ): void {
270
+ if (
271
+ authority.grant.accountId !== scope.accountId ||
272
+ authority.grant.workspaceId !== scope.workspaceId ||
273
+ authority.grant.subjectId !== actor.subjectId
274
+ ) {
275
+ throw new EditableArtifactLiveError(
276
+ "invalid_ticket",
277
+ "Live ticket source scope does not match",
278
+ );
279
+ }
280
+ }
281
+
197
282
  function assertModality(value: EditableArtifactModality): EditableArtifactModality {
198
283
  if (value !== "document" && value !== "spreadsheet" && value !== "presentation") {
199
284
  throw new TypeError("ticket modality is invalid");
@@ -1,4 +1,6 @@
1
1
  import { EDITABLE_ARTIFACT_LIVE_WIRE_VERSION } from "@opengeni/contracts/editable-artifact-live";
2
+ import type { AccessGrant } from "@opengeni/contracts";
3
+ import type { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
2
4
  import type {
3
5
  EditableArtifactActor,
4
6
  EditableArtifactCausalFrontier,
@@ -14,6 +16,13 @@ import type {
14
16
 
15
17
  export const EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION = EDITABLE_ARTIFACT_LIVE_WIRE_VERSION;
16
18
 
19
+ /** Authenticated by the digest of the complete one-use token, never client claims. */
20
+ export type EditableArtifactLiveSourceSessionAuthority = Readonly<{
21
+ sessionId: string;
22
+ grant: AccessGrant;
23
+ externalContinuation?: ExternalActorContinuation | undefined;
24
+ }>;
25
+
17
26
  export type EditableArtifactLiveTicket = Readonly<{
18
27
  artifactId: EditableArtifactId;
19
28
  modality: EditableArtifactModality;
@@ -30,6 +39,7 @@ export type EditableArtifactLiveTicketRecord = Readonly<{
30
39
  artifactId: EditableArtifactId;
31
40
  modality: EditableArtifactModality;
32
41
  actor: EditableArtifactActor;
42
+ sourceSessionAuthority?: EditableArtifactLiveSourceSessionAuthority;
33
43
  /** Delegated ceiling captured by the authenticated ticket-minting request. */
34
44
  allowEdit: boolean;
35
45
  protocolVersion: number;
@@ -0,0 +1,220 @@
1
+ import {
2
+ isModelAvailableForNewSelection,
3
+ policyChargesCreditsForModel,
4
+ policyProviderIdForModel,
5
+ resolveModelProvider,
6
+ runnableLatencyModesForModel,
7
+ withCodexCatalogProvider,
8
+ withXaiSubscriptionCatalogProvider,
9
+ type Settings,
10
+ } from "@opengeni/config";
11
+ import {
12
+ evaluateWorkspaceModelPolicy,
13
+ type GoalAdmissionPausedReason,
14
+ type Session,
15
+ } from "@opengeni/contracts";
16
+ import { isCodexBilledModel } from "@opengeni/codex";
17
+ import {
18
+ checkSessionUsageLimitAdmission,
19
+ getWorkspaceModelPolicy,
20
+ type Database,
21
+ } from "@opengeni/db";
22
+ import { resolveWorkspaceCatalogSettings } from "./model-catalog";
23
+ import { agentRunAdmissionDenial } from "./billing/agent-run-admission";
24
+
25
+ export type GoalAdmissionBlock = { pausedReason: GoalAdmissionPausedReason; message: string };
26
+
27
+ /** Catalog membership never grants access to a connection or changes the selected model. */
28
+ export function goalContinuationModelDecision(input: {
29
+ settings: Settings;
30
+ workspaceModelPolicy: Awaited<ReturnType<typeof getWorkspaceModelPolicy>>;
31
+ inheritedModel: string;
32
+ codexCompactionMode?: Session["codexCompactionMode"];
33
+ latencyMode?: Session["latencyMode"];
34
+ }): { model: string; blocked: string | null; pausedReason?: GoalAdmissionPausedReason } {
35
+ const catalogSettings = input.settings.supergrokSubscriptionEnabled
36
+ ? withXaiSubscriptionCatalogProvider(
37
+ input.settings.codexSubscriptionEnabled
38
+ ? withCodexCatalogProvider(input.settings)
39
+ : input.settings,
40
+ )
41
+ : input.settings.codexSubscriptionEnabled
42
+ ? withCodexCatalogProvider(input.settings)
43
+ : input.settings;
44
+ const model = input.inheritedModel;
45
+ if (!resolveModelProvider(catalogSettings, model)) {
46
+ return {
47
+ model,
48
+ blocked: "The selected model is unavailable. Choose an available model before resuming.",
49
+ pausedReason: "model_unavailable",
50
+ };
51
+ }
52
+ if (!isModelAvailableForNewSelection(catalogSettings, model)) {
53
+ return {
54
+ model,
55
+ blocked: "The selected model has been retired. Choose an available model before resuming.",
56
+ pausedReason: "model_unavailable",
57
+ };
58
+ }
59
+ const policyVerdict =
60
+ input.workspaceModelPolicy === null
61
+ ? null
62
+ : evaluateWorkspaceModelPolicy(input.workspaceModelPolicy, {
63
+ providerId: policyProviderIdForModel(catalogSettings, model),
64
+ modelId: model,
65
+ chargesCredits: policyChargesCreditsForModel(catalogSettings, model),
66
+ });
67
+ if (policyVerdict && !policyVerdict.allowed) {
68
+ return {
69
+ model,
70
+ blocked:
71
+ policyVerdict.reason === "credits"
72
+ ? "Opengeni credits are turned off in this workspace. Choose a model paid by a subscription or API key before resuming."
73
+ : "Workspace policy blocks the selected model. Choose an allowed model before resuming.",
74
+ pausedReason: "model_policy",
75
+ };
76
+ }
77
+ if (input.codexCompactionMode === "remote_v2" && !isCodexBilledModel(model)) {
78
+ return {
79
+ model,
80
+ blocked: "This conversation requires a Codex model. Choose a Codex model before resuming.",
81
+ pausedReason: "model_policy",
82
+ };
83
+ }
84
+ if (
85
+ !runnableLatencyModesForModel(catalogSettings, model).includes(input.latencyMode ?? "standard")
86
+ ) {
87
+ return {
88
+ model,
89
+ blocked:
90
+ "The selected model does not support this conversation's latency mode. Choose a supported mode or model before resuming.",
91
+ pausedReason: "model_policy",
92
+ };
93
+ }
94
+ return { model, blocked: null };
95
+ }
96
+
97
+ /** The same scoped, secret-free catalog used at ordinary turn admission. */
98
+ export async function resolveGoalModelAdmission(
99
+ db: Database,
100
+ settings: Settings,
101
+ input: {
102
+ accountId: string;
103
+ workspaceId: string;
104
+ model: string;
105
+ codexCompactionMode?: Session["codexCompactionMode"];
106
+ latencyMode?: Session["latencyMode"];
107
+ },
108
+ ) {
109
+ const catalog = await resolveWorkspaceCatalogSettings(db, settings, {
110
+ accountId: input.accountId,
111
+ workspaceId: input.workspaceId,
112
+ retainedProductModelId: input.model,
113
+ });
114
+ const workspaceModelPolicy = await getWorkspaceModelPolicy(db, input.workspaceId);
115
+ return {
116
+ settings: catalog.settings,
117
+ ...goalContinuationModelDecision({
118
+ settings: catalog.settings,
119
+ workspaceModelPolicy,
120
+ inheritedModel: input.model,
121
+ ...(input.codexCompactionMode ? { codexCompactionMode: input.codexCompactionMode } : {}),
122
+ ...(input.latencyMode ? { latencyMode: input.latencyMode } : {}),
123
+ }),
124
+ };
125
+ }
126
+
127
+ /** Shared with scheduled work; reasons describe the actual admission gate. */
128
+ export async function goalRunBudgetBlocked(
129
+ services: Parameters<typeof agentRunAdmissionDenial>[0],
130
+ input: Omit<Parameters<typeof agentRunAdmissionDenial>[1], "requestedAgentRuns">,
131
+ ): Promise<GoalAdmissionBlock | null> {
132
+ const denial = await agentRunAdmissionDenial(services, { ...input, requestedAgentRuns: 1 });
133
+ if (denial === null) return null;
134
+ const blocks: Record<NonNullable<typeof denial>, GoalAdmissionBlock> = {
135
+ insufficient_credits: services.entitlements
136
+ ? {
137
+ pausedReason: "usage_policy",
138
+ message: "The application's usage policy blocks another run. Resume when it allows.",
139
+ }
140
+ : {
141
+ pausedReason: "credits",
142
+ message: "Insufficient Opengeni credits. Add credits before resuming.",
143
+ },
144
+ allowance_exhausted: {
145
+ pausedReason: "allowance",
146
+ message: "Opengeni usage allowance exhausted. Resume when your allowance is available.",
147
+ },
148
+ monthly_model_cost_limit: {
149
+ pausedReason: "budget",
150
+ message: "Monthly model spending limit reached. Resume when the spending limit allows.",
151
+ },
152
+ monthly_agent_run_limit: {
153
+ pausedReason: "usage_limit",
154
+ message: "Monthly agent run limit reached. Resume when the run limit allows.",
155
+ },
156
+ };
157
+ return blocks[denial];
158
+ }
159
+
160
+ /**
161
+ * Per-task usage limit gate for goal continuation and resume. A
162
+ * reached root limit blocks with its own reason; raising or clearing the limit
163
+ * never resumes a goal by itself. A read failure throws (never unlimited).
164
+ */
165
+ export async function goalSessionUsageLimitBlocked(
166
+ db: Database,
167
+ input: { accountId: string; workspaceId: string; sessionId: string },
168
+ ): Promise<GoalAdmissionBlock | null> {
169
+ // Server-internal: the goal's session is already authorized, so a private
170
+ // root is enforced rather than read as missing; a missing tree fails closed.
171
+ const admission = await checkSessionUsageLimitAdmission(db, { ...input, runtime: true });
172
+ if (admission.allowed) return null;
173
+ return { pausedReason: "session_usage_limit", message: admission.refusal.message };
174
+ }
175
+
176
+ export class GoalResumeBlockedError extends Error {
177
+ constructor(
178
+ readonly pausedReason: GoalAdmissionPausedReason,
179
+ message: string,
180
+ ) {
181
+ super(message);
182
+ this.name = "GoalResumeBlockedError";
183
+ }
184
+ }
185
+
186
+ /** Called under the goal transition's locks, before counters, events or wakes change. */
187
+ export async function assertGoalResumeAllowed(
188
+ services: Parameters<typeof agentRunAdmissionDenial>[0] & { catalogSourceSettings?: Settings },
189
+ session: Pick<Session, "accountId" | "workspaceId" | "model" | "codexCompactionMode"> &
190
+ Partial<Pick<Session, "latencyMode" | "id">>,
191
+ causalTurn: { initiatingHumanSubjectId: string | null } | null,
192
+ ): Promise<void> {
193
+ if (session.id) {
194
+ const limit = await goalSessionUsageLimitBlocked(services.db, {
195
+ accountId: session.accountId,
196
+ workspaceId: session.workspaceId,
197
+ sessionId: session.id,
198
+ });
199
+ if (limit) throw new GoalResumeBlockedError(limit.pausedReason, limit.message);
200
+ }
201
+ const decision = await resolveGoalModelAdmission(
202
+ services.db,
203
+ services.catalogSourceSettings ?? services.settings,
204
+ session,
205
+ );
206
+ if (decision.blocked) {
207
+ throw new GoalResumeBlockedError(decision.pausedReason!, decision.blocked);
208
+ }
209
+ const block = await goalRunBudgetBlocked(
210
+ { ...services, settings: decision.settings },
211
+ {
212
+ accountId: session.accountId,
213
+ workspaceId: session.workspaceId,
214
+ model: decision.model,
215
+ // Resuming does not lend the clicking user's allowance or credentials.
216
+ initiatingHumanSubjectId: causalTurn?.initiatingHumanSubjectId ?? null,
217
+ },
218
+ );
219
+ if (block) throw new GoalResumeBlockedError(block.pausedReason, block.message);
220
+ }
@@ -0,0 +1,25 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ /**
4
+ * A task usage limit the trusted server applies to a root session it creates
5
+ * in this process, as the host actor `subjectId`. Published Site agents use
6
+ * it: site-runtime creates each conversation as `site:<siteId>` through an
7
+ * in-process dispatch, and the per-conversation budget must be in place in the
8
+ * create transaction, before the first model call. Only code in this process
9
+ * can set it; a request cannot.
10
+ */
11
+ export type HostSessionUsageLimit = {
12
+ /** The service principal the session is created as; other callers are ignored. */
13
+ subjectId: string;
14
+ limitMicros: number;
15
+ };
16
+
17
+ const limits = new AsyncLocalStorage<HostSessionUsageLimit>();
18
+
19
+ export function withHostSessionUsageLimit<T>(limit: HostSessionUsageLimit, run: () => T): T {
20
+ return limits.run(limit, run);
21
+ }
22
+
23
+ export function currentHostSessionUsageLimit(): HostSessionUsageLimit | undefined {
24
+ return limits.getStore();
25
+ }