@opengeni/core 3.0.1 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +4 -4
  2. package/dist/access/index.d.ts +131 -7
  3. package/dist/access/service-initiator.d.ts +3 -0
  4. package/dist/application/archived-session-imports.d.ts +6 -0
  5. package/dist/application/composer-submit.d.ts +1 -1
  6. package/dist/application/connect-operation.d.ts +6 -0
  7. package/dist/application/external-workspace-members.d.ts +13 -1
  8. package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
  9. package/dist/application/modal-native-original-configuration.d.ts +79 -0
  10. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  11. package/dist/application/new-session-drafts.d.ts +29 -2
  12. package/dist/application/organization-privacy.d.ts +57 -0
  13. package/dist/application/prepared-mcp-connection.d.ts +31 -0
  14. package/dist/application/sandbox-recovery.d.ts +58 -0
  15. package/dist/application/session-commands.d.ts +3 -0
  16. package/dist/application/session-tenancy.d.ts +13 -4
  17. package/dist/billing/agent-run-admission.d.ts +21 -0
  18. package/dist/billing/limits.d.ts +27 -1
  19. package/dist/billing/model-call-admission.d.ts +34 -0
  20. package/dist/billing/model-usage-settlement.d.ts +112 -0
  21. package/dist/canonical-human-identities.js +1 -1
  22. package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
  23. package/dist/chunk-7T2SZ7BI.js.map +1 -0
  24. package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
  25. package/dist/chunk-OMJFIIWK.js.map +1 -0
  26. package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
  27. package/dist/chunk-QI2M2B2K.js.map +1 -0
  28. package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
  29. package/dist/chunk-TTWTXQB2.js.map +1 -0
  30. package/dist/codex-model-availability.d.ts +55 -0
  31. package/dist/default-session-model.d.ts +192 -0
  32. package/dist/dependencies.d.ts +41 -7
  33. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  34. package/dist/domain/agent-config-resolution.d.ts +49 -0
  35. package/dist/domain/automations.d.ts +8 -1
  36. package/dist/domain/capabilities.d.ts +112 -8
  37. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  38. package/dist/domain/connector-tool-permissions.d.ts +62 -0
  39. package/dist/domain/direct-model-provider.d.ts +3 -0
  40. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  41. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  42. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  43. package/dist/domain/editable-artifacts/service.d.ts +3 -1
  44. package/dist/domain/editable-artifacts/types.d.ts +1 -1
  45. package/dist/domain/encryption-key.d.ts +12 -0
  46. package/dist/domain/insights-timing.d.ts +16 -0
  47. package/dist/domain/insights-usage.d.ts +27 -0
  48. package/dist/domain/insights.d.ts +21 -3
  49. package/dist/domain/knowledge-files.d.ts +1 -1
  50. package/dist/domain/knowledge-preparation.d.ts +10 -0
  51. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  52. package/dist/domain/knowledge-search.d.ts +15 -76
  53. package/dist/domain/mcp-account-bindings.d.ts +17 -0
  54. package/dist/domain/mcp-account-routes.d.ts +21 -0
  55. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  56. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  57. package/dist/domain/personal-connection-delegations.d.ts +73 -12
  58. package/dist/domain/pr-review.d.ts +121 -6
  59. package/dist/domain/realtime-voice-billing.d.ts +103 -0
  60. package/dist/domain/resources.d.ts +17 -1
  61. package/dist/domain/scheduled-task-access.d.ts +265 -0
  62. package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
  63. package/dist/domain/scheduled-task-audience.d.ts +70 -0
  64. package/dist/domain/scheduled-tasks.d.ts +61 -18
  65. package/dist/domain/session-connection-accounts.d.ts +53 -0
  66. package/dist/domain/session-file-access.d.ts +4 -0
  67. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  68. package/dist/domain/session-start-timing.d.ts +7 -0
  69. package/dist/domain/session-tool-policy.d.ts +102 -4
  70. package/dist/domain/sessions.d.ts +179 -15
  71. package/dist/domain/site-audience.d.ts +52 -0
  72. package/dist/domain/skills.d.ts +29 -1
  73. package/dist/domain/slack-bot.d.ts +46 -1
  74. package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
  75. package/dist/domain/vaults/index.d.ts +7 -0
  76. package/dist/domain/vaults/mapping.d.ts +25 -0
  77. package/dist/domain/vaults/provider-registry.d.ts +72 -0
  78. package/dist/domain/vaults/provider-sync.d.ts +176 -0
  79. package/dist/domain/vaults/provider-writes.d.ts +57 -0
  80. package/dist/domain/vaults/secret-resolver.d.ts +34 -0
  81. package/dist/domain/vaults/types.d.ts +24 -0
  82. package/dist/domain/vaults/vault-service.d.ts +35 -0
  83. package/dist/domain/voice-input-billing.d.ts +77 -0
  84. package/dist/domain/web-search-billing.d.ts +51 -0
  85. package/dist/domain/workspace-credits.d.ts +7 -0
  86. package/dist/editable-artifact-live/application.d.ts +2 -1
  87. package/dist/editable-artifact-live/server.d.ts +5 -1
  88. package/dist/editable-artifact-live/ticket.d.ts +2 -1
  89. package/dist/editable-artifact-live/types.d.ts +9 -0
  90. package/dist/editable-artifact-live.js +2 -2
  91. package/dist/editable-artifacts.js +10 -2
  92. package/dist/goal-admission.d.ts +627 -0
  93. package/dist/host-session-usage-limit.d.ts +15 -0
  94. package/dist/index.d.ts +43 -8
  95. package/dist/index.js +22682 -11743
  96. package/dist/index.js.map +1 -1
  97. package/dist/managed-session.d.ts +110 -7
  98. package/dist/model-call.d.ts +58 -0
  99. package/dist/model-catalog.d.ts +29 -3
  100. package/dist/model-provider-credentials.d.ts +43 -0
  101. package/dist/product-usage-metrics.d.ts +30 -0
  102. package/dist/rigs/index.d.ts +1 -13
  103. package/dist/sandbox/fleet.d.ts +58 -4
  104. package/dist/sandbox/routing.d.ts +42 -2
  105. package/dist/sandbox/runtime-settings.d.ts +27 -17
  106. package/dist/session-authorization.d.ts +10 -0
  107. package/dist/session-usage-limit-metrics.d.ts +60 -0
  108. package/dist/session-usage-limits.d.ts +89 -0
  109. package/dist/site-agent-clamp.d.ts +71 -0
  110. package/dist/site-session-origin.d.ts +39 -2
  111. package/dist/transcription.d.ts +120 -3
  112. package/dist/turn-surface.d.ts +20 -0
  113. package/dist/user-presence.d.ts +32 -0
  114. package/dist/workflow-wake-contract.d.ts +13 -1
  115. package/package.json +15 -22
  116. package/src/access/index.ts +961 -38
  117. package/src/access/service-initiator.ts +50 -0
  118. package/src/application/api-integration-servers.ts +9 -0
  119. package/src/application/archived-session-imports.ts +161 -0
  120. package/src/application/composer-submit.ts +3 -2
  121. package/src/application/connect-authority.ts +3 -3
  122. package/src/application/connect-operation.ts +37 -3
  123. package/src/application/external-continuation.ts +20 -3
  124. package/src/application/external-link-work-admission.ts +3 -0
  125. package/src/application/external-workspace-members.ts +51 -3
  126. package/src/application/modal-native-live-declaration-join.ts +196 -0
  127. package/src/application/modal-native-original-configuration.ts +261 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +172 -55
  130. package/src/application/organization-privacy.ts +161 -0
  131. package/src/application/prepared-mcp-connection.ts +133 -0
  132. package/src/application/sandbox-recovery.ts +78 -0
  133. package/src/application/session-commands.ts +73 -1
  134. package/src/application/session-mcp-credential-rotation.ts +61 -18
  135. package/src/application/session-tenancy.ts +262 -44
  136. package/src/application/user-resource-grants.ts +1 -29
  137. package/src/billing/agent-run-admission.ts +117 -0
  138. package/src/billing/limits.ts +138 -14
  139. package/src/billing/model-call-admission.ts +118 -0
  140. package/src/billing/model-usage-settlement.ts +472 -0
  141. package/src/codex-model-availability.ts +264 -0
  142. package/src/default-session-model.ts +692 -0
  143. package/src/dependencies.ts +48 -6
  144. package/src/developer-setup-credential-ceiling.ts +60 -0
  145. package/src/domain/agent-config-resolution.ts +148 -0
  146. package/src/domain/automations.ts +76 -2
  147. package/src/domain/capabilities.ts +830 -181
  148. package/src/domain/connection-account-selection-error.ts +19 -0
  149. package/src/domain/connector-tool-permissions.ts +572 -0
  150. package/src/domain/direct-model-provider.ts +62 -0
  151. package/src/domain/editable-artifacts/agent-application.ts +44 -6
  152. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  153. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  154. package/src/domain/editable-artifacts/ports.ts +11 -0
  155. package/src/domain/editable-artifacts/service.ts +20 -0
  156. package/src/domain/editable-artifacts/types.ts +1 -1
  157. package/src/domain/encryption-key.ts +26 -0
  158. package/src/domain/file-owner.ts +16 -1
  159. package/src/domain/github-action-policies.ts +1 -1
  160. package/src/domain/github-skill-source.ts +3 -1
  161. package/src/domain/insights-timing.ts +95 -0
  162. package/src/domain/insights-usage.ts +67 -0
  163. package/src/domain/insights.ts +167 -43
  164. package/src/domain/knowledge-files.ts +10 -3
  165. package/src/domain/knowledge-messages.ts +1 -0
  166. package/src/domain/knowledge-preparation.test.ts +276 -0
  167. package/src/domain/knowledge-preparation.ts +147 -0
  168. package/src/domain/knowledge-search.test.ts +617 -5
  169. package/src/domain/knowledge-search.ts +240 -23
  170. package/src/domain/mcp-account-bindings.ts +169 -0
  171. package/src/domain/mcp-account-routes.ts +152 -0
  172. package/src/domain/native-mcp-connection-admission.ts +14 -0
  173. package/src/domain/organization-integration-catalog.ts +88 -0
  174. package/src/domain/personal-connection-delegations.ts +399 -257
  175. package/src/domain/pr-review.ts +24 -8
  176. package/src/domain/realtime-voice-billing.ts +425 -0
  177. package/src/domain/resources.ts +50 -4
  178. package/src/domain/scheduled-task-access.ts +1089 -0
  179. package/src/domain/scheduled-task-account-audit.ts +90 -0
  180. package/src/domain/scheduled-task-audience.ts +249 -0
  181. package/src/domain/scheduled-tasks.ts +1313 -374
  182. package/src/domain/session-connection-accounts.ts +239 -0
  183. package/src/domain/session-file-access.ts +47 -0
  184. package/src/domain/session-native-connection-replacement.ts +64 -0
  185. package/src/domain/session-start-timing.ts +60 -0
  186. package/src/domain/session-tool-policy.ts +489 -14
  187. package/src/domain/sessions.ts +2254 -717
  188. package/src/domain/site-audience.ts +130 -0
  189. package/src/domain/skills.ts +9 -0
  190. package/src/domain/slack-bot.ts +141 -4
  191. package/src/domain/slack-publication-secret-safety.ts +1 -1
  192. package/src/domain/vaults/index.ts +19 -0
  193. package/src/domain/vaults/mapping.ts +190 -0
  194. package/src/domain/vaults/provider-registry.ts +201 -0
  195. package/src/domain/vaults/provider-sync.ts +531 -0
  196. package/src/domain/vaults/provider-writes.ts +288 -0
  197. package/src/domain/vaults/secret-resolver.ts +464 -0
  198. package/src/domain/vaults/types.ts +26 -0
  199. package/src/domain/vaults/vault-service.ts +1316 -0
  200. package/src/domain/voice-input-billing.ts +330 -0
  201. package/src/domain/web-search-billing.ts +169 -0
  202. package/src/domain/workspace-credits.ts +13 -0
  203. package/src/editable-artifact-live/application.ts +8 -1
  204. package/src/editable-artifact-live/server.ts +154 -6
  205. package/src/editable-artifact-live/ticket.ts +86 -1
  206. package/src/editable-artifact-live/types.ts +10 -0
  207. package/src/goal-admission.ts +220 -0
  208. package/src/host-session-usage-limit.ts +25 -0
  209. package/src/index.ts +75 -11
  210. package/src/managed-session.ts +137 -6
  211. package/src/model-call.ts +79 -0
  212. package/src/model-catalog.ts +345 -91
  213. package/src/model-provider-credentials.ts +394 -0
  214. package/src/product-usage-metrics.ts +101 -0
  215. package/src/rigs/index.ts +23 -111
  216. package/src/sandbox/fleet.ts +253 -28
  217. package/src/sandbox/routing.ts +204 -43
  218. package/src/sandbox/runtime-settings.ts +159 -85
  219. package/src/session-authorization.ts +172 -23
  220. package/src/session-usage-limit-metrics.ts +95 -0
  221. package/src/session-usage-limits.ts +415 -0
  222. package/src/site-agent-clamp.ts +226 -0
  223. package/src/site-session-origin.ts +84 -5
  224. package/src/transcription.ts +123 -4
  225. package/src/turn-surface.ts +47 -0
  226. package/src/user-presence.ts +134 -0
  227. package/src/workflow-wake-contract.ts +14 -1
  228. package/dist/application/host-mcp-owner.d.ts +0 -6
  229. package/dist/application/host-mcp-resolvers.d.ts +0 -13
  230. package/dist/chunk-5WZYFL45.js +0 -266
  231. package/dist/chunk-5WZYFL45.js.map +0 -1
  232. package/dist/chunk-H7X52RI2.js.map +0 -1
  233. package/dist/chunk-OF65T3PM.js.map +0 -1
  234. package/dist/chunk-VCZFFEPB.js.map +0 -1
  235. package/dist/chunk-ZYCNLYGH.js.map +0 -1
  236. package/dist/domain/connection-authority.d.ts +0 -97
  237. package/dist/domain/connection-authority.js +0 -169
  238. package/dist/domain/connection-authority.js.map +0 -1
  239. package/dist/domain/environments.d.ts +0 -33
  240. package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
  241. package/dist/domain/host-mcp-task-admission.d.ts +0 -18
  242. package/dist/domain/packs.d.ts +0 -59
  243. package/dist/domain/product-integration-pack.d.ts +0 -44
  244. package/dist/domain/product-integration-skill.gen.d.ts +0 -5
  245. package/dist/remote-mcp-credentials.d.ts +0 -23
  246. package/dist/remote-mcp-credentials.js +0 -11
  247. package/dist/remote-mcp-credentials.js.map +0 -1
  248. package/src/application/host-mcp-owner.ts +0 -41
  249. package/src/application/host-mcp-resolvers.ts +0 -84
  250. package/src/domain/connection-authority.ts +0 -329
  251. package/src/domain/environments.ts +0 -120
  252. package/src/domain/host-mcp-authority-source-admission.ts +0 -25
  253. package/src/domain/host-mcp-task-admission.ts +0 -121
  254. package/src/domain/packs.ts +0 -699
  255. package/src/domain/product-integration-pack.ts +0 -42
  256. package/src/domain/product-integration-skill.gen.ts +0 -52
  257. package/src/remote-mcp-credentials.ts +0 -361
