@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,226 @@
1
+ import type { AccessGrant } from "@opengeni/contracts";
2
+ import {
3
+ getSession,
4
+ getSiteDataAgentSession,
5
+ SITE_DATA_AGENT_LINEAGE_MAX_DEPTH,
6
+ type Database,
7
+ } from "@opengeni/db";
8
+ import { HTTPException } from "hono/http-exception";
9
+ import { siteSessionOriginFromMetadata } from "./site-session-origin";
10
+
11
+ /**
12
+ * Whether an agent acts for a Site page: its session, a spawning ancestor or
13
+ * its root session was started from a Site (a published Site or an inline or
14
+ * preview frame), or its current or a spawning ancestor turn arrived from one.
15
+ * The same evidence the Site Data agent clamp uses
16
+ * (apps/api/src/site-data/service.ts), plus frame origins without a
17
+ * published Site id.
18
+ */
19
+ export type SiteAgentClamp = {
20
+ clamped: boolean;
21
+ /** Published Sites in the lineage; empty for frames without a published Site. */
22
+ originSiteIds: string[];
23
+ };
24
+
25
+ function agentSessionIds(grant: AccessGrant): { sessionId: string; turnId: string | null } | null {
26
+ if (grant.principalKind !== "agent_attempt" && grant.metadata?.delegated !== true) return null;
27
+ const sessionId = grant.metadata?.sessionId;
28
+ if (typeof sessionId !== "string" || sessionId.length === 0) return null;
29
+ const turnId = grant.metadata?.turnId;
30
+ return { sessionId, turnId: typeof turnId === "string" && turnId.length > 0 ? turnId : null };
31
+ }
32
+
33
+ /** Null when the grant is not an agent session. */
34
+ export async function siteAgentClamp(
35
+ db: Database,
36
+ grant: AccessGrant,
37
+ ): Promise<SiteAgentClamp | null> {
38
+ const ids = agentSessionIds(grant);
39
+ if (!ids) return null;
40
+ return await siteSessionClamp(
41
+ db,
42
+ { accountId: grant.accountId, workspaceId: grant.workspaceId },
43
+ ids.sessionId,
44
+ ids.turnId,
45
+ );
46
+ }
47
+
48
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
49
+
50
+ /** How long an agent turn's clamp is reused (per process). */
51
+ const CLAMP_CACHE_MS = 60_000;
52
+ const CLAMP_CACHE_LIMIT = 5_000;
53
+ const clampCache = new Map<string, { clamp: SiteAgentClamp; at: number }>();
54
+
55
+ /**
56
+ * The Site clamp of an agent running in `sessionId` (and `turnId`, when
57
+ * known). A turn's clamp is computed once a minute at most per process: the
58
+ * evidence it reads (where the session, its ancestors and that turn came
59
+ * from) does not change during the turn. Without a turn id it is always
60
+ * read fresh.
61
+ */
62
+ export async function siteSessionClamp(
63
+ db: Database,
64
+ scope: { accountId: string; workspaceId: string },
65
+ sessionId: string,
66
+ turnId: string | null,
67
+ ): Promise<SiteAgentClamp> {
68
+ if (turnId === null) return (await readSiteSessionClamp(db, scope, sessionId, null)).clamp;
69
+ const key = `${scope.workspaceId}:${sessionId}:${turnId}`;
70
+ const now = Date.now();
71
+ const cached = clampCache.get(key);
72
+ if (cached && cached.at > now - CLAMP_CACHE_MS) return cached.clamp;
73
+ const { clamp, cacheable } = await readSiteSessionClamp(db, scope, sessionId, turnId);
74
+ if (cacheable) {
75
+ if (clampCache.size >= CLAMP_CACHE_LIMIT) clampCache.clear();
76
+ clampCache.set(key, { clamp, at: now });
77
+ }
78
+ return clamp;
79
+ }
80
+
81
+ async function readSiteSessionClamp(
82
+ db: Database,
83
+ scope: { accountId: string; workspaceId: string },
84
+ sessionId: string,
85
+ turnId: string | null,
86
+ ): Promise<{ clamp: SiteAgentClamp; cacheable: boolean }> {
87
+ // A session id that is not a UUID names no session.
88
+ const own = UUID.test(sessionId)
89
+ ? await getSession(db, scope.workspaceId, sessionId).catch(() => undefined)
90
+ : null;
91
+ // A read that failed fails closed, as a partial lineage does. Never cached:
92
+ // it may have failed only for now.
93
+ if (own === undefined) return { clamp: { clamped: true, originSiteIds: [] }, cacheable: false };
94
+ // No such session: other checks refuse a caller whose session is gone.
95
+ if (!own) return { clamp: { clamped: false, originSiteIds: [] }, cacheable: false };
96
+ const lineage = await getSiteDataAgentSession(db, scope, sessionId, turnId);
97
+ let frameOrigin = siteSessionOriginFromMetadata(own.metadata) !== null;
98
+ // Frames without a published Site record no Site id; find their origin by walking up.
99
+ let parentId = own.parentSessionId;
100
+ let unread = false;
101
+ const read = (id: string) =>
102
+ getSession(db, scope.workspaceId, id).catch(() => {
103
+ unread = true;
104
+ return null;
105
+ });
106
+ for (let depth = 0; !frameOrigin && parentId && depth < SITE_DATA_AGENT_LINEAGE_MAX_DEPTH;) {
107
+ const parent = await read(parentId);
108
+ if (!parent) break;
109
+ frameOrigin = siteSessionOriginFromMetadata(parent.metadata) !== null;
110
+ parentId = parent.parentSessionId;
111
+ depth += 1;
112
+ }
113
+ if (!frameOrigin && own.rootSessionId && own.rootSessionId !== own.id) {
114
+ const root = await read(own.rootSessionId);
115
+ frameOrigin = siteSessionOriginFromMetadata(root?.metadata) !== null;
116
+ }
117
+ return {
118
+ clamp: {
119
+ // A lineage that cannot be read in full fails closed, as for Site Data.
120
+ clamped:
121
+ frameOrigin || lineage.originSiteIds.length > 0 || lineage.siteTurn || !lineage.complete,
122
+ originSiteIds: lineage.originSiteIds,
123
+ },
124
+ // A partial read (failed closed) is read again next time.
125
+ cacheable: lineage.complete && !unread,
126
+ };
127
+ }
128
+
129
+ /** What a Site-clamped agent may not create or change. */
130
+ export type SiteClampedResource =
131
+ | "schedules"
132
+ | "automations"
133
+ | "Site publications"
134
+ | "workspace instructions"
135
+ | "Skills"
136
+ | "Knowledge"
137
+ | "secrets"
138
+ | "workspace members, connections or settings"
139
+ | "the organization profile"
140
+ | "Connected Machines"
141
+ | "sandbox environments"
142
+ | "browser or computer sessions"
143
+ | "documents, spreadsheets or presentations the Site did not declare"
144
+ | "workspace settings";
145
+
146
+ /** The refusal message for a Site-clamped agent. */
147
+ export function siteClampedAgentMessage(what: SiteClampedResource): string {
148
+ return `An agent started from a Site page cannot create or change ${what}`;
149
+ }
150
+
151
+ /**
152
+ * The refusal when a Site-clamped agent reaches for compute beyond its own
153
+ * session's: `run_on`, attaching, swapping or provisioning a sandbox, or
154
+ * starting a worker on a chosen machine or another session's sandbox.
155
+ */
156
+ export const SITE_AGENT_COMPUTE_MESSAGE =
157
+ "An agent started from a Site page cannot run on, attach, provision or choose sandboxes or Connected Machines";
158
+
159
+ /** Fields of a session create that choose its compute, which a Site-clamped agent may not send. */
160
+ const SITE_AGENT_COMPUTE_FIELDS = ["targetSandboxId", "workingDir"] as const;
161
+
162
+ /**
163
+ * Refuse a Site-clamped agent's session create that chooses compute: a
164
+ * machine target, a self-hosted backend or another session's sandbox group.
165
+ * Its workers run in its own sandbox (shared) or a new managed one.
166
+ */
167
+ export async function assertSiteAgentMayChooseCompute(
168
+ db: Database,
169
+ grant: AccessGrant,
170
+ payload: {
171
+ sandbox?: unknown;
172
+ sandboxBackend?: unknown;
173
+ targetSandboxId?: unknown;
174
+ workingDir?: unknown;
175
+ },
176
+ ): Promise<void> {
177
+ const choosesCompute =
178
+ SITE_AGENT_COMPUTE_FIELDS.some(
179
+ (field) => payload[field] !== undefined && payload[field] !== null,
180
+ ) ||
181
+ payload.sandboxBackend === "selfhosted" ||
182
+ (payload.sandbox !== undefined &&
183
+ payload.sandbox !== null &&
184
+ typeof payload.sandbox === "object");
185
+ if (!choosesCompute) return;
186
+ if (!(await siteAgentClamp(db, grant))?.clamped) return;
187
+ throw new HTTPException(403, { message: SITE_AGENT_COMPUTE_MESSAGE });
188
+ }
189
+
190
+ /**
191
+ * Refuse a Site-clamped agent: an agent started from a Site page acts with
192
+ * that page's reach and cannot create or change schedules, automations, Site
193
+ * publications, workspace instructions, Skills, Knowledge, secrets,
194
+ * workspace members, connections and settings, the organization profile,
195
+ * Connected Machines, sandbox environments or other workspace settings.
196
+ */
197
+ export async function assertNotSiteClampedAgent(
198
+ db: Database,
199
+ grant: AccessGrant,
200
+ what: SiteClampedResource,
201
+ ): Promise<void> {
202
+ const clamp = await siteAgentClamp(db, grant);
203
+ if (!clamp?.clamped) return;
204
+ throw new HTTPException(403, { message: siteClampedAgentMessage(what) });
205
+ }
206
+
207
+ /**
208
+ * Whether an agent running in `caller` may message, steer or control
209
+ * `targetSessionId`. An unclamped agent is not limited here. A Site-clamped
210
+ * agent reaches only sessions that act for the same Site: clamped themselves
211
+ * (its own lineage, or another session the Site started) and naming no
212
+ * published Site the caller does not.
213
+ */
214
+ export async function siteAgentMayReachSession(
215
+ db: Database,
216
+ scope: { accountId: string; workspaceId: string },
217
+ caller: { sessionId: string; turnId: string | null },
218
+ targetSessionId: string,
219
+ ): Promise<boolean> {
220
+ if (targetSessionId === caller.sessionId) return true;
221
+ const callerClamp = await siteSessionClamp(db, scope, caller.sessionId, caller.turnId);
222
+ if (!callerClamp.clamped) return true;
223
+ const target = await siteSessionClamp(db, scope, targetSessionId, null);
224
+ const callerSites = new Set(callerClamp.originSiteIds);
225
+ return target.clamped && target.originSiteIds.every((siteId) => callerSites.has(siteId));
226
+ }
@@ -1,9 +1,31 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
 
