@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
@@ -0,0 +1,95 @@
1
+ import type { Observability } from "@opengeni/observability";
2
+
3
+ /**
4
+ * Content-free diagnostics for per-task usage limits. Every label is
5
+ * a closed set: where the decision was made and what it was. No workspace,
6
+ * session, subject, model, prompt or amount ever becomes a label, so the
7
+ * series count is bounded by these two lists.
8
+ */
9
+ export type SessionUsageLimitMetricsSink = Pick<Observability, "incrementCounter">;
10
+
11
+ export const SESSION_USAGE_LIMIT_METRIC_BOUNDARIES = [
12
+ "create",
13
+ "child_create",
14
+ "send",
15
+ "steer",
16
+ "goal",
17
+ "claim",
18
+ "between_calls",
19
+ "single_call",
20
+ "settlement",
21
+ "write",
22
+ "import",
23
+ ] as const;
24
+ export type SessionUsageLimitMetricBoundary =
25
+ (typeof SESSION_USAGE_LIMIT_METRIC_BOUNDARIES)[number];
26
+
27
+ export const SESSION_USAGE_LIMIT_METRIC_OUTCOMES = [
28
+ /** Admission refused: observed settled usage reached the root limit. */
29
+ "reached",
30
+ /** The usage state could not be read or written; the request was blocked. */
31
+ "unavailable",
32
+ /** Settlement could not be recorded; the next dispatch waits for recovery. */
33
+ "accounting_failure",
34
+ /** A response had no known price: excluded from the total and disclosed. */
35
+ "unpriced",
36
+ /** A priced response was recorded once. */
37
+ "recorded",
38
+ /** An exact replay of an already recorded source. */
39
+ "duplicate",
40
+ /** A previously unpriced source gained its stored price. */
41
+ "filled",
42
+ /** A different amount was offered for an already recorded source. */
43
+ "source_conflict",
44
+ /** The session was deleted before its in-flight response settled. */
45
+ "source_gone",
46
+ /** A write lost its compare-and-set race. */
47
+ "cas_conflict",
48
+ /** A write targeted a root whose legacy history is still preparing. */
49
+ "preparing",
50
+ /** A write was refused for an agent, scoped bearer or other non-person actor. */
51
+ "forbidden",
52
+ /** A new non-null limit was refused because producers are disabled. */
53
+ "disabled",
54
+ /** A write was refused because the session is not the task root. */
55
+ "not_root",
56
+ /** An authorized write changed the limit. */
57
+ "set",
58
+ /** An authorized write removed the limit. */
59
+ "cleared",
60
+ /** An authorized write repeated an already applied request. */
61
+ "replayed",
62
+ /** Legacy import pages processed by maintenance. */
63
+ "imported",
64
+ /** Legacy import maintenance failed and will be retried. */
65
+ "import_failed",
66
+ /** A settled receipt could not be written yet and is held for a DB-only retry. */
67
+ "held",
68
+ /** An unfilled single-call custody marker passed its lost window; disclosed as unpriced. */
69
+ "lost_resolved",
70
+ ] as const;
71
+ export type SessionUsageLimitMetricOutcome = (typeof SESSION_USAGE_LIMIT_METRIC_OUTCOMES)[number];
72
+
73
+ export const SESSION_USAGE_LIMIT_EVENTS_METRIC = {
74
+ name: "opengeni_session_usage_limit_events_total",
75
+ help: "Per-task usage limit decisions by boundary and outcome; labels are closed sets.",
76
+ } as const;
77
+
78
+ /** Count one usage-limit decision. Never throws: telemetry cannot change the outcome. */
79
+ export function recordSessionUsageLimitEvent(
80
+ sink: SessionUsageLimitMetricsSink | null | undefined,
81
+ boundary: SessionUsageLimitMetricBoundary,
82
+ outcome: SessionUsageLimitMetricOutcome,
83
+ amount = 1,
84
+ ): void {
85
+ if (!sink || amount <= 0) return;
86
+ try {
87
+ sink.incrementCounter({
88
+ ...SESSION_USAGE_LIMIT_EVENTS_METRIC,
89
+ labels: { boundary, outcome },
90
+ amount,
91
+ });
92
+ } catch {
93
+ // Telemetry must never change an admission or settlement outcome.
94
+ }
95
+ }
@@ -0,0 +1,415 @@
1
+ import { HTTPException } from "hono/http-exception";
2
+ import {
3
+ checkSessionUsageLimitAdmission,
4
+ readSessionUsageLimit,
5
+ SessionUsageLimitForbiddenError,
6
+ SessionUsageLimitUnavailableError,
7
+ setInitialSessionUsageLimitInTransaction,
8
+ setSessionUsageLimitInTransaction,
9
+ withWorkspaceSessionActivityRls,
10
+ withWorkspaceSubjectSessionActivityRls,
11
+ type Database,
12
+ type SessionActivityDatabase,
13
+ type SessionUsageLimitSetResult,
14
+ type SessionUsageLimitWriteActor,
15
+ } from "@opengeni/db";
16
+ import {
17
+ SESSION_USAGE_LIMIT_BASIS,
18
+ type Session,
19
+ type SessionUsageLimit,
20
+ type SessionUsageLimitReachedRefusal,
21
+ type SetSessionUsageLimitRequest,
22
+ } from "@opengeni/contracts";
23
+ import {
24
+ isVerifiedDelegatedHumanAuthorization,
25
+ requirePermission,
26
+ verifiedDelegatedHumanAuthorizationForRequest,
27
+ type AccessGrantAuthorization,
28
+ } from "./access";
29
+ import {
30
+ requireSessionAuthorization,
31
+ type SessionAuthorizationDependencies,
32
+ } from "./session-authorization";
33
+ import {
34
+ recordSessionUsageLimitEvent,
35
+ type SessionUsageLimitMetricBoundary,
36
+ type SessionUsageLimitMetricOutcome,
37
+ type SessionUsageLimitMetricsSink,
38
+ } from "./session-usage-limit-metrics";
39
+
40
+ /**
41
+ * Producer flag for per-task usage limits. Off by default. When off, new
42
+ * non-null limits are rejected (`session_usage_limit_disabled`), but limits
43
+ * already persisted keep being enforced and clearing stays allowed, so turning
44
+ * the flag off is never a way to lift a limit or strand one.
45
+ */
46
+ export function sessionUsageLimitsProducerEnabled(settings: {
47
+ sessionUsageLimitsEnabled?: boolean | undefined;
48
+ }): boolean {
49
+ return settings.sessionUsageLimitsEnabled === true;
50
+ }
51
+
52
+ /** Typed route error with a stable `details.code`. */
53
+ export class SessionUsageLimitHttpError extends HTTPException {
54
+ constructor(
55
+ status: 403 | 404 | 409 | 422 | 503,
56
+ readonly code:
57
+ | "session_usage_limit_conflict"
58
+ | "session_usage_limit_not_root"
59
+ | "session_usage_limit_preparing"
60
+ | "session_usage_limit_disabled"
61
+ | "session_usage_limit_forbidden"
62
+ | "session_usage_limit_unavailable"
63
+ | "session_not_found",
64
+ message: string,
65
+ readonly details: Record<string, unknown> = {},
66
+ ) {
67
+ super(status, { message });
68
+ this.name = "SessionUsageLimitHttpError";
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Who may change a task's usage limit: an authenticated person acting
74
+ * directly, or the trusted host's server key. Never an agent attempt, a
75
+ * delegated-human or admin-access agent (an OAuth/MCP dispatch acting as a
76
+ * person, or a personal API key), a service-delegated token or a session-scoped
77
+ * bearer. `principalKind` alone is not proof of a present person.
78
+ */
79
+ export function sessionUsageLimitWriteActorForAccess(
80
+ access: AccessGrantAuthorization,
81
+ request?: Request,
82
+ ): SessionUsageLimitWriteActor {
83
+ const { grant } = access;
84
+ const forbidden = () =>
85
+ new SessionUsageLimitHttpError(
86
+ 403,
87
+ "session_usage_limit_forbidden",
88
+ "Only a person or the application's server can change a task usage limit.",
89
+ );
90
+ if (
91
+ (request && verifiedDelegatedHumanAuthorizationForRequest(request) !== null) ||
92
+ isVerifiedDelegatedHumanAuthorization(access) ||
93
+ grant.metadata?.attemptId !== undefined ||
94
+ grant.metadata?.turnId !== undefined ||
95
+ grant.metadata?.sessionId !== undefined
96
+ ) {
97
+ throw forbidden();
98
+ }
99
+ if (
100
+ grant.principalKind === "human_session" &&
101
+ access.contextIntegrity &&
102
+ access.authenticatedSubjectId === grant.subjectId &&
103
+ !grant.serviceInitiator &&
104
+ !grant.serviceInitiatorContext
105
+ ) {
106
+ return { subjectId: grant.subjectId, type: "human" };
107
+ }
108
+ if (
109
+ (grant.principalKind === "api_key" || grant.principalKind === "configured_key") &&
110
+ access.contextIntegrity
111
+ ) {
112
+ return { subjectId: grant.subjectId, type: "host" };
113
+ }
114
+ throw forbidden();
115
+ }
116
+
117
+ /** Projection for a session whose usage state cannot be read right now. Never $0 or unlimited. */
118
+ export function unavailableSessionUsageLimit(
119
+ session: Pick<Session, "id" | "rootSessionId">,
120
+ ): SessionUsageLimit {
121
+ return {
122
+ basis: SESSION_USAGE_LIMIT_BASIS,
123
+ rootSessionId: session.rootSessionId ?? session.id,
124
+ inherited: (session.rootSessionId ?? session.id) !== session.id,
125
+ status: "unavailable",
126
+ limitMicros: null,
127
+ usedMicros: null,
128
+ version: 0,
129
+ usageRevision: 0,
130
+ history: "preparing",
131
+ coverage: "partial",
132
+ pricedCalls: 0,
133
+ freeCalls: 0,
134
+ unpricedCalls: 0,
135
+ updatedAt: null,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Read a session's usage projection for an already-authorized session read.
141
+ * A read failure yields the `unavailable` projection rather than failing the
142
+ * whole session read. `prepare` advances a pending historical import inline.
143
+ */
144
+ export async function sessionUsageLimitForRead(
145
+ db: Database,
146
+ session: Pick<Session, "id" | "rootSessionId" | "accountId" | "workspaceId">,
147
+ options: { prepare?: boolean; subjectId?: string | null } = {},
148
+ ): Promise<SessionUsageLimit | undefined> {
149
+ try {
150
+ const read = await readSessionUsageLimit(db, {
151
+ accountId: session.accountId,
152
+ workspaceId: session.workspaceId,
153
+ sessionId: session.id,
154
+ ...(options.subjectId ? { subjectId: options.subjectId } : {}),
155
+ ...(options.prepare ? { prepare: true } : {}),
156
+ });
157
+ return read.outcome === "ok" ? read.state : undefined;
158
+ } catch (error) {
159
+ if (error instanceof SessionUsageLimitUnavailableError) {
160
+ return unavailableSessionUsageLimit(session);
161
+ }
162
+ throw error;
163
+ }
164
+ }
165
+
166
+ /** GET usage-limit: authorize a session read, then read (advancing a pending import). */
167
+ export async function getSessionUsageLimitForAccess(
168
+ deps: SessionAuthorizationDependencies,
169
+ access: AccessGrantAuthorization,
170
+ sessionId: string,
171
+ ): Promise<SessionUsageLimit> {
172
+ const { grant } = access;
173
+ requirePermission(grant, "sessions:read");
174
+ await requireSessionAuthorization(deps, grant, {
175
+ sessionId,
176
+ operation: "session.read",
177
+ surface: "core",
178
+ });
179
+ let read: Awaited<ReturnType<typeof readSessionUsageLimit>>;
180
+ try {
181
+ read = await readSessionUsageLimit(deps.db, {
182
+ accountId: grant.accountId,
183
+ workspaceId: grant.workspaceId,
184
+ sessionId,
185
+ subjectId: grant.subjectId,
186
+ prepare: true,
187
+ });
188
+ } catch (error) {
189
+ if (error instanceof SessionUsageLimitUnavailableError) {
190
+ throw new SessionUsageLimitHttpError(
191
+ 503,
192
+ "session_usage_limit_unavailable",
193
+ "Usage for this task is temporarily unavailable. Try again shortly.",
194
+ );
195
+ }
196
+ throw error;
197
+ }
198
+ if (read.outcome !== "ok") {
199
+ throw new SessionUsageLimitHttpError(404, "session_not_found", "session not found");
200
+ }
201
+ return read.state;
202
+ }
203
+
204
+ function setResultToHttp(result: SessionUsageLimitSetResult): SessionUsageLimit {
205
+ switch (result.outcome) {
206
+ case "ok":
207
+ case "replayed":
208
+ return result.state!;
209
+ case "not_found":
210
+ throw new SessionUsageLimitHttpError(404, "session_not_found", "session not found");
211
+ case "not_root":
212
+ throw new SessionUsageLimitHttpError(
213
+ 409,
214
+ "session_usage_limit_not_root",
215
+ "This chat shares its parent task's usage limit. Change it on the task's root chat.",
216
+ result.rootSessionId ? { rootSessionId: result.rootSessionId } : {},
217
+ );
218
+ case "conflict":
219
+ throw new SessionUsageLimitHttpError(
220
+ 409,
221
+ "session_usage_limit_conflict",
222
+ "The usage limit changed since it was read. Review the current limit and try again.",
223
+ result.state ? { currentVersion: result.state.version } : {},
224
+ );
225
+ case "operation_conflict":
226
+ throw new SessionUsageLimitHttpError(
227
+ 409,
228
+ "session_usage_limit_conflict",
229
+ "This operation id was already used for a different change.",
230
+ );
231
+ case "preparing":
232
+ throw new SessionUsageLimitHttpError(
233
+ 409,
234
+ "session_usage_limit_preparing",
235
+ "Earlier usage for this task is still being counted. Try again shortly.",
236
+ );
237
+ case "disabled":
238
+ throw new SessionUsageLimitHttpError(
239
+ 422,
240
+ "session_usage_limit_disabled",
241
+ "Usage limits are not enabled on this server. A limit can only be removed.",
242
+ );
243
+ }
244
+ }
245
+
246
+ /**
247
+ * PUT usage-limit: a person or trusted host compare-and-sets a ROOT task's
248
+ * limit. Appends `session.usage_limit.updated` on change. Never wakes or
249
+ * resumes work: a paused goal stays paused until someone resumes it.
250
+ */
251
+ export async function setSessionUsageLimitForAccess(
252
+ deps: SessionAuthorizationDependencies & {
253
+ settings: { sessionUsageLimitsEnabled?: boolean | undefined };
254
+ observability?: SessionUsageLimitMetricsSink | undefined;
255
+ },
256
+ access: AccessGrantAuthorization,
257
+ sessionId: string,
258
+ request: SetSessionUsageLimitRequest,
259
+ rawRequest?: Request,
260
+ ): Promise<SessionUsageLimit> {
261
+ const { grant } = access;
262
+ requirePermission(grant, "sessions:control");
263
+ let actor: SessionUsageLimitWriteActor;
264
+ try {
265
+ actor = sessionUsageLimitWriteActorForAccess(access, rawRequest);
266
+ } catch (error) {
267
+ recordSessionUsageLimitEvent(deps.observability, "write", "forbidden");
268
+ throw error;
269
+ }
270
+ await requireSessionAuthorization(deps, grant, {
271
+ sessionId,
272
+ operation: "session.control",
273
+ surface: "core",
274
+ });
275
+ const write = async (tx: SessionActivityDatabase) =>
276
+ await setSessionUsageLimitInTransaction(tx, {
277
+ accountId: grant.accountId,
278
+ workspaceId: grant.workspaceId,
279
+ sessionId,
280
+ limitMicros: request.limitMicros,
281
+ expectedVersion: request.expectedVersion,
282
+ operationId: request.operationId,
283
+ actor,
284
+ producerEnabled: sessionUsageLimitsProducerEnabled(deps.settings),
285
+ });
286
+ let result: SessionUsageLimitSetResult;
287
+ try {
288
+ result =
289
+ actor.type === "human"
290
+ ? await withWorkspaceSubjectSessionActivityRls(
291
+ deps.db,
292
+ grant.workspaceId,
293
+ grant.subjectId,
294
+ write,
295
+ )
296
+ : await withWorkspaceSessionActivityRls(deps.db, grant.workspaceId, write);
297
+ } catch (error) {
298
+ if (error instanceof SessionUsageLimitForbiddenError) {
299
+ recordSessionUsageLimitEvent(deps.observability, "write", "forbidden");
300
+ throw new SessionUsageLimitHttpError(403, "session_usage_limit_forbidden", error.message);
301
+ }
302
+ if (error instanceof SessionUsageLimitUnavailableError) {
303
+ recordSessionUsageLimitEvent(deps.observability, "write", "unavailable");
304
+ }
305
+ throw error;
306
+ }
307
+ const metricOutcome = writeMetricOutcome(result, request.limitMicros);
308
+ if (metricOutcome) recordSessionUsageLimitEvent(deps.observability, "write", metricOutcome);
309
+ return setResultToHttp(result);
310
+ }
311
+
312
+ function writeMetricOutcome(
313
+ result: SessionUsageLimitSetResult,
314
+ limitMicros: number | null,
315
+ ): SessionUsageLimitMetricOutcome | null {
316
+ switch (result.outcome) {
317
+ case "ok":
318
+ return limitMicros === null ? "cleared" : "set";
319
+ case "replayed":
320
+ return "replayed";
321
+ case "conflict":
322
+ case "operation_conflict":
323
+ return "cas_conflict";
324
+ case "preparing":
325
+ return "preparing";
326
+ case "disabled":
327
+ return "disabled";
328
+ case "not_root":
329
+ return "not_root";
330
+ case "not_found":
331
+ return null;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Apply a create-time limit inside the session-creating transaction, before
337
+ * the initial turn can be claimed. Only a person or trusted host may set it
338
+ * and only on a new root; children always share their root's limit.
339
+ */
340
+ export async function applyInitialSessionUsageLimit(
341
+ tx: Database,
342
+ input: {
343
+ accountId: string;
344
+ workspaceId: string;
345
+ sessionId: string;
346
+ limitMicros: number;
347
+ actor: SessionUsageLimitWriteActor;
348
+ producerEnabled: boolean;
349
+ },
350
+ ): Promise<void> {
351
+ const result = await setInitialSessionUsageLimitInTransaction(tx, {
352
+ accountId: input.accountId,
353
+ workspaceId: input.workspaceId,
354
+ sessionId: input.sessionId,
355
+ limitMicros: input.limitMicros,
356
+ actor: input.actor,
357
+ producerEnabled: input.producerEnabled,
358
+ });
359
+ if (result.outcome !== "ok" && result.outcome !== "replayed") setResultToHttp(result);
360
+ }
361
+
362
+ /**
363
+ * Edge admission for per-task usage limits. Called after an exact
364
+ * committed replay has been returned and before any queue, interrupt, draft or
365
+ * turn side effect, so a refused Send/Steer leaves nothing behind and the
366
+ * composer keeps its draft.
367
+ *
368
+ * - Reached: HTTP 402 whose cause is the typed refusal (`session_usage_limit_reached`).
369
+ * - Unreadable state: HTTP 503 (`session_usage_limit_unavailable`), never unlimited.
370
+ */
371
+ export async function requireSessionUsageLimitAllows(
372
+ db: Database,
373
+ input: {
374
+ accountId: string;
375
+ workspaceId: string;
376
+ sessionId: string;
377
+ /** Server-internal admission for an authorized attempt: fail closed on a missing tree. */
378
+ runtime?: boolean;
379
+ /** Runtime only: open a durable custody marker for this single call when admitted. */
380
+ openSourceId?: string;
381
+ },
382
+ metrics?: {
383
+ sink: SessionUsageLimitMetricsSink | null | undefined;
384
+ boundary: SessionUsageLimitMetricBoundary;
385
+ },
386
+ ): Promise<void> {
387
+ let admission: Awaited<ReturnType<typeof checkSessionUsageLimitAdmission>>;
388
+ try {
389
+ admission = await checkSessionUsageLimitAdmission(db, input);
390
+ } catch (error) {
391
+ if (error instanceof SessionUsageLimitUnavailableError) {
392
+ if (metrics) recordSessionUsageLimitEvent(metrics.sink, metrics.boundary, "unavailable");
393
+ throw new HTTPException(503, { message: error.message, cause: error });
394
+ }
395
+ throw error;
396
+ }
397
+ if (!admission.allowed) {
398
+ if (metrics) recordSessionUsageLimitEvent(metrics.sink, metrics.boundary, "reached");
399
+ throw new HTTPException(402, {
400
+ message: admission.refusal.message,
401
+ cause: admission.refusal,
402
+ });
403
+ }
404
+ }
405
+
406
+ /** True when this error is the typed edge refusal raised above. */
407
+ export function sessionUsageLimitRefusalFromError(
408
+ error: unknown,
409
+ ): SessionUsageLimitReachedRefusal | null {
410
+ if (!(error instanceof HTTPException) || error.status !== 402) return null;
411
+ const cause = error.cause as Partial<SessionUsageLimitReachedRefusal> | undefined;
412
+ return cause && typeof cause === "object" && cause.code === "session_usage_limit_reached"
413
+ ? (cause as SessionUsageLimitReachedRefusal)
414
+ : null;
415
+ }