@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,154 @@
1
+ import type {
2
+ IssueUserResourceGrantRequest,
3
+ Permission,
4
+ SessionAuthorizationSurface,
5
+ UserResourceKind,
6
+ } from "@opengeni/contracts";
7
+ import {
8
+ issueSelfUserResourceGrant,
9
+ listSelfUserResourceAuthorities,
10
+ revokeSelfUserResourceGrant,
11
+ sessionTenancyProductActivated,
12
+ SessionTenancyNotActivatedError,
13
+ } from "@opengeni/db";
14
+ import { requirePermission, type AccessGrantAuthorization } from "../access";
15
+ import type { AppDependencies } from "../dependencies";
16
+ import { requireSessionAuthorization } from "../session-authorization";
17
+ import {
18
+ requireVerifiedOwningUser,
19
+ SessionTenancyManagedHumanRequiredError,
20
+ } from "./session-tenancy";
21
+
22
+ type UserResourceGrantDependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
23
+
24
+ const LIST_PERMISSIONS = {
25
+ connection: ["connections:read"],
26
+ document: ["documents:search"],
27
+ variable_set: ["variable-sets:list"],
28
+ rig: ["rigs:use"],
29
+ connected_machine: ["enrollments:read"],
30
+ } as const satisfies Record<UserResourceKind, readonly Permission[]>;
31
+
32
+ const ISSUE_PERMISSIONS = {
33
+ connection: ["connections:read"],
34
+ document: ["documents:search"],
35
+ variable_set: ["variable-sets:list", "variable-sets:attach", "variable-sets:use"],
36
+ rig: ["rigs:use"],
37
+ connected_machine: ["enrollments:read"],
38
+ } as const satisfies Record<UserResourceKind, readonly Permission[]>;
39
+
40
+ async function requireOwnerProductGate(
41
+ deps: Pick<UserResourceGrantDependencies, "db">,
42
+ authorization: AccessGrantAuthorization,
43
+ workspaceId: string,
44
+ permissions: readonly Permission[],
45
+ ): Promise<void> {
46
+ // Target-free gates must run before an optional target-session lookup or host
47
+ // callback so rejected principals cannot use grant management as an oracle.
48
+ requireOwnerAuthority(authorization, workspaceId, permissions);
49
+ if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
50
+ throw new SessionTenancyNotActivatedError();
51
+ }
52
+ }
53
+
54
+ function requireOwnerAuthority(
55
+ authorization: AccessGrantAuthorization,
56
+ workspaceId: string,
57
+ permissions: readonly Permission[],
58
+ ): void {
59
+ if (!authorization.canonicalLocalHumanSession) {
60
+ requireVerifiedOwningUser(authorization, workspaceId);
61
+ } else if (
62
+ !authorization.contextIntegrity ||
63
+ authorization.authenticatedSubjectId !== authorization.grant.subjectId ||
64
+ authorization.grant.workspaceId !== workspaceId
65
+ ) {
66
+ throw new SessionTenancyManagedHumanRequiredError();
67
+ }
68
+ for (const permission of permissions) requirePermission(authorization.grant, permission);
69
+ }
70
+
71
+ export async function listManagedHumanUserResourceAuthorities(
72
+ deps: Pick<UserResourceGrantDependencies, "db">,
73
+ authorization: AccessGrantAuthorization,
74
+ workspaceId: string,
75
+ input: { resourceKind: UserResourceKind; cursor?: string | undefined; limit: number },
76
+ ) {
77
+ requireOwnerAuthority(authorization, workspaceId, LIST_PERMISSIONS[input.resourceKind]);
78
+ // Before activation there cannot be usable personal-resource authority in
79
+ // this organization. Discovery therefore has an exact empty answer; only
80
+ // issue/revoke and runtime use remain behind the forward-only product gate.
81
+ // This keeps ordinary workspace-owned Rig/Variable Set selection independent
82
+ // from an optional personal-resource product that is not enabled.
83
+ if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
84
+ return { authorities: [], nextCursor: null };
85
+ }
86
+ return await listSelfUserResourceAuthorities(deps.db, {
87
+ accountId: authorization.grant.accountId,
88
+ workspaceId,
89
+ subjectId: authorization.grant.subjectId,
90
+ ...input,
91
+ });
92
+ }
93
+
94
+ export async function issueManagedHumanUserResourceGrant(
95
+ deps: UserResourceGrantDependencies,
96
+ authorization: AccessGrantAuthorization,
97
+ workspaceId: string,
98
+ authorityId: string,
99
+ request: IssueUserResourceGrantRequest,
100
+ authorizationSurface: SessionAuthorizationSurface = "core",
101
+ ) {
102
+ const modePermissions: Permission[] =
103
+ request.mode === "session" ? ["sessions:control"] : ["sessions:create"];
104
+ if (authorization.canonicalLocalHumanSession) {
105
+ throw new SessionTenancyManagedHumanRequiredError();
106
+ }
107
+ await requireOwnerProductGate(deps, authorization, workspaceId, [
108
+ ...ISSUE_PERMISSIONS[request.resourceKind],
109
+ ...modePermissions,
110
+ ]);
111
+
112
+ if (request.mode === "session") {
113
+ // Contract refinement guarantees both fields. Keep the defensive check so
114
+ // direct core callers cannot accidentally weaken the target fence.
115
+ if (!request.sessionId || !request.expectedAuthorityEpoch) {
116
+ throw new TypeError("session grant requires its exact authority epoch");
117
+ }
118
+ await requireSessionAuthorization(deps, authorization.grant, {
119
+ sessionId: request.sessionId,
120
+ operation: "session.personal_resource.grant",
121
+ surface: authorizationSurface,
122
+ });
123
+ }
124
+
125
+ return await issueSelfUserResourceGrant(deps.db, {
126
+ accountId: authorization.grant.accountId,
127
+ workspaceId,
128
+ subjectId: authorization.grant.subjectId,
129
+ authorityId,
130
+ resourceKind: request.resourceKind,
131
+ mode: request.mode,
132
+ context: request.context,
133
+ sessionId: request.sessionId ?? null,
134
+ expectedAuthorityEpoch: request.expectedAuthorityEpoch ?? null,
135
+ workspaceSharedAcknowledged: request.workspaceSharedAcknowledged,
136
+ });
137
+ }
138
+
139
+ export async function revokeManagedHumanUserResourceGrant(
140
+ deps: Pick<UserResourceGrantDependencies, "db">,
141
+ authorization: AccessGrantAuthorization,
142
+ workspaceId: string,
143
+ grantId: string,
144
+ ) {
145
+ // Revocation narrows authority. It intentionally does not require the
146
+ // resource-specific permission that may have been removed since issuance.
147
+ await requireOwnerProductGate(deps, authorization, workspaceId, []);
148
+ return await revokeSelfUserResourceGrant(deps.db, {
149
+ accountId: authorization.grant.accountId,
150
+ workspaceId,
151
+ subjectId: authorization.grant.subjectId,
152
+ grantId,
153
+ });
154
+ }
@@ -1,4 +1,8 @@
1
- import { configuredStaticUsageLimits } from "@opengeni/config";
1
+ import {
2
+ configuredStaticUsageLimits,
3
+ resolveModelProviderForTurn,
4
+ type Settings,
5
+ } from "@opengeni/config";
2
6
  import type {
3
7
  LimitAction,
4
8
  LimitDecision,
@@ -7,7 +11,9 @@ import type {
7
11
  TurnInitiatorContext,
8
12
  } from "@opengeni/contracts";
9
13
  import {
14
+ checkWorkspaceAllowance,
10
15
  countActiveApiKeysForWorkspace,
16
+ countActiveOrganizationApiKeysForAccount,
11
17
  countScheduledTasksForWorkspace,
12
18
  countWorkspacesForAccount,
13
19
  getBillingBalance,
@@ -20,28 +26,74 @@ import type { ApiRouteDeps } from "../dependencies";
20
26
 
21
27
  export type LimitDependencies = Pick<ApiRouteDeps, "db" | "settings">;
22
28
 
29
+ /** Deterministic/local embeddings never debit credits, even in paid mode. */
30
+ export function paidDocumentEmbedding(settings: Settings): boolean {
31
+ return (
32
+ settings.documentEmbeddingBillingMode === "credits" &&
33
+ settings.documentEmbeddingProvider === "openai"
34
+ );
35
+ }
36
+
37
+ /** Round up each provider request so a positive input cannot become a free debit. */
38
+ export function documentEmbeddingCostMicros(settings: Settings, inputBytes: number): number {
39
+ if (!Number.isSafeInteger(inputBytes) || inputBytes < 0)
40
+ throw new Error("invalid embedding bytes");
41
+ const amount =
42
+ (BigInt(inputBytes) * BigInt(settings.documentEmbeddingRateMicrosPerMillionBytes) + 999_999n) /
43
+ 1_000_000n;
44
+ if (amount > BigInt(Number.MAX_SAFE_INTEGER))
45
+ throw new Error("embedding cost exceeds safe integer");
46
+ return Number(amount);
47
+ }
48
+
23
49
  export type LimitCheckInput = {
24
50
  accountId: string;
25
51
  workspaceId?: string;
52
+ /** Frozen causal human, never the API key or service admitting the work. */
53
+ initiatingHumanSubjectId?: string | null;
26
54
  action: LimitAction;
27
55
  quantity?: number;
28
- // The turn's model id, when the action represents an agent turn. When this is a
29
- // Codex-billed turn (codex/<slug> + feature enabled + active workspace
30
- // credential) the turn is paid by the user's ChatGPT/Codex plan and consumes
31
- // ZERO OpenGeni credits, so the credit-balance + model-cost + token gates are
32
- // skipped. Non-model infra actions (workspace/api_key/schedule create) leave
33
- // this undefined and are unaffected.
56
+ // The turn's model id, when the action represents an agent turn. The model's
57
+ // deployment cost controls credit/cost gates; upstream metering independently
58
+ // controls the token cap. Connected subscriptions still require live workspace
59
+ // readiness. Non-model infra actions leave this undefined.
34
60
  model?: string | null;
35
61
  };
36
62
 
63
+ export function modelFundingForAdmission(
64
+ settings: Settings,
65
+ model: string | null | undefined,
66
+ codexBilled: boolean,
67
+ ): { fundedWithoutCredits: boolean; countsTowardTokenCap: boolean } {
68
+ const resolvedModel = model ? resolveModelProviderForTurn(settings, model)?.model : null;
69
+ const codexSubscriptionModel =
70
+ resolvedModel?.credentialSource.kind === "connected_subscription" &&
71
+ resolvedModel.credentialSource.provider === "codex";
72
+ return {
73
+ // A Codex namespace/definition never bypasses credits by itself: the live
74
+ // workspace credential predicate above remains authoritative. SuperGrok's
75
+ // static overlay is likewise secret-free; its worker/provider admission
76
+ // owns live account selection before any upstream request can occur.
77
+ fundedWithoutCredits:
78
+ codexBilled ||
79
+ (resolvedModel != null && !codexSubscriptionModel && resolvedModel.cost !== "credits"),
80
+ countsTowardTokenCap:
81
+ !codexBilled && resolvedModel != null && resolvedModel.billing.upstreamPayer === "deployment",
82
+ };
83
+ }
84
+
37
85
  export async function requireLimit(deps: LimitDependencies, input: LimitCheckInput): Promise<void> {
38
86
  const decision = await checkLimit(deps, input);
39
87
  if (decision.allowed) {
40
88
  return;
41
89
  }
42
- throw new HTTPException(decision.code === "insufficient_credits" ? 402 : 429, {
43
- message: decision.message,
44
- });
90
+ throw new HTTPException(
91
+ decision.code === "insufficient_credits" || decision.code === "allowance_exhausted" ? 402 : 429,
92
+ {
93
+ message: decision.message,
94
+ cause: decision,
95
+ },
96
+ );
45
97
  }
46
98
 
47
99
  export async function checkLimit(
@@ -59,23 +111,39 @@ export async function checkLimit(
59
111
  model: input.model,
60
112
  })
61
113
  : false;
62
- const creditDecision = await checkCreditBalance(deps, input, codexBilled);
114
+ const { fundedWithoutCredits, countsTowardTokenCap } = modelFundingForAdmission(
115
+ deps.settings,
116
+ input.model,
117
+ codexBilled,
118
+ );
119
+ const creditDecision = await checkCreditBalance(deps, input, fundedWithoutCredits);
63
120
  if (!creditDecision.allowed) {
64
121
  return creditDecision;
65
122
  }
123
+ if (!fundedWithoutCredits && isCostlyAction(input.action) && input.workspaceId) {
124
+ const refusal = await checkWorkspaceAllowance(deps.db, {
125
+ accountId: input.accountId,
126
+ workspaceId: input.workspaceId,
127
+ subjectId: input.initiatingHumanSubjectId ?? null,
128
+ });
129
+ if (refusal) return { allowed: false, ...refusal };
130
+ }
66
131
  if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
67
132
  return { allowed: true };
68
133
  }
69
- return await checkStaticCaps(deps, input, codexBilled);
134
+ return await checkStaticCaps(deps, input, {
135
+ fundedWithoutCredits,
136
+ countsTowardTokenCap,
137
+ });
70
138
  }