@@ -1,7 +1,7 @@
1
- import type { Settings } from "@opengeni/config";
1
+ import { sandboxImageAllowlist, type Settings } from "@opengeni/config";
2
2
  import {
3
- CapabilityPack,
4
- StoredCapabilityPack,
3
+ resolveWorkspaceDefaultSandboxImage,
4
+ type AccessGrant,
5
5
  type RigVersion,
6
6
  type Session,
7
7
  type SandboxBackend,
@@ -9,11 +9,17 @@ import {
9
9
  } from "@opengeni/contracts";
10
10
  import {
11
11
  getRigVersion,
12
- getWorkspacePack,
13
- listPackInstallations,
12
+ getScheduledScopedRigVersionMetadata,
13
+ getWorkspace,
14
+ nestedPostgresSqlState,
14
15
  type Database,
15
16
  } from "@opengeni/db";
16
17
  import { resolveModalCheckpointProviderBinding } from "@opengeni/runtime/sandbox";
18
+ import { HTTPException } from "hono/http-exception";
19
+ import {
20
+ grantHasAgentAttemptAuthority,
21
+ requireLiveAgentAttemptAuthorization,
22
+ } from "../session-authorization";
17
23
  import {
18
24
  rigProviderImageContentHash,
19
25
  rigProviderImageMatchesDefinition,
@@ -41,6 +47,29 @@ export function managedSessionGroupBackend(
41
47
  return backend === "none" || backend === "selfhosted" ? null : backend;
42
48
  }
43
49
 
50
+ const SESSION_GROUP_MACHINE_NAMES: Record<ManagedSessionGroupBackend, string> = {
51
+ local: "this computer",
52
+ docker: "Docker",
53
+ modal: "Modal",
54
+ daytona: "Daytona",
55
+ runloop: "Runloop",
56
+ e2b: "E2B",
57
+ blaxel: "Blaxel",
58
+ cloudflare: "Cloudflare",
59
+ vercel: "Vercel",
60
+ opensandbox: "OpenSandbox",
61
+ };
62
+
63
+ /** Display name and kind for the session's own managed box. The kind is the
64
+ * real provider. Callers must not collapse local, Docker, or other providers
65
+ * into Modal. */
66
+ export function sessionGroupMachinePresentation(backend: ManagedSessionGroupBackend): {
67
+ name: string;
68
+ kind: ManagedSessionGroupBackend;
69
+ } {
70
+ return { name: SESSION_GROUP_MACHINE_NAMES[backend], kind: backend };
71
+ }
72
+
44
73
  /** A machine-home row carries the machine's host OS; its managed group uses the
45
74
  * platform's canonical managed-sandbox OS instead. Ordinary sessions preserve
46
75
  * their explicitly selected OS. */
@@ -51,74 +80,25 @@ export function managedSessionGroupOs(
51
80
  return sessionBackend === "selfhosted" ? "linux" : sessionOs;
52
81
  }
53
82
 
54
- /** Pre-V2 Packs are the sole compatibility path where a Pack can still own a
55
- * sandbox image. V2 installations select a Rig instead. Keep this predicate at
56
- * the shared runtime boundary so turns, API-direct tools, viewers, Browser and
57
- * Computer cannot resolve different physical machines for the same session. */
58
- export function packInstallationUsesLegacyRuntime(input: {
59
- manifestSnapshot: unknown | null;
60
- manifestDigest: string | null;
61
- }): boolean {
62
- return input.manifestSnapshot === null && input.manifestDigest === null;
63
- }
64
-
65
- export async function resolveWorkspaceLegacyRuntimePacks(
66
- db: Database,
67
- workspaceId: string,
68
- ): Promise<CapabilityPack[]> {
69
- const installations = await listPackInstallations(db, workspaceId);
70
- const packs: CapabilityPack[] = [];
71
- for (const installation of installations) {
72
- if (installation.status !== "active" || !packInstallationUsesLegacyRuntime(installation)) {
73
- continue;
74
- }
75
- const registration = await getWorkspacePack(db, workspaceId, installation.packId);
76
- if (!registration)
77
- throw new Error(`Enabled Pack ${installation.packId} has no registered manifest`);
78
- const parsed = StoredCapabilityPack.safeParse(registration.pack);
79
- if (!parsed.success)
80
- throw new Error(
81
- `Enabled Pack ${installation.packId} requires repair: ${parsed.error.message}`,
82
- );
83
- packs.push(parsed.data);
84
- }
85
- return packs;
86
- }
87
-
88
- export function legacySandboxRuntimeFromPacks(packs: readonly CapabilityPack[]): {
89
- sandboxImage: string | null;
90
- sandboxProviderImages: CapabilityPack["sandboxProviderImages"] | null;
91
- } {
92
- const imagePacks = packs.filter(
93
- (pack) => typeof pack.sandboxImage === "string" && pack.sandboxImage.trim().length > 0,
94
- );
95
- if (imagePacks.length > 1) {
96
- const ids = imagePacks
97
- .map((pack) => pack.id)
98
- .sort()
99
- .join(", ");
100
- throw new Error(
101
- `Multiple enabled packs declare a sandbox image (${ids}). Only one enabled pack per workspace may declare sandboxImage; disable the others and retry.`,
102
- );
103
- }
104
- return {
105
- sandboxImage: imagePacks[0]?.sandboxImage?.trim() ?? null,
106
- sandboxProviderImages: imagePacks[0]?.sandboxProviderImages ?? null,
107
- };
108
- }
109
-
110
- export function settingsWithPackSandboxImage(
83
+ /**
84
+ * Apply a workspace's selected sandbox image. The deployment keeps authority:
85
+ * only an image that is on its allowlist right now is used, so removing an
86
+ * entry returns every workspace that chose it to the deployment image. The
87
+ * lease treats the image as shared state, so a change rotates a live box
88
+ * through the normal capture-and-restore path instead of reusing it.
89
+ */
90
+ export function settingsWithWorkspaceSandboxImage(
111
91
  settings: Settings,
112
- sandboxImage: string | null,
113
- sandboxProviderImages: CapabilityPack["sandboxProviderImages"] | null = null,
92
+ workspaceSettings: unknown,
93
+ backend: SandboxBackend,
114
94
  ): Settings {
115
- if (!sandboxImage) return settings;
116
- return {
117
- ...settings,
118
- dockerImage: sandboxImage,
119
- modalImageRef: sandboxImage,
120
- modalImageId: sandboxProviderImages?.modal?.imageId,
121
- };
95
+ const image = resolveWorkspaceDefaultSandboxImage(workspaceSettings);
96
+ if (!image || !sandboxImageAllowlist(settings).includes(image)) return settings;
97
+ if (backend === "docker") return { ...settings, dockerImage: image };
98
+ if (backend === "modal") {
99
+ return { ...settings, modalImageRef: image, modalImageId: undefined };
100
+ }
101
+ return settings;
122
102
  }
123
103
 
124
104
  export function settingsWithRigImage(settings: Settings, rigImage: string | null): Settings {
@@ -272,27 +252,121 @@ export type SessionSandboxRuntime = {
272
252
  rigVersion: RigVersion | null;
273
253
  };
274
254
 
255
+ /** The subject a direct attach resolves under when no human subject drives it
256
+ * (the same sentinel the attach Variable Set defaults use). It holds no
257
+ * organization membership, so it can resolve organization and workspace
258
+ * Sandbox Environments but never a personal one. */
259
+ const SESSION_ATTACH_SUBJECT = "session-attach";
260
+
261
+ /** Who a direct attach resolves a session's Sandbox Environment as: an explicit
262
+ * subject (null for a service attach), or the authenticated route grant. */
263
+ export type SessionAttachRigAuthority = { subjectId: string | null } | { grant: AccessGrant };
264
+
265
+ /**
266
+ * The subject whose scoped visibility applies. An agent-attempt grant carries a
267
+ * technical worker identity with no organization membership, so it resolves as
268
+ * its live attempt's frozen initiating human, as the MCP Sandbox Environment and
269
+ * fleet tools do; an attempt without one resolves as the service sentinel.
270
+ */
271
+ async function sessionAttachRigSubjectId(
272
+ db: Database,
273
+ authority: SessionAttachRigAuthority,
274
+ ): Promise<string | null> {
275
+ if (!("grant" in authority)) return authority.subjectId;
276
+ const { grant } = authority;
277
+ if (!grantHasAgentAttemptAuthority(grant)) return grant.subjectId;
278
+ const callerSessionId = grant.metadata?.["sessionId"];
279
+ if (typeof callerSessionId !== "string") return null;
280
+ const actor = await requireLiveAgentAttemptAuthorization(db, grant, callerSessionId);
281
+ return actor.initiatingHumanSubjectId;
282
+ }
283
+
284
+ /** The scoped seam's refusal for a subject that is an organization member but
285
+ * has no current access to the session's workspace. Other 42501 failures
286
+ * (grants, capability rows) remain errors. */
287
+ function isWorkspaceAccessRefusal(error: unknown): boolean {
288
+ if (nestedPostgresSqlState(error) !== "42501") return false;
289
+ let current: unknown = error;
290
+ for (let depth = 0; depth < 8 && current instanceof Error; depth += 1) {
291
+ if (current.message.includes("lacks current workspace access")) return true;
292
+ current = current.cause;
293
+ }
294
+ return false;
295
+ }
296
+
297
+ /**
298
+ * The exact frozen Sandbox Environment version a session is bound to, resolved
299
+ * for a direct attach (terminal, Files, desktop viewer, Browser, Computer).
300
+ *
301
+ * A version homed in the session's own workspace resolves physically under the
302
+ * session access the route already enforced, exactly as before. An
303
+ * organization Sandbox Environment, or a personal one used outside its home
304
+ * workspace, keeps its versions in that home workspace, so those resolve
305
+ * through the same scoped authority the attach Variable Set defaults and
306
+ * scheduled tasks use: same organization, active, and visible to the attaching
307
+ * subject in the session's workspace.
308
+ */
309
+ async function resolveSessionRigVersion(
310
+ db: Database,
311
+ session: Pick<Session, "accountId" | "workspaceId"> & { rigId: string; rigVersionId: string },
312
+ authority: SessionAttachRigAuthority,
313
+ ): Promise<RigVersion | null> {
314
+ const local = await getRigVersion(db, session.workspaceId, session.rigId, session.rigVersionId);
315
+ if (local) return local;
316
+ const subjectId = await sessionAttachRigSubjectId(db, authority);
317
+ let scoped: Awaited<ReturnType<typeof getScheduledScopedRigVersionMetadata>>;
318
+ try {
319
+ scoped = await getScheduledScopedRigVersionMetadata(
320
+ db,
321
+ {
322
+ accountId: session.accountId,
323
+ workspaceId: session.workspaceId,
324
+ subjectId: subjectId ?? SESSION_ATTACH_SUBJECT,
325
+ },
326
+ session.rigId,
327
+ session.rigVersionId,
328
+ );
329
+ } catch (error) {
330
+ if (!isWorkspaceAccessRefusal(error)) throw error;
331
+ console.warn("[session-attach] sandbox environment refused without workspace access", {
332
+ workspaceId: session.workspaceId,
333
+ rigId: session.rigId,
334
+ rigVersionId: session.rigVersionId,
335
+ });
336
+ return null;
337
+ }
338
+ return scoped?.version.rigId === session.rigId ? scoped.version : null;
339
+ }
340
+
275
341
  export async function resolveSessionSandboxRuntime(
276
342
  db: Database,
277
343
  settings: Settings,
278
- session: Pick<Session, "workspaceId" | "sandboxBackend" | "rigId" | "rigVersionId">,
344
+ session: Pick<Session, "accountId" | "workspaceId" | "sandboxBackend" | "rigId" | "rigVersionId">,
345
+ /** Who drives the attach; pass the route grant whenever one exists. */
346
+ authority: SessionAttachRigAuthority,
279
347
  ): Promise<SessionSandboxRuntime> {
280
- const [packs, rigVersion] = await Promise.all([
281
- resolveWorkspaceLegacyRuntimePacks(db, session.workspaceId),
348
+ const rigVersion =
282
349
  session.rigId && session.rigVersionId
283
- ? getRigVersion(db, session.workspaceId, session.rigId, session.rigVersionId)
284
- : Promise.resolve(null),
285
- ]);
350
+ ? await resolveSessionRigVersion(
351
+ db,
352
+ { ...session, rigId: session.rigId, rigVersionId: session.rigVersionId },
353
+ authority,
354
+ )
355
+ : null;
286
356
  if (session.rigVersionId && !rigVersion) {
287
- throw new Error(`Frozen rig version ${session.rigVersionId} is unavailable`);
357
+ throw new HTTPException(403, {
358
+ message: "This session's Sandbox Environment version is not available.",
359
+ });
288
360
  }
289
- const legacy = legacySandboxRuntimeFromPacks(packs);
290
- // A Rig is always a setup/check layer over the deployment-owned platform
291
- // sandbox. Legacy pre-v2 Pack image compatibility remains available only to
292
- // rig-less sessions; it cannot replace the base beneath a Rig.
293
- const logicalSettings = rigVersion
294
- ? settings
295
- : settingsWithPackSandboxImage(settings, legacy.sandboxImage, legacy.sandboxProviderImages);
361
+ // Setup and checks layer on the deployment image, or on the workspace's
362
+ // allowlisted selection of one.
363
+ const logicalSettings = sandboxImageAllowlist(settings).length
364
+ ? settingsWithWorkspaceSandboxImage(
365
+ settings,
366
+ (await getWorkspace(db, session.workspaceId))?.settings,
367
+ session.sandboxBackend,
368
+ )
369
+ : settings;
296
370
  return {
297
371
  settings: {
298
372
  ...logicalSettings,
@@ -1,4 +1,5 @@
1
1
  import {
2
+ canonicalSessionAuthorizationOperation,
2
3
  SessionAuthorizationActor,
3
4
  SessionAuthorizationDecision,
4
5
  SessionAuthorizationListScope,
@@ -10,8 +11,11 @@ import {
10
11
  type SessionAuthorizationTarget,
11
12
  type SessionScopeSubjectId,
12
13
  } from "@opengeni/contracts";
14
+ import { AsyncLocalStorage } from "node:async_hooks";
13
15
  import {
16
+ currentSessionRlsActorIdentityKey,
14
17
  getSessionAuthorityProjection,
18
+ getSessionAuthorizationTargetProjection,
15
19
  getSession,
16
20
  getSessionTurnForAttempt,
17
21
  getSlackInteractionSessionAccessForSession,
@@ -20,9 +24,86 @@ import {
20
24
  type SessionRlsActorContext,
21
25
  } from "@opengeni/db";
22
26
  import type { AppDependencies } from "./dependencies";
27
+ import {
28
+ currentSiteSessionOrigin,
29
+ siteOriginsMatch,
30
+ siteSessionOriginFromMetadata,
31
+ } from "./site-session-origin";
32
+ import { siteAgentMayReachSession } from "./site-agent-clamp";
23
33
 
24
34
  export type SessionAuthorizationDependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
25
35
 
36
+ /**
37
+ * Identical caller-session and attempt reads shared by the authorization
38
+ * checks of one first-party MCP request. Phases bound reuse to checks that run
39
+ * back to back: the route's own checks, then only checks a tool handler starts
40
+ * synchronously on entry. Anything later (a re-check after a wait or a write)
41
+ * reads fresh. Results are keyed by database handle and session RLS actor, so
42
+ * a read is only reused under the exact visibility it was made with.
43
+ */
44
+ type SessionAuthorizationReads = {
45
+ phase: "route" | "handoff" | "dispatch" | "closed";
46
+ readonly results: WeakMap<Database, Map<string, unknown>>;
47
+ };
48
+
49
+ const sessionAuthorizationReads = new AsyncLocalStorage<SessionAuthorizationReads>();
50
+
51
+ /** Scope read reuse to one request; it never outlives `fn`. */
52
+ export async function withSessionAuthorizationReadReuse<T>(
53
+ fn: (reuse: { handOffToToolDispatch(): void }) => Promise<T>,
54
+ ): Promise<T> {
55
+ const reads: SessionAuthorizationReads = { phase: "route", results: new WeakMap() };
56
+ const handOffToToolDispatch = () => {
57
+ if (reads.phase === "route") reads.phase = "handoff";
58
+ };
59
+ try {
60
+ return await sessionAuthorizationReads.run(reads, () => fn({ handOffToToolDispatch }));
61
+ } finally {
62
+ reads.phase = "closed";
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Invoke a tool handler so that authorization checks it starts before its
68
+ * first await reuse the route's reads. Later checks, and every other dispatch
69
+ * in the same request, read fresh.
70
+ */
71
+ export function dispatchWithSessionAuthorizationReadReuse<T>(handler: () => T): T {
72
+ const reads = sessionAuthorizationReads.getStore();
73
+ if (reads?.phase !== "handoff") return handler();
74
+ reads.phase = "dispatch";
75
+ try {
76
+ return handler();
77
+ } finally {
78
+ reads.phase = "closed";
79
+ }
80
+ }
81
+
82
+ /** Call before the first await, so the phase is the one the check started in. */
83
+ function claimSessionAuthorizationReads(): SessionAuthorizationReads | null {
84
+ const reads = sessionAuthorizationReads.getStore();
85
+ return reads?.phase === "route" || reads?.phase === "dispatch" ? reads : null;
86
+ }
87
+
88
+ async function reusableAuthorizationRead<T>(
89
+ reads: SessionAuthorizationReads | null,
90
+ db: Database,
91
+ key: readonly string[],
92
+ read: () => Promise<T>,
93
+ ): Promise<T> {
94
+ if (!reads) return await read();
95
+ const scopedKey = JSON.stringify([...key, currentSessionRlsActorIdentityKey()]);
96
+ let results = reads.results.get(db);
97
+ if (results?.has(scopedKey)) return results.get(scopedKey) as T;
98
+ const value = await read();
99
+ if (!results) {
100
+ results = new Map();
101
+ reads.results.set(db, results);
102
+ }
103
+ results.set(scopedKey, value);
104
+ return value;
105
+ }
106
+
26
107
  /** Maximum time an omitted host hint leaves a live session stream unchecked. */
27
108
  export const SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS = 15_000;
28
109
 
@@ -120,7 +201,6 @@ export function agentAccessListScopeForViewer(
120
201
 
121
202
  type ResolvedSessionAuthorizationTarget = {
122
203
  target: SessionAuthorizationTarget;
123
- parentSessionId: string | null;
124
204
  };
125
205
 
126
206
  /**
@@ -178,7 +258,8 @@ export async function requireLiveAgentAttemptAuthorization(
178
258
  grant: AccessGrant,
179
259
  callerSessionId: string,
180
260
  ): Promise<Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>> {
181
- const { actor } = await resolveSessionAuthorizationActor(db, grant);
261
+ const reads = claimSessionAuthorizationReads();
262
+ const { actor } = await resolveSessionAuthorizationActor(db, grant, reads);
182
263
  if (actor.kind !== "agent_attempt" || actor.callerSessionId !== callerSessionId) {
183
264
  throw new SessionAuthorizationDeniedError("caller_stale");
184
265
  }
@@ -205,16 +286,42 @@ export async function requireSessionAuthorization(
205
286
  surface: SessionAuthorizationSurface;
206
287
  },
207
288
  ): Promise<ResolvedSessionAuthorization | null> {
289
+ const reads = claimSessionAuthorizationReads();
208
290
  const port = deps.sessionAuthorization;
209
291
  const isAgentAttempt = grantHasAgentAttemptAuthority(grant);
210
- const [slackAccess, authority] = await Promise.all([
211
- getSlackInteractionSessionAccessForSession(deps.db, {
212
- accountId: grant.accountId,
213
- workspaceId: grant.workspaceId,
214
- sessionId: input.sessionId,
215
- }),
216
- getSessionAuthorityProjection(deps.db, grant.workspaceId, input.sessionId),
217
- ]);
292
+ // Callers can supply a transaction handle. Both reads open nested RLS
293
+ // savepoints, which must not race on the same PostgreSQL connection.
294
+ const slackAccess = await getSlackInteractionSessionAccessForSession(deps.db, {
295
+ accountId: grant.accountId,
296
+ workspaceId: grant.workspaceId,
297
+ sessionId: input.sessionId,
298
+ });
299
+ const authority = await getSessionAuthorityProjection(
300
+ deps.db,
301
+ grant.workspaceId,
302
+ input.sessionId,
303
+ );
304
+
305
+ // The projection is read under the caller's own RLS context, so a missing
306
+ // session and one the caller may not see (another member's private session)
307
+ // are the same answer. Every request-facing surface refuses both here,
308
+ // before a route-specific read can observe the difference or fail on the
309
+ // absent row. In-process `core` callers keep the standalone null result and
310
+ // their own not-found handling.
311
+ if (!authority && input.surface !== "core") {
312
+ throw new SessionAuthorizationDeniedError("not_found");
313
+ }
314
+
315
+ // A request from a Site page (an HTTP frame request, or a tool the page
316
+ // called) may act on a session only when that same Site, or for a frame
317
+ // without a published Site the same mounted frame, started it. Anything
318
+ // else is indistinguishable from a missing session.
319
+ const siteOrigin = currentSiteSessionOrigin();
320
+ if (siteOrigin && authority && !input.operation.endsWith(".read")) {
321
+ const session = await getSession(deps.db, grant.workspaceId, input.sessionId);
322
+ if (!siteOriginsMatch(siteOrigin, siteSessionOriginFromMetadata(session?.metadata)))
323
+ throw new SessionAuthorizationDeniedError("not_found");
324
+ }
218
325
 
219
326
  // Preserve the standalone workspace-shared path. Private sessions continue
220
327
  // through the durable actor and ownership checks even without a host port.
@@ -228,10 +335,13 @@ export async function requireSessionAuthorization(
228
335
  }
229
336
  if (!authority) throw new SessionAuthorizationDeniedError("not_found");
230
337
 
231
- const [resolvedActor, resolvedTarget] = await Promise.all([
232
- resolveSessionAuthorizationActor(deps.db, grant),
233
- resolveSessionAuthorizationTarget(deps.db, grant, input.sessionId),
234
- ]);
338
+ const resolvedActor = await resolveSessionAuthorizationActor(deps.db, grant, reads);
339
+ const resolvedTarget = await resolveSessionAuthorizationTarget(
340
+ deps.db,
341
+ grant,
342
+ input.sessionId,
343
+ reads,
344
+ );
235
345
  const actor = resolvedActor.actor;
236
346
  const target = resolvedTarget.target;
237
347
  const agentRelatedSessionAccess =
@@ -247,6 +357,23 @@ export async function requireSessionAuthorization(
247
357
  if (actor.kind === "agent_attempt" && input.operation === "session.approval.write") {
248
358
  throw new SessionAuthorizationDeniedError("forbidden");
249
359
  }
360
+ // An agent started from a Site page acts with that page's reach: it may
361
+ // read, message, steer or control only sessions that act for the same Site
362
+ // (its own lineage, or another session that Site started), never a session
363
+ // a person started elsewhere. Reads count too: a transcript is data the
364
+ // Site page must not reach. Indistinguishable from a missing session.
365
+ if (
366
+ actor.kind === "agent_attempt" &&
367
+ target.sessionId !== actor.callerSessionId &&
368
+ !(await siteAgentMayReachSession(
369
+ deps.db,
370
+ { accountId: grant.accountId, workspaceId: grant.workspaceId },
371
+ { sessionId: actor.callerSessionId, turnId: actor.turnId },
372
+ target.sessionId,
373
+ ))
374
+ ) {
375
+ throw new SessionAuthorizationDeniedError("not_found");
376
+ }
250
377
 
251
378
  if (authority.visibility === "user_private") {
252
379
  const allowed =
@@ -290,12 +417,18 @@ export async function requireSessionAuthorization(
290
417
 
291
418
  let rawDecision: unknown;
292
419
  try {
420
+ // Adapt once, before the host decision. Never retry a denial under an alias.
421
+ const operation = canonicalSessionAuthorizationOperation(input.operation);
293
422
  rawDecision = await port.authorizeSession({
294
423
  accountId: grant.accountId,
295
424
  workspaceId: grant.workspaceId,
296
425
  actor,
297
426
  target,
298
- operation: input.operation,
427
+ operation:
428
+ operation === "session.model_account.write" &&
429
+ port.capabilities?.modelAccountOperation !== true
430
+ ? "session.codex_account.write"
431
+ : operation,
299
432
  surface: input.surface,
300
433
  });
301
434
  } catch (error) {
@@ -325,10 +458,11 @@ export async function requireSessionAuthorizationListScope(
325
458
  grant: AccessGrant,
326
459
  surface: SessionAuthorizationSurface,
327
460
  ): Promise<SessionAuthorizationListScope | null> {
461
+ const reads = claimSessionAuthorizationReads();
328
462
  const port = deps.sessionAuthorization;
329
463
  const isAgentAttempt = grantHasAgentAttemptAuthority(grant);
330
464
  if (!port && !isAgentAttempt) return null;
331
- const { actor, callerAccess } = await resolveSessionAuthorizationActor(deps.db, grant);
465
+ const { actor, callerAccess } = await resolveSessionAuthorizationActor(deps.db, grant, reads);
332
466
  // Standalone agents retain compact workspace discovery only while the signed
333
467
  // caller attempt is still the exact live attempt, and only within their own
334
468
  // declared agent-access reach (migration 0427). The viewer is derived from
@@ -357,7 +491,7 @@ export async function requireSessionAuthorizationListScope(
357
491
  if (!parsed.success) {
358
492
  throw new SessionAuthorizationUnavailableError({ cause: parsed.error });
359
493
  }
360
- // A host never supplies the viewer: OpenGeni resolved it above from durable
494
+ // A host never supplies the viewer: Opengeni resolved it above from durable
361
495
  // state, so a host-returned value is dropped before the intersection.
362
496
  const hostScope: SessionAuthorizationListScope =
363
497
  parsed.data.kind === "all"
@@ -374,19 +508,25 @@ async function resolveSessionAuthorizationTarget(
374
508
  db: Database,
375
509
  grant: AccessGrant,
376
510
  sessionId: string,
511
+ reads: SessionAuthorizationReads | null,
377
512
  ): Promise<ResolvedSessionAuthorizationTarget> {
378
- const session = await getSession(db, grant.workspaceId, sessionId);
513
+ const session = await reusableAuthorizationRead(
514
+ reads,
515
+ db,
516
+ ["target-identity", grant.workspaceId, sessionId],
517
+ () => getSessionAuthorizationTargetProjection(db, grant.workspaceId, sessionId),
518
+ );
379
519
  if (!session || session.accountId !== grant.accountId) {
380
520
  throw new SessionAuthorizationDeniedError("not_found");
381
521
  }
382
522
  return {
383
523
  target: { sessionId: session.id, rootSessionId: session.rootSessionId },
384
- parentSessionId: session.parentSessionId,
385
524
  };
386
525
  }
387
526
  async function resolveSessionAuthorizationActor(
388
527
  db: Database,
389
528
  grant: AccessGrant,
529
+ reads: SessionAuthorizationReads | null,
390
530
  ): Promise<ResolvedSessionAuthorizationActor> {
391
531
  const callerSessionId = grant.metadata?.["sessionId"];
392
532
  const turnId = grant.metadata?.["turnId"];
@@ -414,10 +554,19 @@ async function resolveSessionAuthorizationActor(
414
554
  ) {
415
555
  throw new SessionAuthorizationDeniedError("caller_stale");
416
556
  }
417
- const [callerSession, turn] = await Promise.all([
418
- getSession(db, grant.workspaceId, callerSessionId),
419
- getSessionTurnForAttempt(db, grant.workspaceId, callerSessionId, attemptId),
420
- ]);
557
+ // These scoped reads can also open nested savepoints on a transaction handle.
558
+ const callerSession = await reusableAuthorizationRead(
559
+ reads,
560
+ db,
561
+ ["session", grant.workspaceId, callerSessionId],
562
+ () => getSession(db, grant.workspaceId, callerSessionId),
563
+ );
564
+ const turn = await reusableAuthorizationRead(
565
+ reads,
566
+ db,
567
+ ["turn", grant.workspaceId, callerSessionId, attemptId],
568
+ () => getSessionTurnForAttempt(db, grant.workspaceId, callerSessionId, attemptId),
569
+ );
421
570
  if (
422
571
  !callerSession ||
423
572
  callerSession.accountId !== grant.accountId ||