3
- export type SiteSessionOrigin = { siteId: string; title: string };
3
+ /**
4
+ * Which Site page a request came from. Provenance, plus the key the API uses
5
+ * to keep one Site's pages away from another Site's sessions.
6
+ *
7
+ * A published Site has a `siteId` (and `versionId` when the host pinned one).
8
+ * Inline HTML and Codemode preview frames have no published id: they record
9
+ * `siteId: null`, the `frame` kind and the host-minted `frameId` of the
10
+ * mounted page, so a frame reaches only the sessions it started itself.
11
+ */
12
+ export type SiteSessionOrigin = {
13
+ siteId: string | null;
14
+ title: string;
15
+ versionId?: string;
16
+ frame?: "inline" | "preview";
17
+ frameId?: string;
18
+ };
19
+
20
+ export const SITE_ORIGIN_METADATA_KEY = "_opengeniSiteOrigin";
21
+
4
22
  const origins = new AsyncLocalStorage<SiteSessionOrigin>();
5
23
 
6
- /** Request-local provenance, set only after the API validates a published Site version. */
24
+ /**
25
+ * Request-local provenance, set only after the API validated the Site the
26
+ * request came from (a published Site version the caller can see, or a
27
+ * host-marked inline or preview frame).
28
+ */
7
29
  export function withSiteSessionOrigin<T>(origin: SiteSessionOrigin, run: () => T): T {
8
30
  return origins.run(origin, run);
9
31
  }