71
139
 
72
140
  async function checkCreditBalance(
73
141
  deps: LimitDependencies,
74
142
  input: LimitCheckInput,
75
- codexBilled: boolean,
143
+ externallyBilled: boolean,
76
144
  ): Promise<LimitDecision> {
77
- if (codexBilled) {
78
- return { allowed: true }; // paid by the user's ChatGPT/Codex plan — zero OpenGeni credits
145
+ if (externallyBilled) {
146
+ return { allowed: true }; // paid outside OpenGeni — zero OpenGeni credits
79
147
  }
80
148
  if (!usesCreditLimits(deps) || !isCostlyAction(input.action)) {
81
149
  return { allowed: true };
@@ -84,16 +152,20 @@ async function checkCreditBalance(
84
152
  if (balance.balanceMicros > 0) {
85
153
  return { allowed: true };
86
154
  }
87
- return { allowed: false, code: "insufficient_credits", message: "insufficient OpenGeni credits" };
155
+ return { allowed: false, code: "insufficient_credits", message: "insufficient Opengeni credits" };
88
156
  }
89
157
 
90
158
  async function checkStaticCaps(
91
159
  deps: LimitDependencies,
92
160
  input: LimitCheckInput,
93
- codexBilled: boolean,
161
+ funding: { fundedWithoutCredits: boolean; countsTowardTokenCap: boolean },
94
162
  ): Promise<LimitDecision> {
95
163
  const limits = configuredStaticUsageLimits(deps.settings);
96
- if (limits.maxMonthlyCostMicrosPerAccount && isCostlyAction(input.action) && !codexBilled) {
164
+ if (
165
+ limits.maxMonthlyCostMicrosPerAccount &&
166
+ isCostlyAction(input.action) &&
167
+ !funding.fundedWithoutCredits
168
+ ) {
97
169
  const used = await sumUsageQuantity(deps.db, {
98
170
  accountId: input.accountId,
99
171
  eventType: "model.cost",
@@ -120,10 +192,12 @@ async function checkStaticCaps(
120
192
  );
121
193
  }
122
194
  case "api_key:create": {
123
- if (!limits.maxApiKeysPerWorkspace || !input.workspaceId) {
195
+ if (!limits.maxApiKeysPerWorkspace) {
124
196
  return { allowed: true };
125
197
  }
126
- const count = await countActiveApiKeysForWorkspace(deps.db, input.workspaceId);
198
+ const count = input.workspaceId
199
+ ? await countActiveApiKeysForWorkspace(deps.db, input.workspaceId)
200
+ : await countActiveOrganizationApiKeysForAccount(deps.db, input.accountId);
127
201
  return count < limits.maxApiKeysPerWorkspace
128
202
  ? { allowed: true }
129
203
  : blocked(
@@ -172,7 +246,11 @@ async function checkStaticCaps(
172
246
  );
173
247
  }
174
248
  case "tokens:consume": {
175
- if (codexBilled || !limits.maxMonthlyTokensPerWorkspace || !input.workspaceId) {
249
+ if (
250
+ !funding.countsTowardTokenCap ||
251
+ !limits.maxMonthlyTokensPerWorkspace ||
252
+ !input.workspaceId
253
+ ) {
176
254
  return { allowed: true };
177
255
  }
178
256
  const used = await sumUsageQuantity(deps.db, {
@@ -253,9 +331,10 @@ function usesCreditLimits(deps: LimitDependencies): boolean {
253
331
  }
254
332
 
255
333
  function isCostlyAction(action: LimitAction): boolean {
256
- return (
257
- action === "agent_run:create" || action === "tokens:consume" || action === "document:index"
258
- );
334
+ // Document admission retains the monthly chunk quota above, but parsing and
335
+ // source retention do not consume embedding credits. The worker admits paid
336
+ // embedding separately and settles its actual provider input after use.
337
+ return action === "agent_run:create" || action === "tokens:consume";
259
338
  }
260
339
 
261
340
  function blocked(code: string, message: string): LimitDecision {
@@ -3,6 +3,13 @@ import type { Context } from "hono";
3
3
  import { HTTPException } from "hono/http-exception";
4
4
  import type { ManagedAuth } from "./managed-auth-type";
5
5
  import { getManagedSession } from "./managed-session";
6
+ export {
7
+ getManagedAuthRequestActorAdmissionStamp,
8
+ getManagedAuthRequestActorLeaseStamp,
9
+ markManagedAuthRequestActorTransitionApplied,
10
+ } from "./managed-session";
11
+ import type { ManagedAuthSessionAdapter } from "./managed-auth-session-sets";
12
+ import type { ManagedAuthSessionSetMode } from "@opengeni/contracts/managed-auth-session-sets";
6
13
 
7
14
  export type CanonicalHumanRequestIdentity = {
8
15
  authUserId: string;
@@ -11,17 +18,29 @@ export type CanonicalHumanRequestIdentity = {
11
18
 
12
19
  export async function requireCanonicalHumanRequestIdentity(
13
20
  context: Context,
14
- input: { db: Database; managedAuth?: ManagedAuth | null; allowRecovery?: boolean },
21
+ input: {
22
+ db: Database;
23
+ managedAuth?: ManagedAuth | null | undefined;
24
+ managedAuthSessionAdapter?: ManagedAuthSessionAdapter | null | undefined;
25
+ managedAuthSessionSetMode?: ManagedAuthSessionSetMode | undefined;
26
+ allowRecovery?: boolean | undefined;
27
+ },
15
28
  ): Promise<CanonicalHumanRequestIdentity> {
16
29
  if (!input.managedAuth) {
17
- throw new HTTPException(404, { message: "Canonical human identity is unavailable" });
30
+ throw new HTTPException(404, {
31
+ message: "Canonical human identity is unavailable",
32
+ });
18
33
  }
19
34
  const session = await getManagedSession(context, input.managedAuth, {
20
35
  db: input.db,
36
+ sessionAdapter: input.managedAuthSessionAdapter,
37
+ sessionSetMode: input.managedAuthSessionSetMode,
21
38
  ...(input.allowRecovery === undefined ? {} : { allowIdentityRecovery: input.allowRecovery }),
22
39
  });
23
40
  if (!session?.user || typeof session.session?.id !== "string") {
24
- throw new HTTPException(401, { message: "Managed human authentication required" });
41
+ throw new HTTPException(401, {
42
+ message: "Managed human authentication required",
43
+ });
25
44
  }
26
45
  return { authUserId: session.user.id, authSessionId: session.session.id };
27
46
  }
@@ -0,0 +1,136 @@
1
+ import { CODEX_CLIENT_VERSION, fetchCodexModels } from "@opengeni/codex";
2
+ import { configuredModels, withCodexCatalogProvider, type Settings } from "@opengeni/config";
3
+ import {
4
+ buildCodexTokenResolver,
5
+ connectionModelAllowed,
6
+ getCodexRotationSettings,
7
+ listCodexAccountStatuses,
8
+ loadCodexCredentialForRun,
9
+ type Database,
10
+ } from "@opengeni/db";
11
+ import type { ModelAvailabilityObservation } from "./model-catalog";
12
+
13
+ const CATALOG_CACHE_MS = 60_000;
14
+ const CATALOG_ERROR_CACHE_MS = 5_000;
15
+ type CatalogCacheEntry = {
16
+ expiresAt: number;
17
+ result: Promise<{ ok: boolean; slugs: string[]; checkedAt: string }>;
18
+ };
19
+ const catalogs = new Map<string, CatalogCacheEntry>();
20
+
21
+ type Dependencies = {
22
+ listAccounts: typeof listCodexAccountStatuses;
23
+ getRotation: typeof getCodexRotationSettings;
24
+ loadCredential: typeof loadCodexCredentialForRun;
25
+ fetchModels: typeof fetchCodexModels;
26
+ getToken: (
27
+ db: Database,
28
+ settings: Settings,
29
+ workspaceId: string,
30
+ credentialId: string,
31
+ ) => ReturnType<ReturnType<typeof buildCodexTokenResolver>["getToken"]>;
32
+ };
33
+
34
+ /** Live provider support is separate from deployment membership and model permissions. */
35
+ export async function loadWorkspaceCodexModelAvailability(
36
+ db: Database,
37
+ settings: Settings,
38
+ workspaceId: string,
39
+ deps: Dependencies = {
40
+ listAccounts: listCodexAccountStatuses,
41
+ getRotation: getCodexRotationSettings,
42
+ loadCredential: loadCodexCredentialForRun,
43
+ fetchModels: fetchCodexModels,
44
+ getToken: (targetDb, targetSettings, targetWorkspaceId, credentialId) =>
45
+ buildCodexTokenResolver(targetDb, targetSettings, targetWorkspaceId, credentialId).getToken(),
46
+ },
47
+ ): Promise<Record<string, ModelAvailabilityObservation>> {
48
+ if (!settings.codexSubscriptionEnabled) return {};
49
+ const [accounts, rotation] = await Promise.all([
50
+ deps.listAccounts(db, workspaceId),
51
+ deps.getRotation(db, workspaceId),
52
+ ]);
53
+ const candidates = accounts.filter(
54
+ (account) =>
55
+ account.status === "active" &&
56
+ account.allocatorEnabled &&
57
+ (rotation?.rotationEnabled || account.isActive),
58
+ );
59
+ const live = await Promise.all(
60
+ candidates.map(async (account) => {
61
+ const unavailable = () => ({
62
+ account,
63
+ ok: false,
64
+ slugs: [] as string[],
65
+ checkedAt: new Date().toISOString(),
66
+ });
67
+ try {
68
+ // Recheck current workspace/source authority before consulting the cache.
69
+ const credential = await deps.loadCredential(db, settings, workspaceId, account.id);
70
+ if (!credential || credential.status !== "active") return unavailable();
71
+ let key = `${workspaceId}:${credential.id}:${credential.version}`;
72
+ let cached = catalogs.get(key);
73
+ if (!cached || cached.expiresAt <= Date.now()) {
74
+ const token = await deps.getToken(db, settings, workspaceId, credential.id);
75
+ key = `${workspaceId}:${credential.id}:${token.credentialVersion}`;
76
+ cached = catalogs.get(key);
77
+ if (cached && cached.expiresAt > Date.now()) {
78
+ return { account, ...(await cached.result) };
79
+ }
80
+ for (const [id, entry] of catalogs) {
81
+ if (entry.expiresAt <= Date.now()) catalogs.delete(id);
82
+ }
83
+ if (catalogs.size >= 1_024) catalogs.delete(catalogs.keys().next().value!);
84
+ const entry: CatalogCacheEntry = {
85
+ expiresAt: Date.now() + CATALOG_CACHE_MS,
86
+ result: deps
87
+ .fetchModels({
88
+ accessToken: token.accessToken,
89
+ chatgptAccountId: token.chatgptAccountId,
90
+ isFedramp: token.isFedramp,
91
+ clientVersion: CODEX_CLIENT_VERSION,
92
+ })
93
+ .catch(() => ({ ok: false, slugs: [] as string[] }))
94
+ .then((result) => {
95
+ entry.expiresAt =
96
+ Date.now() + (result.ok ? CATALOG_CACHE_MS : CATALOG_ERROR_CACHE_MS);
97
+ return { ok: result.ok, slugs: result.slugs, checkedAt: new Date().toISOString() };
98
+ }),
99
+ };
100
+ catalogs.set(key, entry);
101
+ cached = entry;
102
+ }
103
+ return { account, ...(await cached.result) };
104
+ } catch {
105
+ return unavailable();
106
+ }
107
+ }),
108
+ );
109
+ return Object.fromEntries(
110
+ configuredModels(withCodexCatalogProvider(settings))
111
+ .filter(
112
+ (model) =>
113
+ model.credentialSource.kind === "connected_subscription" &&
114
+ model.credentialSource.provider === "codex",
115
+ )
116
+ .map((model) => {
117
+ // Support and permission must hold on the SAME serving account.
118
+ const permitted = live.filter(({ account }) =>
119
+ connectionModelAllowed(account.allowedModelIds, model.id),
120
+ );
121
+ // The allocator may choose any permitted candidate, so all must support it.
122
+ const supported =
123
+ permitted.length > 0 &&
124
+ permitted.every(({ ok, slugs }) => ok && slugs.includes(model.upstreamModelId));
125
+ const uncertain = permitted.find(({ ok }) => !ok);
126
+ return [
127
+ model.definitionVersion,
128
+ {
129
+ status: supported ? "available" : "unavailable",
130
+ reason: supported ? null : uncertain ? "provider_unhealthy" : "not_entitled",
131
+ checkedAt: (uncertain ?? permitted[0])?.checkedAt ?? new Date().toISOString(),
132
+ } satisfies ModelAvailabilityObservation,
133
+ ];
134
+ }),
135
+ );
136
+ }