@@ -11,7 +33,64 @@ export function withSiteSessionOrigin<T>(origin: SiteSessionOrigin, run: () => T
11
33
  export function sessionCreationMetadata(
12
34
  metadata: Record<string, unknown>,
13
35
  ): Record<string, unknown> {
14
- const { _opengeniSiteOrigin: _ignored, ...rest } = metadata;
15
- const origin = origins.getStore();
16
- return origin ? { ...rest, _opengeniSiteOrigin: origin } : rest;
36
+ return withSiteOriginMetadata(metadata, origins.getStore() ?? null);
37
+ }
38
+
39
+ /** The validated Site origin of the current request, if any. Provenance only. */
40
+ export function currentSiteSessionOrigin(): SiteSessionOrigin | undefined {
41
+ return origins.getStore();
42
+ }
43
+
44
+ /**
45
+ * Replace any caller-supplied Site origin in `metadata` with `origin` (or drop
46
+ * it). Callers never choose their own provenance.
47
+ */
48
+ export function withSiteOriginMetadata(
49
+ metadata: Record<string, unknown>,
50
+ origin: SiteSessionOrigin | null | undefined,
51
+ ): Record<string, unknown> {
52
+ const { [SITE_ORIGIN_METADATA_KEY]: _ignored, ...rest } = metadata;
53
+ return origin ? { ...rest, [SITE_ORIGIN_METADATA_KEY]: origin } : rest;
54
+ }
55
+
56
+ /**
57
+ * Read the recorded Site origin from session (or schedule agent config)
58
+ * metadata. Any object under the key counts as a Site origin, so a malformed
59
+ * record still marks the session as Site-started (fail closed).
60
+ */
61
+ export function siteSessionOriginFromMetadata(
62
+ metadata: Record<string, unknown> | null | undefined,
63
+ ): SiteSessionOrigin | null {
64
+ const raw = metadata?.[SITE_ORIGIN_METADATA_KEY];
65
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
66
+ const value = raw as Record<string, unknown>;
67
+ return {
68
+ siteId: typeof value.siteId === "string" && value.siteId.length > 0 ? value.siteId : null,
69
+ title: typeof value.title === "string" ? value.title : "",
70
+ ...(typeof value.versionId === "string" ? { versionId: value.versionId } : {}),
71
+ ...(value.frame === "inline" || value.frame === "preview" ? { frame: value.frame } : {}),
72
+ ...(typeof value.frameId === "string" && value.frameId.length > 0
73
+ ? { frameId: value.frameId }
74
+ : {}),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Whether a request from the `requester` Site page may reach a session (or
80
+ * schedule) whose recorded origin is `target`. Same published Site, or, for
81
+ * frames without a published id, the same mounted frame. Anything without a
82
+ * Site origin is out of reach for a Site page.
83
+ */
84
+ export function siteOriginsMatch(
85
+ requester: SiteSessionOrigin,
86
+ target: SiteSessionOrigin | null | undefined,
87
+ ): boolean {
88
+ if (!target) return false;
89
+ if (requester.siteId !== null) return target.siteId === requester.siteId;
90
+ return (
91
+ target.siteId === null &&
92
+ requester.frameId !== undefined &&
93
+ target.frameId !== undefined &&
94
+ target.frameId === requester.frameId
95
+ );
17
96
  }
@@ -1,10 +1,12 @@
1
+ import type { VoiceInputPricing, VoiceInputUsage } from "@opengeni/config";
1
2
  import type { TranscribeAudioResponse, VoiceInputErrorCode } from "@opengeni/contracts";
3
+ import type { CreditDebitAttribution } from "@opengeni/db";
2
4
 
3
5
  /**
4
6
  * Server-owned upstream budget for one provider attempt. Resumable recording
5
7
  * claims remain fenced for longer than this budget before another worker may
6
8
  * reclaim them. Provider adapters must honor the supplied AbortSignal and must
7
- * not return while their upstream request is still live; OpenGeni does not
9
+ * not return while their upstream request is still live; Opengeni does not
8
10
  * claim remote-side idempotency or cancellation for vendors that cannot meet
9
11
  * that adapter contract.
10
12
  */
@@ -34,6 +36,39 @@ export type TranscriptionRequest = {
34
36
  preferredProvider?: string | null | undefined;
35
37
  fallbackEnabled?: boolean | undefined;
36
38
  excludedProviders?: readonly string[] | undefined;
39
+ /**
40
+ * Credit settlement facts for a deployment-funded provider. Required when
41
+ * the selected provider is deployment-funded and credit billing is active;
42
+ * the service fails closed without it.
43
+ */
44
+ billing?: TranscriptionBillingContext | undefined;
45
+ /**
46
+ * Internal: return `settleBilling` instead of settling before returning, so
47
+ * a resumable segment's transcript commits before any billing write.
48
+ */
49
+ deferBillingSettlement?: boolean;
50
+ };
51
+
52
+ /**
53
+ * Trusted, server-built settlement identity for one transcription unit. The
54
+ * usage receipt and debit idempotency keys derive from `workspaceId` +
55
+ * `sourceId` (see `voiceInputSettlementKeys`), so a retry or a later
56
+ * reconciliation settles the same unit exactly once.
57
+ */
58
+ export type TranscriptionBillingContext = {
59
+ /**
60
+ * Server-derived unit identity. Never a client-chosen value: a reused id
61
+ * settles once, so a client-controlled id would make later calls free.
62
+ */
63
+ sourceId: string;
64
+ /** Trusted payer facts from the authenticated request boundary. */
65
+ attribution: CreditDebitAttribution;
66
+ /**
67
+ * Audio duration measured by the server from WAV bytes it produced (the
68
+ * resumable segment, or the normalized one-shot upload). Bills whenever the
69
+ * provider reports no usage this deployment has a price for.
70
+ */
71
+ trustedDurationSeconds?: number | undefined;
37
72
  };
38
73
 
39
74
  export type TranscriptionResult = TranscribeAudioResponse & {
@@ -41,17 +76,79 @@ export type TranscriptionResult = TranscribeAudioResponse & {
41
76
  providerId: string;
42
77
  audioSeconds: number;
43
78
  latencyMs: number;
79
+ /** Opengeni credits charged for this call (0 for subscription/free providers). */
80
+ creditCostMicros?: number | undefined;
81
+ /**
82
+ * Server-only settlement callback; never serialized into a public response.
83
+ * Never rejects: a settlement failure after provider success is logged and
84
+ * retried, and the transcript is still delivered.
85
+ */
86
+ settleBilling?: () => Promise<void>;
87
+ };
88
+
89
+ export type TranscriptionBillingRefusalCode =
90
+ | "insufficient_credits"
91
+ | "allowance_exhausted"
92
+ | "monthly_model_cost_limit";
93
+
94
+ /**
95
+ * Pre-use refusal for deployment-funded transcription: 402 for credits and
96
+ * allowances (same codes as session admission), 429 for a static monthly cap.
97
+ */
98
+ export class TranscriptionBillingRefusedError extends Error {
99
+ readonly status: 402 | 429;
100
+ readonly code: TranscriptionBillingRefusalCode;
101
+ readonly details: { scope?: string; resetsAt?: string | null; subjectId?: string };
102
+
103
+ constructor(input: {
104
+ code: TranscriptionBillingRefusalCode;
105
+ message: string;
106
+ details?: { scope?: string; resetsAt?: string | null; subjectId?: string };
107
+ }) {
108
+ super(input.message);
109
+ this.name = "TranscriptionBillingRefusedError";
110
+ this.code = input.code;
111
+ this.status = input.code === "monthly_model_cost_limit" ? 429 : 402;
112
+ this.details = input.details ?? {};
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Credit admission and settlement for deployment-funded transcription.
118
+ * Subscription providers (Codex, SuperGrok) never reach this port.
119
+ */
120
+ export type TranscriptionBilling = {
121
+ /** Refuse before any audio is sent when the payer cannot fund the call. */
122
+ admit(input: {
123
+ accountId: string;
124
+ workspaceId: string;
125
+ attribution: CreditDebitAttribution;
126
+ }): Promise<void>;
127
+ /**
128
+ * Record usage and debit credits once per unit, after use. The usage receipt
129
+ * commits before the debit, so a failed debit leaves a durable receipt that
130
+ * `admit` reconciles on the workspace's next voice request.
131
+ */
132
+ settle(input: {
133
+ accountId: string;
134
+ workspaceId: string;
135
+ providerId: string;
136
+ model: string;
137
+ pricing: VoiceInputPricing;
138
+ usage: VoiceInputUsage | null;
139
+ billing: TranscriptionBillingContext;
140
+ }): Promise<{ creditCostMicros: number }>;
44
141
  };
45
142
 
46
143
  export class TranscriptionServiceError extends Error {
47
- readonly code: VoiceInputErrorCode;
144
+ readonly code: Exclude<VoiceInputErrorCode, TranscriptionBillingRefusalCode>;
48
145
  readonly status: number;
49
146
  readonly retryable: boolean;
50
147
  /** Explicit rejection before any transcription result; safe to try another provider. */
51
148
  readonly fallbackSafe: boolean;
52
149
 
53
150
  constructor(input: {
54
- code: VoiceInputErrorCode;
151
+ code: Exclude<VoiceInputErrorCode, TranscriptionBillingRefusalCode>;
55
152
  message: string;
56
153
  status?: number;
57
154
  retryable?: boolean;
@@ -114,6 +211,14 @@ export type TranscriptionProvider = {
114
211
  /** The adapter guarantees that its upstream transport honors AbortSignal. */
115
212
  readonly supportsServerDeadline: true;
116
213
  readonly experimental?: boolean | undefined;
214
+ /**
215
+ * Present when the deployment pays the upstream provider (OpenAI/Azure):
216
+ * the call is admitted against and settled in Opengeni credits. Absent for
217
+ * the workspace's own subscriptions, which are never charged.
218
+ */
219
+ readonly deploymentFunded?:
220
+ | { readonly model: string; readonly pricing: VoiceInputPricing | null }
221
+ | undefined;
117
222
  /**
118
223
  * Deployment readiness when called without a workspace. When `workspaceId` is
119
224
  * provided, providers may require a workspace-attached credential (e.g. Codex).
@@ -128,17 +233,29 @@ export type TranscriptionProvider = {
128
233
  subjectId: string;
129
234
  requestId: string;
130
235
  signal?: AbortSignal | undefined;
131
- }): Promise<{ text: string; languages: string[] }>;
236
+ }): Promise<{ text: string; languages: string[]; usage?: VoiceInputUsage | null | undefined }>;
132
237
  };
133
238
 
134
239
  export type TranscriptionService = {
135
240
  limits(): TranscriptionLimits;
136
241
  /** True when at least one ready provider can serve requests. */
137
242
  available(context?: TranscriptionAvailabilityContext): boolean | Promise<boolean>;
243
+ availableProviderIds?(context: TranscriptionAvailabilityContext): Promise<string[]>;
138
244
  /** Select one provider before a durable segment attempt; retries pin this id. */
139
245
  selectProvider?(
140
246
  context: TranscriptionAvailabilityContext,
141
247
  ): string | null | Promise<string | null>;
248
+ /**
249
+ * Credit admission for an exact provider before durable work is claimed.
250
+ * Resolves for subscription providers; throws TranscriptionBillingRefusedError
251
+ * when a deployment-funded provider cannot be paid for.
252
+ */
253
+ admit?(input: {
254
+ providerId: string;
255
+ accountId: string;
256
+ workspaceId: string;
257
+ attribution: CreditDebitAttribution;
258
+ }): Promise<void>;
142
259
  transcribe(request: TranscriptionRequest): Promise<TranscriptionResult>;
143
260
  };
144
261
 
@@ -156,6 +273,8 @@ export type TranscriptionSegmenter = {
156
273
  sourceMimeType: string;
157
274
  totalDurationMilliseconds: number;
158
275
  providerSegmentSeconds: number;
276
+ /** Hard decode ceiling (`ffmpeg -t`); audio past it is never decoded. */
277
+ maxDecodeSeconds?: number | undefined;
159
278
  chunks: AsyncIterable<Uint8Array>;
160
279
  signal?: AbortSignal | undefined;
161
280
  }): AsyncIterable<PreparedTranscriptionSegment>;
@@ -0,0 +1,47 @@
1
+ import type { AccessGrant, SessionTurnSurface } from "@opengeni/contracts";
2
+ import type { AccessGrantAuthorization } from "./access";
3
+ import { currentSiteSessionOrigin } from "./site-session-origin";
4
+
5
+ /**
6
+ * Resolve the content-free product surface a request entered through.
7
+ *
8
+ * The result is a label frozen on the accepted turn, mainly for analytics. It
9
+ * never grants access; the one place it affects authority is Site Data, where a
10
+ * `site` surface only narrows an agent to viewer reach
11
+ * (apps/api/src/site-data/service.ts). It reads trusted provenance only where
12
+ * it already exists: an explicit entry-point choice (Slack, automations, maintenance), the
13
+ * API-validated Site scope, and the access path the grant was resolved from.
14
+ * A grant whose path this function cannot place yields null rather than a
15
+ * guess.
16
+ */
17
+ export function resolveTurnSurface(input: {
18
+ grant: AccessGrant;
19
+ authorization?: AccessGrantAuthorization | null | undefined;
20
+ /** An entry point that knows its surface (for example Slack) passes it here. */
21
+ requested?: SessionTurnSurface | null | undefined;
22
+ }): SessionTurnSurface | null {
23
+ if (input.requested) return input.requested;
24
+ if (currentSiteSessionOrigin()) return "site";
25
+ const { grant } = input;
26
+ const metadata = grant.metadata ?? {};
27
+ if (
28
+ grant.principalKind === "agent_attempt" ||
29
+ metadata.turnId !== undefined ||
30
+ metadata.attemptId !== undefined
31
+ ) {
32
+ return "agent";
33
+ }
34
+ if (metadata.externalActor !== undefined) return "embedded";
35
+ if (metadata.mcpOAuth === true) return "mcp";
36
+ if (grant.principalKind === "api_key" || grant.principalKind === "configured_key") {
37
+ return "api_key";
38
+ }
39
+ if (metadata.delegated === true || grant.serviceInitiator) return "embedded";
40
+ if (
41
+ input.authorization?.canonicalManagedHumanSession ||
42
+ input.authorization?.canonicalLocalHumanSession
43
+ ) {
44
+ return "web";
45
+ }
46
+ return grant.principalKind === "human_session" ? "web" : null;
47
+ }